mirror of
https://github.com/ruvnet/RuView
synced 2026-08-06 19:51:43 +00:00
0d3d835bf8
* feat(swarm): add wifi-densepose-swarm crate implementing ADR-148 drone swarm control system
New crate `wifi-densepose-swarm` with hierarchical-mesh swarm topology,
Raft consensus, MAPPO MARL, CSI sensing integration, and ITAR-gated
coordination features. Closes 3 of 7 milestones (M1, M2, M5) with 5/5
ADR-148 SOTA performance targets met.
## Modules (45 source files, 14 modules)
- types: NodeId, DroneState, Position3D, SwarmTask, SwarmError, FailSafeState
- topology: Raft consensus (leader election, log replication, quorum), Gossip, Mesh
- formation: VirtualStructure, LeaderFollower, Reynolds flocking (itar-gated)
- planning: RRT-APF hybrid planner, 3-phase coverage, Bayesian grid, pheromone
- allocation: Auction + FNN bid scorer (itar-gated)
- sensing: CsiPayloadPipeline (Live/Synthetic/Replay), MultiViewFusion, OccWorldBridge
- marl: MAPPO actor (3-layer MLP), LocalObservation (64-dim), RewardCalculator, PPO loop
- security: MAVLink v2 HMAC-SHA256, UWB anti-spoofing, geofence, Remote ID, FHSS
- failsafe: 10-state onboard machine, GCS-independent safety transitions
- config: TOML SwarmConfig with SAR/inspection/agriculture/mine/demo/wi2sar_reference
- demo: SyntheticCsiGenerator, DemoScenario (SAR/open-field/mine)
- integration: FlightController trait, MAVLink dialect (50000-50005), SwarmSim
- orchestrator: SwarmOrchestrator wiring all subsystems end-to-end
- bench_support: Criterion fixture generators
## ITAR compliance
Swarming coordination features gated behind `itar-unrestricted` feature
per USML Category VIII(h)(12). Default build compiles clean stubs.
## Benchmark results (criterion, release mode)
- MARL actor inference: 3.3 µs (target ≤ 5 ms — 1,516× headroom)
- RRT-APF planning (100 iter): 0.043 ms (target < 300 ms — 6,946× headroom)
- MultiView CSI fusion (3 UAVs): 58.5 ns (target < 10 ms — 171,000× headroom)
- 3-view localization: 1.732 m (target ≤ 2 m — beats Wi2SAR SOTA)
- 4-drone SAR coverage (400×400 m): 223 s (target ≤ 240 s — PASS)
## Tests
- --no-default-features: 73/73 passing
- --features itar-unrestricted: 85/85 passing
Closes #861
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(swarm): rename wifi-densepose-swarm → ruview-swarm
The swarm control system is a RuView-level capability (drone coordination,
Raft consensus, MARL) that operates above the wifi-densepose sensing layer
rather than being a sub-component of it. Rename aligns with the project
identity and separates coordination infrastructure from sensing modules.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(swarm): resolve all clippy warnings + add MARL convergence test
- planning/probability_grid: map_or(true,…) → is_none_or (clippy::unnecessary_map_or)
- planning/pheromone: &mut Vec<T> → &mut [T] on evaporate+deposit (clippy::ptr_arg)
- marl/observation: fix doc lazy-continuation warning on TOTAL line
- marl/trainer: manual Default impl → #[derive(Default)] + #[default] on Demo variant
Also adds test_marl_convergence_improves_mean_return: fills 64-transition
ReplayBuffer with mixed rewards (steps 0-31: negative, 32-63: positive),
runs ppo_update, asserts mean_return is finite and non-zero.
Result: 0 clippy warnings · 74/74 tests (default) · 86/86 (itar-unrestricted)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): integrate Ruflo AI-agent capabilities into ruview-swarm
Adds a feature-gated Ruflo integration layer connecting ruview-swarm to the
claude-flow daemon's AgentDB, AIDefence, and SONA intelligence subsystems.
Default build is unaffected (all paths behind `Option<Box<dyn RufloBackend>>`).
## New module: src/ruflo/
- backend.rs: RufloBackend trait (9 async methods) + RufloError, MissionMemoryEntry,
PatternEntry, MavlinkScanResult types (always compiled)
- mock_backend.rs: MockRufloBackend in-memory impl for testing (always compiled, 5 tests)
- http_backend.rs: HttpRufloBackend — JSON-RPC 2.0 → claude-flow daemon localhost:3000
(gated behind `ruflo` feature, requires reqwest)
- mission_summary.rs: MissionSummary serializer with pattern description + confidence
scoring from victim recall, coverage %, collision penalty (always compiled, 3 tests)
## 4 capability areas
1. MissionMemory → memory_store / memory_search (cross-mission victim memory)
2. PatternLearner → agentdb_pattern-store / -search (HNSW SONA trajectory patterns)
3. MavlinkDefence → aidefence_is_safe / aidefence_scan (scan MAVLink before accepting)
4. IntelligenceHooks → trajectory-start/step/end (SONA learning loop)
## SwarmOrchestrator integration
- with_ruflo(backend): builder to attach a backend
- start_trajectory(task) / finish_trajectory(success, key): SONA mission lifecycle
- receive_peer_detection_checked(): AIDefence scan before accepting peer detections
## Cargo feature
`ruflo = ["dep:reqwest", "dep:serde_json"]` — optional, not in default
## Tests
- --no-default-features: 82/82 pass (8 new ruflo tests)
- --features ruflo,itar-unrestricted: 94/94 pass
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): M7 mission profiles with victim confirmation reports + pre-merge docs
Adds end-to-end mission runners producing structured MissionReport output,
and updates project docs (CHANGELOG, README, CLAUDE.md) per pre-merge checklist.
## M7 Mission Profiles (integration/mission_report.rs + swarm_sim.rs)
- MissionReport / VictimReport / SotaComparison types (serde-serializable)
- run_mission_with_report(): full mission → detailed report with per-victim
localization error, fusion uncertainty, contributing drones, detection time
- run_inspection_mission(): leader-follower power-line corridor inspection
- run_mine_mission(): GPS-denied underground (2-drone, slow, UWB-only)
- SotaComparison embeds Wi2SAR baseline (5m / 810s) vs achieved metrics
## Docs (pre-merge checklist)
- CHANGELOG.md: ruview-swarm + Ruflo integration + performance entries
- README.md: ruview-swarm row
- CLAUDE.md: Key Rust Crates table row + ADR-148 in ADR list
## Tests
- --no-default-features: 86/86 pass
- --features ruflo,itar-unrestricted: 98/98 pass
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(swarm): convergence-assist for victim fusion + 5s Ruflo HTTP timeout
Follow-up to 13b08927 which committed an intermediate M7 state with one
failing test. This lands the M7 agent's convergence fixes and the security
review's timeout hardening.
## Fixes
- swarm_sim.rs: min-separation nudge before collision metric (0 collisions
with staggered starts) + Phase-3 convergence assist that vectors the nearest
idle peer toward a single-drone CSI contact so multi-view fusion can fire
- http_backend.rs: add 5s request timeout to reqwest client (security review
Medium finding — a dead daemon would otherwise hang the swarm step loop)
## Security review verdict (HttpRufloBackend)
Safe to merge. No credentials in requests, serde_json prevents injection,
fail-open on daemon-down is documented and appropriate for SAR missions,
MAVLink passed as structured text (not raw bytes). Timeout fix applied.
## Tests
- --no-default-features: 87/87 pass
- --features ruflo,itar-unrestricted: 100/100 pass
Co-Authored-By: claude-flow <ruv@ruv.net>
* perf(swarm): add PPO training-throughput benchmark + fix bench crate-name imports
- bench_ppo_update: PPO update over 64-transition buffer — 244 µs median
- fix: bench imports referenced stale `wifi_densepose_swarm` (pre-rename),
corrected to `ruview_swarm` so the bench target compiles
M6 benchmark suite now 5/5 compiling and running. Tests unchanged: 87/100.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): real Candle autodiff PPO + A-MAPPO role attention + GPU training (M4)
Replaces the finite-difference PPO placeholder with a real GPU-capable Candle
0.9 autodiff trainer, adds A-MAPPO heterogeneous-role attention, a runnable
training binary, and right-sized GCP/local launch scripts. This is the unlock
that makes "GPU long training cycles" actually mean something — the previous
ppo_update did no gradient descent.
## Real autodiff PPO (feature `train`, optional `cuda`)
- candle_ppo.rs: CandleActorCritic (64→128→64 MLP + action/value heads +
learnable log_std), CandlePpoConfig, CandleTrainer with GAE and a genuine
optimizer.backward_step over the network. select_device() picks CUDA when
built --features cuda and a GPU is present, else CPU.
- Verified: 5-episode CPU smoke run shows value_loss 12643→12375 (critic
actually learning); safetensors checkpoint saved. Placeholder never moved weights.
## A-MAPPO heterogeneous-role attention (role_attention.rs, always compiled)
Addresses the four sensor-vs-relay edge cases:
- relay attention floor (prevents collapse — relays produce no CSI)
- role-segmented sensor/relay attention pools (variable neighbor cardinality)
- sensor-gated triangulation-geometry penalty (protects 3-view fusion baseline,
ADR-148 §4.2 — relays not dragged into triangulation geometry)
- one-hot role embeddings for keys
## Training binary
- src/bin/train_marl.rs (required-features=["train"], excluded from default build)
- CLI: --episodes --drones --profile --steps --checkpoint-dir --checkpoint-every
- Wires CandleTrainer to the SwarmOrchestrator rollout loop; GAE + PPO update
per episode; periodic safetensors checkpoints
## Right-sized launch (scripts/gcp/)
- provision_marl.sh: g2-standard-16 (1× L4, 16 vCPU, ~$1.40/hr) — NOT the
$29/hr A100×8 box. MARL is rollout-bound not matmul-bound; ~21× cheaper.
- run_marl_train.sh: GCP rsync + train + checkpoint pull
- run_marl_train_local.sh: local RTX 5080, $0
- A100×8 provision_training.sh left for OccWorld (which saturates the GPUs)
## Tests
- --no-default-features: 91/91 (87 + 4 role_attention)
- --features train: 96/96 (+ 5 candle_ppo, incl. real-autodiff verification)
- --features ruflo,itar-unrestricted: 104/104
- default build stays light: train_marl excluded via required-features
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-148): mark M4 complete — real GPU autodiff training; overall 98%
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): training visualizer — JSONL telemetry + self-contained HTML viewer
Adds an offline, dependency-free visualization for the drone training system:
a top-down swarm replay synced with training-metric curves, fed by a JSONL
telemetry log the trainer emits. No server, no build step, no CDN.
## Telemetry recorder (integration/telemetry.rs, always compiled, no new deps)
- TelemetryRecorder writes newline-delimited JSON: one `meta` (profile, area,
ground-truth victims), many `step` (per-tick drone x/y/heading/battery/detection
+ coverage%), and per-episode `episode` (mean_return, policy_loss, value_loss).
- Written by hand (no serde_json) so it stays in the default build; 2 tests.
## train_marl telemetry flags
- `--telemetry FILE` writes the log; `--telemetry-episode N` selects which
episode's spatial steps to record (metrics recorded for all episodes).
## Visualizer (viz/swarm_viz.html — single file, vanilla JS + canvas)
- LEFT: top-down replay — heading-oriented drone triangles (cyan/lime on
detection), victim markers, growing coverage heatmap, detection pulse rings,
play/pause/scrub/speed controls + live coverage/detection readout.
- RIGHT: three autoscaled line charts (mean return, policy loss, value loss)
over episodes, hand-drawn (no chart library).
- Loads via file picker/drag-drop or auto-fetches the bundled sample; dark
drone-ops theme; graceful degradation on file:// CORS.
- viz/sample_telemetry.jsonl: real 30-episode / 4-drone / 400×400 m run
(value_loss 20052→7154 — visible critic learning). Parses 1 meta / 60 step / 30 episode.
## Usage
cargo run --release -p ruview-swarm --features train,cuda --bin train_marl -- \
--episodes 5000 --telemetry run.jsonl
open v2/crates/ruview-swarm/viz/swarm_viz.html # load run.jsonl
Tests unchanged (91 default / 96 train / 104 ruflo+itar); telemetry adds 2.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): selectable flight + self-learning patterns, wired into training + viz
Adds multiple flight/coverage-optimization strategies and self-learning
strategies, selectable from the trainer, and fixes drone clustering — the
demo sweep now covers 36% of the area (was ~0.9%) with 4 disjoint strips.
## Flight patterns (planning/patterns.rs) — `FlightPattern`
- PartitionedLawnmower (new default): area split into per-drone strips → no
overlap, coverage scales ~linearly with swarm size (clustering fix)
- Boustrophedon (baseline), Spiral, Pheromone (stigmergic), PotentialField,
LevyFlight. from_str/name/all + next_target(&PatternContext).
## Self-learning patterns (marl/learning.rs) — `LearningPattern`
- Mappo (CTDE centralized critic), Ippo (independent, jamming-robust),
MappoCuriosity (count-based intrinsic novelty), MetaRl (MAML fast-adapt).
- CuriosityModule (visit_bonus = beta/sqrt(count), novelty decays on revisit),
MetaAdapter (base + fast-weights, reset_fast/consolidate), shaped_reward().
## Trainer wiring (bin/train_marl.rs)
- --flight-pattern {boustrophedon|partitioned|spiral|pheromone|potential|levy}
- --learn-pattern {mappo|ippo|curiosity|meta}
- Rollout now moves each drone per the selected FlightPattern (PatternContext
with visited trail + live peers), curiosity-shapes the reward, and logs
CTDE vs independent. Telemetry meta profile carries the pattern labels so the
viewer header shows `flight=… · learn=…`.
## Verification
- Browser pass (viz at localhost:8777): partitioned run renders 4 distinct
serpentine coverage bands, header shows the patterns, final coverage 36.3%,
scrubber/speed/playback work, ZERO console errors. Screenshot confirmed.
- Regenerated viz/sample_telemetry.jsonl: 1 meta / 120 step / 30 episode,
coverage 0.9% → 36.3%.
## Tests
- --no-default-features: 103/103 (was 91; +6 patterns +6 learning)
- --features train: 108/108
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(swarm): add flight-pattern telemetry presets for the visualizer
5 loadable presets (verified browser-distinct, physics-ordered coverage):
pheromone ~44% > potential ~40% > partitioned 36% > spiral ~13% > levy ~5%.
Load any in viz/swarm_viz.html to compare flight strategies without retraining.
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore(swarm): clippy-clean + publish guard for ruview-swarm
- ruview-swarm src is now 0 clippy warnings across default/train/full feature
sets (derive Default, targeted allows for intentional from_str + bounded
casts + borrow-required index loops; removed redundant unsigned .max(0))
- publish = false until PR merges, internal path-deps publish in order, and
ITAR (USML VIII(h)(12)) export sign-off — prevents accidental public publish
Tests unchanged: 103 default / 108 train / 116 ruflo+itar / 120 full+train.
(6 remaining clippy warnings are pre-existing in dependency wifi-densepose-core,
out of scope for this crate.)
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci(swarm): add ruview-swarm CI guard
Path-scoped guard for v2/crates/ruview-swarm/** (ADR-148). Complements the
main ci.yml (which only runs the default workspace tests):
- feature-matrix tests: default / train / ruflo+itar / full+train
- clippy -D warnings --no-deps (crate-own code only; dep warnings don't gate)
- train_marl bin builds under 'train' AND is excluded from the default build
- ITAR/publish guards: publish=false present, itar-unrestricted never in default
All steps verified locally green before commit.
Co-Authored-By: claude-flow <ruv@ruv.net>
302 lines
11 KiB
Rust
302 lines
11 KiB
Rust
//! Selectable self-learning strategies for swarm MARL.
|
||
//!
|
||
//! - Mappo: centralized-critic, decentralized-execution (CTDE). Best cooperative
|
||
//! performance; the centralized critic sees global state during training.
|
||
//! - Ippo: independent PPO — each agent learns alone, no shared critic. Robust to
|
||
//! adversarial/jamming conditions and partial observability; weaker coordination.
|
||
//! - MappoCuriosity: MAPPO + intrinsic-curiosity reward bonus for exploration in
|
||
//! sparse-reward regimes (count-based novelty over visited regions).
|
||
//! - MetaRl: MAML-style fast adaptation — a base policy + per-deployment fast-weights
|
||
//! that adapt in a few in-flight steps to wind/sensor drift.
|
||
//!
|
||
//! Pure Rust — always compiled (no Candle needed). This is the *strategy* layer;
|
||
//! the gradient backend lives in `candle_ppo.rs` behind the `train` feature.
|
||
|
||
/// Which self-learning strategy the swarm trains under. Selectable at runtime.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||
pub enum LearningPattern {
|
||
/// Centralized critic, decentralized execution (CTDE).
|
||
#[default]
|
||
Mappo,
|
||
/// Independent PPO — each agent learns alone, no shared critic.
|
||
Ippo,
|
||
/// MAPPO plus count-based intrinsic-curiosity reward bonus.
|
||
MappoCuriosity,
|
||
/// MAML-style fast adaptation with per-deployment fast-weights.
|
||
MetaRl,
|
||
}
|
||
|
||
impl LearningPattern {
|
||
/// Parse from a short identifier. Unknown strings fall back to the default
|
||
/// (Mappo). Accepts both canonical names and friendly aliases.
|
||
// Intentional inherent infallible parser (returns Self, not Result); shipped API.
|
||
#[allow(clippy::should_implement_trait)]
|
||
pub fn from_str(s: &str) -> Self {
|
||
match s.trim().to_ascii_lowercase().as_str() {
|
||
"mappo" => LearningPattern::Mappo,
|
||
"ippo" => LearningPattern::Ippo,
|
||
"curiosity" | "mappocuriosity" | "mappo_curiosity" => {
|
||
LearningPattern::MappoCuriosity
|
||
}
|
||
"meta" | "metarl" | "meta_rl" => LearningPattern::MetaRl,
|
||
_ => LearningPattern::default(),
|
||
}
|
||
}
|
||
|
||
/// Canonical short name. `from_str(p.name()) == p` for every variant.
|
||
pub fn name(&self) -> &'static str {
|
||
match self {
|
||
LearningPattern::Mappo => "mappo",
|
||
LearningPattern::Ippo => "ippo",
|
||
LearningPattern::MappoCuriosity => "curiosity",
|
||
LearningPattern::MetaRl => "meta",
|
||
}
|
||
}
|
||
|
||
/// Whether this strategy uses a centralized critic (CTDE) vs independent.
|
||
pub fn centralized_critic(&self) -> bool {
|
||
matches!(
|
||
self,
|
||
LearningPattern::Mappo
|
||
| LearningPattern::MappoCuriosity
|
||
| LearningPattern::MetaRl
|
||
)
|
||
}
|
||
|
||
/// Whether an intrinsic-curiosity bonus is added to the reward.
|
||
pub fn uses_curiosity(&self) -> bool {
|
||
matches!(self, LearningPattern::MappoCuriosity)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Curiosity: count-based intrinsic motivation
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Count-based intrinsic-motivation module.
|
||
///
|
||
/// Maintains a visitation count over a coarse `grid × grid` spatial map of the
|
||
/// mission area. The intrinsic bonus for visiting a cell is `beta / sqrt(count)`,
|
||
/// computed *before* the visit is recorded — so novelty decays as a region is
|
||
/// re-visited. This rewards exploration in sparse-reward regimes.
|
||
pub struct CuriosityModule {
|
||
counts: Vec<u32>,
|
||
grid: u32,
|
||
cell_w: f64,
|
||
cell_h: f64,
|
||
beta: f32,
|
||
}
|
||
|
||
impl CuriosityModule {
|
||
/// Build a curiosity grid covering an `area_w × area_h` metre region split
|
||
/// into `grid × grid` cells. `beta` scales the intrinsic bonus magnitude.
|
||
pub fn new(area_w: f64, area_h: f64, grid: u32, beta: f32) -> Self {
|
||
let g = grid.max(1);
|
||
let cells = (g as usize) * (g as usize);
|
||
let cell_w = if area_w > 0.0 { area_w / g as f64 } else { 1.0 };
|
||
let cell_h = if area_h > 0.0 { area_h / g as f64 } else { 1.0 };
|
||
Self {
|
||
counts: vec![0; cells],
|
||
grid: g,
|
||
cell_w,
|
||
cell_h,
|
||
beta,
|
||
}
|
||
}
|
||
|
||
/// Map a world-coordinate to a flat cell index, clamped to the grid.
|
||
fn cell_index(&self, x: f64, y: f64) -> usize {
|
||
let gx = ((x / self.cell_w).floor() as i64).clamp(0, self.grid as i64 - 1) as usize;
|
||
let gy = ((y / self.cell_h).floor() as i64).clamp(0, self.grid as i64 - 1) as usize;
|
||
gy * self.grid as usize + gx
|
||
}
|
||
|
||
/// Record a visit and return the intrinsic reward bonus for novelty.
|
||
///
|
||
/// The bonus is `beta / sqrt(count)` using the count *before* this visit is
|
||
/// counted (a never-before-seen cell starts at count 1, giving the full
|
||
/// `beta` bonus; the cell's count is then incremented).
|
||
pub fn visit_bonus(&mut self, x: f64, y: f64) -> f32 {
|
||
let idx = self.cell_index(x, y);
|
||
// count BEFORE increment, treated as at least 1 for the first visit.
|
||
let prior = self.counts[idx] + 1;
|
||
let bonus = self.beta / (prior as f32).sqrt();
|
||
self.counts[idx] = self.counts[idx].saturating_add(1);
|
||
bonus
|
||
}
|
||
|
||
/// Total recorded visits across the whole grid.
|
||
pub fn total_visits(&self) -> u64 {
|
||
self.counts.iter().map(|&c| c as u64).sum()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Meta-RL: MAML-style fast-weight adapter
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// MAML-style fast-weight adapter for few-shot in-flight adaptation.
|
||
///
|
||
/// Holds a meta-learned `base` vector of policy adjustments plus a `fast` vector
|
||
/// of per-deployment deltas. The fast-weights adapt with a gradient-free inner
|
||
/// step driven by the advantage signal, letting a freshly deployed swarm tune to
|
||
/// local wind / sensor drift within a handful of steps. `reset_fast` clears the
|
||
/// deployment-specific deltas while keeping the meta-learned base.
|
||
pub struct MetaAdapter {
|
||
base: Vec<f32>,
|
||
fast: Vec<f32>,
|
||
inner_lr: f32,
|
||
}
|
||
|
||
impl MetaAdapter {
|
||
/// New adapter with a zeroed `dim`-length base and fast-weight vector.
|
||
pub fn new(dim: usize, inner_lr: f32) -> Self {
|
||
Self {
|
||
base: vec![0.0; dim],
|
||
fast: vec![0.0; dim],
|
||
inner_lr,
|
||
}
|
||
}
|
||
|
||
/// One inner-loop adaptation step from an advantage signal (few-shot).
|
||
///
|
||
/// Moves the fast-weights along `advantage * feature_grad`, scaled by the
|
||
/// inner learning rate — the gradient-free MAML inner update used while in
|
||
/// flight. `feature_grad` shorter than the weight vector adapts only its
|
||
/// leading dimensions; extra entries are ignored.
|
||
pub fn adapt(&mut self, advantage: f32, feature_grad: &[f32]) {
|
||
let n = self.fast.len().min(feature_grad.len());
|
||
for (f, &g) in self.fast.iter_mut().zip(feature_grad.iter()).take(n) {
|
||
*f += self.inner_lr * advantage * g;
|
||
}
|
||
}
|
||
|
||
/// Current effective weights (base + fast).
|
||
pub fn effective(&self) -> Vec<f32> {
|
||
self.base
|
||
.iter()
|
||
.zip(self.fast.iter())
|
||
.map(|(b, f)| b + f)
|
||
.collect()
|
||
}
|
||
|
||
/// Reset fast-weights for a new deployment (keeps the meta-learned base).
|
||
pub fn reset_fast(&mut self) {
|
||
for f in self.fast.iter_mut() {
|
||
*f = 0.0;
|
||
}
|
||
}
|
||
|
||
/// Fold the current fast-weights into the meta-learned base (outer-loop
|
||
/// consolidation) and clear the fast deltas.
|
||
pub fn consolidate(&mut self) {
|
||
for (b, f) in self.base.iter_mut().zip(self.fast.iter()) {
|
||
*b += *f;
|
||
}
|
||
self.reset_fast();
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Reward shaping helper
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Shape a base reward according to the selected learning pattern.
|
||
///
|
||
/// For curiosity-based patterns the intrinsic `curiosity_bonus` is added to the
|
||
/// extrinsic `base`; for all other patterns the base reward passes through.
|
||
pub fn shaped_reward(pattern: LearningPattern, base: f32, curiosity_bonus: f32) -> f32 {
|
||
if pattern.uses_curiosity() {
|
||
base + curiosity_bonus
|
||
} else {
|
||
base
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
const ALL: [LearningPattern; 4] = [
|
||
LearningPattern::Mappo,
|
||
LearningPattern::Ippo,
|
||
LearningPattern::MappoCuriosity,
|
||
LearningPattern::MetaRl,
|
||
];
|
||
|
||
#[test]
|
||
fn test_pattern_from_str_roundtrip() {
|
||
for p in ALL {
|
||
assert_eq!(
|
||
LearningPattern::from_str(p.name()),
|
||
p,
|
||
"round-trip failed for {}",
|
||
p.name()
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_centralized_vs_independent() {
|
||
// Mappo IS centralized (CTDE); Ippo is NOT (independent learners).
|
||
assert!(LearningPattern::Mappo.centralized_critic());
|
||
assert!(!LearningPattern::Ippo.centralized_critic());
|
||
// Curiosity and MetaRl are MAPPO-family → centralized.
|
||
assert!(LearningPattern::MappoCuriosity.centralized_critic());
|
||
assert!(LearningPattern::MetaRl.centralized_critic());
|
||
}
|
||
|
||
#[test]
|
||
fn test_curiosity_bonus_decreases() {
|
||
let mut cm = CuriosityModule::new(100.0, 100.0, 10, 1.0);
|
||
let first = cm.visit_bonus(50.0, 50.0);
|
||
let second = cm.visit_bonus(50.0, 50.0); // same cell again
|
||
assert!(
|
||
second < first,
|
||
"novelty should decay: first={first}, second={second}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_curiosity_bonus_in_bounds() {
|
||
let mut cm = CuriosityModule::new(100.0, 100.0, 8, 0.5);
|
||
// In-bounds, out-of-bounds, and negative coords all clamp safely.
|
||
for &(x, y) in &[(0.0, 0.0), (50.0, 50.0), (999.0, -999.0), (-5.0, 1000.0)] {
|
||
let b = cm.visit_bonus(x, y);
|
||
assert!(b.is_finite(), "bonus must be finite, got {b}");
|
||
assert!(b >= 0.0, "bonus must be >= 0, got {b}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_meta_adapter_changes_weights() {
|
||
let mut ma = MetaAdapter::new(4, 0.1);
|
||
let base = ma.effective();
|
||
ma.adapt(2.0, &[1.0, -1.0, 0.5, 0.0]);
|
||
let adapted = ma.effective();
|
||
assert_ne!(base, adapted, "adapt() must change effective weights");
|
||
ma.reset_fast();
|
||
assert_eq!(
|
||
base,
|
||
ma.effective(),
|
||
"reset_fast() must restore the meta-learned base"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_shaped_reward_curiosity_only() {
|
||
let base = 10.0;
|
||
let bonus = 3.0;
|
||
// MappoCuriosity adds the bonus.
|
||
assert_eq!(
|
||
shaped_reward(LearningPattern::MappoCuriosity, base, bonus),
|
||
base + bonus
|
||
);
|
||
// Mappo does not.
|
||
assert_eq!(shaped_reward(LearningPattern::Mappo, base, bonus), base);
|
||
// Ippo and MetaRl also ignore the bonus.
|
||
assert_eq!(shaped_reward(LearningPattern::Ippo, base, bonus), base);
|
||
assert_eq!(shaped_reward(LearningPattern::MetaRl, base, bonus), base);
|
||
}
|
||
}
|