1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! Tests for rollout buffer functionality
#[cfg(test)]
mod gae_tests {
use crate::buffer::rollout::{
compute_advantages_multi_agent, gae::compute_gae_single_env, storage::RolloutBuffer,
};
#[test]
fn test_gae_episode_boundaries() {
// Test that GAE correctly handles episode boundaries
// Episode 1: steps 0-2 (ends at step 2)
// Episode 2: steps 3-4
let rewards = vec![1.0, 1.0, 1.0, 2.0, 2.0]; // Different rewards for each episode
let values = vec![0.5, 0.5, 0.5, 1.0, 1.0];
let terminated = vec![false, false, true, false, false];
let mut advantages = vec![0.0; 5];
let mut returns = vec![0.0; 5];
compute_gae_single_env(
&rewards,
&values,
&terminated,
0.0, // last_value = 0 for simplicity
0.99, // gamma
0.95, // gae_lambda
&mut advantages,
&mut returns,
);
// Episode 1 (steps 0-2) should have advantages independent of episode 2
// Episode 2 (steps 3-4) should have different advantage values
// For episode 1, step 2 (terminal):
// delta = reward[2] + 0 - value[2] = 1.0 - 0.5 = 0.5
// gae = delta (no accumulation because GAE was reset)
// advantage[2] = 0.5
assert!(
(advantages[2] - 0.5).abs() < 0.01,
"Terminal step advantage incorrect: {}",
advantages[2]
);
// For episode 2, advantages should be computed independently
// They should NOT be affected by episode 1 values
println!("Advantages: {:?}", advantages);
println!("Returns: {:?}", returns);
// Episode 2 should have different advantages than episode 1
// because rewards are different (2.0 vs 1.0)
assert!(
(advantages[3] - advantages[0]).abs() > 0.1,
"Episode 2 advantages should differ from episode 1"
);
}
#[test]
fn test_gae_simple_episode() {
// Single episode with constant rewards
let rewards = vec![1.0, 1.0, 1.0];
let values = vec![2.0, 2.0, 2.0];
let terminated = vec![false, false, true];
let mut advantages = vec![0.0; 3];
let mut returns = vec![0.0; 3];
compute_gae_single_env(
&rewards,
&values,
&terminated,
0.0,
1.0, // gamma = 1.0 for simplicity
1.0, // gae_lambda = 1.0
&mut advantages,
&mut returns,
);
// With gamma=1, gae_lambda=1, last_value=0:
// Step 2 (terminal): delta = 1.0 + 0 - 2.0 = -1.0, gae = -1.0
// Step 1: delta = 1.0 + 2.0 - 2.0 = 1.0, gae = 1.0 + 1.0*1.0*(-1.0) = 0.0
// Step 0: delta = 1.0 + 2.0 - 2.0 = 1.0, gae = 1.0 + 1.0*1.0*0.0 = 1.0
println!("Simple episode advantages: {:?}", advantages);
assert!(
(advantages[2] - (-1.0)).abs() < 0.01,
"Step 2: expected -1.0, got {}",
advantages[2]
);
assert!(
(advantages[1] - 0.0).abs() < 0.01,
"Step 1: expected 0.0, got {}",
advantages[1]
);
assert!(
(advantages[0] - 1.0).abs() < 0.01,
"Step 0: expected 1.0, got {}",
advantages[0]
);
}
#[test]
fn test_rollout_buffer_advantages() {
// Test the full rollout buffer compute_advantages method
let num_steps = 5;
let num_envs = 2;
let obs_dim = 1;
let mut buffer = RolloutBuffer::new(num_steps, num_envs, obs_dim);
// Add some dummy data
for step in 0..num_steps {
for env in 0..num_envs {
let terminated = step == 2; // Episode ends at step 2
buffer.add(
step,
env,
&[0.0],
0,
1.0, // reward
0.5, // value
0.0, // log_prob
terminated,
false,
);
}
}
// Compute advantages
let last_values = vec![0.0; num_envs];
buffer.compute_advantages(&last_values, 0.99, 0.95);
// Check that advantages were computed
let advantages = buffer.advantages();
println!("Buffer advantages:");
for (step, adv) in advantages.iter().enumerate().take(num_steps) {
println!(" Step {}: {:?}", step, adv);
}
// Advantages should be different before and after episode boundary
// (step 2 is terminal, step 3 starts new episode)
assert!(advantages[3][0] != 0.0, "Advantages should be non-zero");
}
/// Hand-computed regression test for
/// [`compute_advantages_multi_agent`].
///
/// Setup: `num_steps = 2`, `num_envs = 2`, `num_agents = 2`, giving a
/// flat buffer of 8 entries indexed as
/// `t * num_envs * num_agents + env * num_agents + agent`. All
/// rewards are 1.0, all values are 0.0, `gamma = 1.0`, `gae_lambda = 1.0`,
/// and `last_values` is zero for every (env, agent).
///
/// We mark `dones[3] = true` at step 0, slot 3
/// (`env = 1, agent = 1`). Every other transition is non-terminal.
///
/// Expected per (env, agent) trajectory (gamma = lambda = 1):
///
/// * Non-terminating trajectory:
/// * Step 1: delta = 1 + 1 * 0 - 0 = 1, gae = 1, A_1 = 1.
/// * Step 0: delta = 1 + 1 * 0 - 0 = 1, gae = 1 + 1 * 1 = 2, A_0 = 2.
/// * Terminating trajectory (env=1, agent=1, done at step 0):
/// * Step 1: delta = 1, gae = 1, A_1 = 1.
/// * Step 0 (terminal): gae resets via the (1 - done) mask, then delta =
/// 1, gae = 1, A_0 = 1.
///
/// Returns equal advantages here since all values are 0.
#[test]
fn test_compute_advantages_multi_agent_two_env_two_agent_with_termination() {
let num_envs = 2usize;
let num_agents = 2usize;
let num_steps = 2usize;
let stride = num_envs * num_agents;
let total = num_steps * stride;
let rewards = vec![1.0_f32; total];
let values = vec![0.0_f32; total];
let mut dones = vec![false; total];
// Index 3 == step 0, env 1, agent 1: early termination.
dones[3] = true;
let last_values = vec![0.0_f32; stride];
let gamma = 1.0_f32;
let gae_lambda = 1.0_f32;
let (advantages, returns) = compute_advantages_multi_agent(
&rewards,
&values,
&dones,
&last_values,
num_envs,
num_agents,
gamma,
gae_lambda,
);
// Layout sanity.
assert_eq!(advantages.len(), total);
assert_eq!(returns.len(), total);
let eps = 1e-6_f32;
// Non-terminating slots: (env, agent) in {(0,0), (0,1), (1,0)}.
for (env, agent) in [(0, 0), (0, 1), (1, 0)] {
let slot = env * num_agents + agent;
let idx_step0 = slot; // step 0: 0 * stride + slot
let idx_step1 = stride + slot; // step 1: 1 * stride + slot
assert!(
(advantages[idx_step0] - 2.0).abs() < eps,
"(env={}, agent={}) step 0 advantage expected 2.0, got {}",
env,
agent,
advantages[idx_step0]
);
assert!(
(advantages[idx_step1] - 1.0).abs() < eps,
"(env={}, agent={}) step 1 advantage expected 1.0, got {}",
env,
agent,
advantages[idx_step1]
);
}
// Terminating slot: (env=1, agent=1). The done at step 0 must
// prevent the step-1 advantage from leaking back into step 0.
let term_slot = num_agents + 1; // env=1, agent=1: 1 * num_agents + 1
let term_step0 = term_slot; // step 0: 0 * stride + term_slot
let term_step1 = stride + term_slot; // step 1: 1 * stride + term_slot
assert!(
(advantages[term_step0] - 1.0).abs() < eps,
"terminating slot step 0 advantage expected 1.0 (no bootstrap, \
no GAE accumulation), got {}",
advantages[term_step0]
);
assert!(
(advantages[term_step1] - 1.0).abs() < eps,
"terminating slot step 1 advantage expected 1.0, got {}",
advantages[term_step1]
);
// Returns == advantages + values; values are all zero so returns
// should equal advantages.
for i in 0..total {
assert!(
(returns[i] - advantages[i]).abs() < eps,
"returns[{}] expected == advantages[{}] (values are zero), got R={}, A={}",
i,
i,
returns[i],
advantages[i]
);
}
}
/// `compute_advantages_multi_agent` with a single env and single
/// agent must reduce to the same math as `compute_gae_single_env`.
/// This pins down that the multi-agent generalization is a strict
/// superset of the single-agent case.
#[test]
fn test_compute_advantages_multi_agent_degenerates_to_single_agent() {
let rewards = vec![1.0_f32, 0.5, -0.25, 0.75];
let values = vec![0.4_f32, 0.6, 0.8, 0.2];
let terminated = vec![false, false, true, false];
let bootstrap = 0.7_f32;
let gamma = 0.99_f32;
let gae_lambda = 0.95_f32;
// Reference: single-agent in-place GAE.
let mut ref_advantages = vec![0.0_f32; rewards.len()];
let mut ref_returns = vec![0.0_f32; rewards.len()];
compute_gae_single_env(
&rewards,
&values,
&terminated,
bootstrap,
gamma,
gae_lambda,
&mut ref_advantages,
&mut ref_returns,
);
// Multi-agent path with num_envs=1, num_agents=1.
let (advantages, returns) = compute_advantages_multi_agent(
&rewards,
&values,
&terminated,
&[bootstrap],
1,
1,
gamma,
gae_lambda,
);
let eps = 1e-6_f32;
for i in 0..rewards.len() {
assert!(
(advantages[i] - ref_advantages[i]).abs() < eps,
"advantage[{}] mismatch: multi-agent={}, single-agent ref={}",
i,
advantages[i],
ref_advantages[i]
);
assert!(
(returns[i] - ref_returns[i]).abs() < eps,
"return[{}] mismatch: multi-agent={}, single-agent ref={}",
i,
returns[i],
ref_returns[i]
);
}
}
}