Skip to main content

thrust_rl/multi_agent/
messages.rs

1//! Message types for multi-agent coordination.
2//!
3//! Defines the per-agent message formats exchanged between the components
4//! of a population-based / threaded multi-agent training setup
5//! (simulator threads producing experience, learner threads consuming
6//! it). After the Burn migration the message payloads are all plain
7//! `Vec<f32>` / `Vec<i64>` host data — no tensor types cross the channel
8//! boundary, which keeps the producer and consumer free to choose
9//! different Burn backends if they want to.
10//!
11//! # Scope
12//!
13//! Only the data carriers and control envelope live here. The threaded
14//! learner / simulator implementations that consume these messages live
15//! one layer up (and are intentionally minimal in this first Burn-native
16//! port — see [`crate::multi_agent`] for the module-level scope).
17
18/// Unique identifier for an agent across a multi-agent training run.
19pub type AgentId = usize;
20
21/// Experience tuple sent from a simulator to a learner.
22///
23/// All tensor payloads from the pre-Burn (`tch`-coupled) implementation are
24/// replaced with plain `Vec<f32>` host buffers. Construct Burn tensors at
25/// the learner-side rollout buffer with `Tensor::from_floats(...)` once
26/// the buffer is full.
27#[derive(Debug, Clone)]
28pub struct Experience {
29    /// Agent that generated this experience.
30    pub agent_id: AgentId,
31
32    /// Observation vector `[obs_dim]`.
33    pub observation: Vec<f32>,
34
35    /// Action taken. Length 1 for scalar discrete; `num_dims` for
36    /// multi-discrete (matches
37    /// [`crate::multi_agent::environment::MultiAgentEnvironment::agent_action_space`]).
38    pub action: Vec<i64>,
39
40    /// Reward received.
41    pub reward: f32,
42
43    /// Next observation vector `[obs_dim]`.
44    pub next_observation: Vec<f32>,
45
46    /// Whether episode terminated (natural episode end).
47    pub terminated: bool,
48
49    /// Whether episode was truncated (time limit / external reset).
50    pub truncated: bool,
51
52    /// Value estimate at this state (from the rollout-time policy).
53    pub value: f32,
54
55    /// Log probability of the action taken under the rollout-time policy.
56    pub log_prob: f32,
57}
58
59impl Experience {
60    /// Create a new experience tuple.
61    #[allow(clippy::too_many_arguments)]
62    pub fn new(
63        agent_id: AgentId,
64        observation: Vec<f32>,
65        action: Vec<i64>,
66        reward: f32,
67        next_observation: Vec<f32>,
68        terminated: bool,
69        truncated: bool,
70        value: f32,
71        log_prob: f32,
72    ) -> Self {
73        Self {
74            agent_id,
75            observation,
76            action,
77            reward,
78            next_observation,
79            terminated,
80            truncated,
81            value,
82            log_prob,
83        }
84    }
85
86    /// Check if this experience marks the end of an episode
87    /// (terminated or truncated).
88    pub fn is_done(&self) -> bool {
89        self.terminated || self.truncated
90    }
91}
92
93/// Policy update message sent from a learner back to a simulator.
94///
95/// Pre-Burn this carried a path to a saved `tch` model file so the
96/// simulator could reload via `VarStore`. Post-Burn the same pattern
97/// works: the learner saves a Burn `BinFileRecorder` checkpoint and
98/// publishes the path. The simulator may also pull updates in-process
99/// by `clone()`ing the learner's [`burn::module::Module`] directly —
100/// this struct is the cross-thread path.
101#[derive(Debug, Clone)]
102pub struct PolicyUpdate {
103    /// Agent whose policy was updated.
104    pub agent_id: AgentId,
105
106    /// New policy version number (monotonically increasing).
107    pub version: u64,
108
109    /// Path to saved model file (learner saves, simulator loads).
110    /// Avoids sending large parameter blobs through channels.
111    pub model_path: String,
112
113    /// Training statistics for logging.
114    pub stats: TrainingStats,
115}
116
117/// Training statistics from a policy update.
118///
119/// Mirrors [`crate::train::ppo::TrainingStats`] field-for-field for the
120/// quantities a simulator typically needs to log. Stays a separate
121/// struct so the multi-agent message surface does not pull in the
122/// per-update aggregator.
123#[derive(Debug, Clone)]
124pub struct TrainingStats {
125    /// Total loss (weighted sum of policy / value / entropy / aux).
126    pub total_loss: f64,
127
128    /// Policy loss component.
129    pub policy_loss: f64,
130
131    /// Value loss component.
132    pub value_loss: f64,
133
134    /// Entropy bonus.
135    pub entropy: f64,
136
137    /// KL divergence (for monitoring).
138    pub kl_divergence: f64,
139
140    /// Number of gradient updates completed.
141    pub step: usize,
142
143    /// Average episode reward (if available).
144    pub avg_reward: Option<f64>,
145}
146
147impl Default for TrainingStats {
148    fn default() -> Self {
149        Self {
150            total_loss: 0.0,
151            policy_loss: 0.0,
152            value_loss: 0.0,
153            entropy: 0.0,
154            kl_divergence: 0.0,
155            step: 0,
156            avg_reward: None,
157        }
158    }
159}
160
161/// Control message for coordinating training.
162///
163/// The Burn-native receivers handle these as best-effort hints — there
164/// is no global broadcast required to participate in the multi-agent
165/// surface, but a runner that wants to support checkpointing and
166/// learning-rate scheduling can plumb this enum through its channel
167/// network.
168#[derive(Debug, Clone)]
169pub enum ControlMessage {
170    /// Stop training and shut down.
171    Shutdown,
172
173    /// Save checkpoint.
174    SaveCheckpoint {
175        /// Destination path for the checkpoint blob. Format is determined
176        /// by the receiving learner; typical is a Burn
177        /// `BinFileRecorder` `.bin` file.
178        path: String,
179    },
180
181    /// Load checkpoint.
182    LoadCheckpoint {
183        /// Source path of the checkpoint blob to restore into the
184        /// receiving learner's module.
185        path: String,
186    },
187
188    /// Adjust learning rate.
189    SetLearningRate {
190        /// New learning rate (replaces the optimizer's current rate;
191        /// not applied as a delta). Effective on the next optimizer
192        /// step.
193        rate: f64,
194    },
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_experience_creation() {
203        let exp = Experience::new(
204            0,
205            vec![0.1, 0.2, 0.3, 0.4],
206            vec![1],
207            1.0,
208            vec![0.5, 0.6, 0.7, 0.8],
209            false,
210            false,
211            0.5,
212            -0.69,
213        );
214
215        assert_eq!(exp.agent_id, 0);
216        assert_eq!(exp.action, vec![1]);
217        assert_eq!(exp.reward, 1.0);
218        assert!(!exp.is_done());
219    }
220
221    #[test]
222    fn test_experience_done() {
223        let make = |term, trunc| {
224            Experience::new(0, vec![0.0; 4], vec![1], 1.0, vec![0.0; 4], term, trunc, 0.5, -0.69)
225        };
226
227        assert!(make(true, false).is_done());
228        assert!(make(false, true).is_done());
229        assert!(make(true, true).is_done());
230        assert!(!make(false, false).is_done());
231    }
232
233    #[test]
234    fn test_training_stats_default() {
235        let stats = TrainingStats::default();
236        assert_eq!(stats.step, 0);
237        assert_eq!(stats.total_loss, 0.0);
238        assert!(stats.avg_reward.is_none());
239    }
240
241    #[test]
242    fn test_policy_update_creation() {
243        let update = PolicyUpdate {
244            agent_id: 0,
245            version: 1,
246            model_path: "/tmp/model_0_v1.bin".to_string(),
247            stats: TrainingStats::default(),
248        };
249
250        assert_eq!(update.agent_id, 0);
251        assert_eq!(update.version, 1);
252    }
253}