1use burn::tensor::{Int, Tensor as BurnTensor, TensorData, backend::Backend};
10use rand::Rng;
11
12use super::storage::ReplayBuffer;
13
14#[derive(Debug, Clone)]
19pub struct ReplayBatch {
20 pub observations: Vec<f32>,
22 pub actions: Vec<i64>,
24 pub rewards: Vec<f32>,
26 pub next_observations: Vec<f32>,
28 pub dones: Vec<bool>,
31 pub obs_dim: usize,
33}
34
35impl ReplayBatch {
36 pub fn len(&self) -> usize {
38 self.actions.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
43 self.actions.is_empty()
44 }
45
46 pub fn to_burn_tensors<B: Backend>(&self, device: &B::Device) -> ReplayBurnTensors<B> {
59 let batch = self.len();
60 let obs_dim = self.obs_dim;
61
62 let observations = BurnTensor::<B, 2>::from_data(
66 TensorData::new(self.observations.clone(), [batch, obs_dim]),
67 device,
68 );
69 let next_observations = BurnTensor::<B, 2>::from_data(
70 TensorData::new(self.next_observations.clone(), [batch, obs_dim]),
71 device,
72 );
73 let actions = BurnTensor::<B, 1, Int>::from_data(
74 TensorData::new(self.actions.clone(), [batch]),
75 device,
76 );
77 let rewards =
78 BurnTensor::<B, 1>::from_data(TensorData::new(self.rewards.clone(), [batch]), device);
79 let dones_f: Vec<f32> = self.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
80 let dones = BurnTensor::<B, 1>::from_data(TensorData::new(dones_f, [batch]), device);
81
82 ReplayBurnTensors { observations, actions, rewards, next_observations, dones }
83 }
84}
85
86#[derive(Debug)]
93pub struct ReplayBurnTensors<B: Backend> {
94 pub observations: BurnTensor<B, 2>,
96 pub actions: BurnTensor<B, 1, Int>,
98 pub rewards: BurnTensor<B, 1>,
100 pub next_observations: BurnTensor<B, 2>,
102 pub dones: BurnTensor<B, 1>,
105}
106
107pub fn sample<R: Rng>(buffer: &ReplayBuffer, batch_size: usize, rng: &mut R) -> ReplayBatch {
117 assert!(!buffer.is_empty(), "ReplayBuffer is empty; cannot sample");
118 assert!(batch_size > 0, "batch_size must be > 0");
119
120 let obs_dim = buffer.obs_dim();
121 let len = buffer.len();
122
123 let mut observations = vec![0.0f32; batch_size * obs_dim];
124 let mut next_observations = vec![0.0f32; batch_size * obs_dim];
125 let mut actions = Vec::with_capacity(batch_size);
126 let mut rewards = Vec::with_capacity(batch_size);
127 let mut dones = Vec::with_capacity(batch_size);
128
129 for k in 0..batch_size {
130 let idx = rng.random_range(0..len);
131 let obs_slice = &mut observations[k * obs_dim..(k + 1) * obs_dim];
132 let next_slice = &mut next_observations[k * obs_dim..(k + 1) * obs_dim];
133 let (a, r, d) = buffer.read_into(idx, obs_slice, next_slice);
134 actions.push(a);
135 rewards.push(r);
136 dones.push(d);
137 }
138
139 ReplayBatch { observations, actions, rewards, next_observations, dones, obs_dim }
140}
141
142#[cfg(test)]
143mod tests {
144 use rand::{SeedableRng, rngs::StdRng};
145
146 use super::*;
147
148 #[test]
149 fn test_sample_returns_correct_count() {
150 let mut buf = ReplayBuffer::new(16, 3);
151 for i in 0..10 {
152 buf.push(
153 &[i as f32, i as f32 + 0.1, i as f32 + 0.2],
154 (i % 2) as i64,
155 i as f32,
156 &[(i + 1) as f32, (i + 1) as f32 + 0.1, (i + 1) as f32 + 0.2],
157 false,
158 );
159 }
160 let mut rng = StdRng::seed_from_u64(42);
161 let batch = sample(&buf, 5, &mut rng);
162 assert_eq!(batch.len(), 5);
163 assert_eq!(batch.actions.len(), 5);
164 assert_eq!(batch.rewards.len(), 5);
165 assert_eq!(batch.dones.len(), 5);
166 assert_eq!(batch.observations.len(), 5 * 3);
167 assert_eq!(batch.next_observations.len(), 5 * 3);
168 assert_eq!(batch.obs_dim, 3);
169 }
170
171 #[test]
172 fn test_sampled_values_match_pushed_values() {
173 let mut buf = ReplayBuffer::new(8, 2);
175 buf.push(&[7.0, 8.0], 1, 42.0, &[9.0, 10.0], true);
176
177 let mut rng = StdRng::seed_from_u64(0);
178 let batch = sample(&buf, 4, &mut rng);
179 for k in 0..4 {
180 assert_eq!(batch.actions[k], 1);
181 assert_eq!(batch.rewards[k], 42.0);
182 assert!(batch.dones[k]);
183 assert_eq!(&batch.observations[k * 2..(k + 1) * 2], &[7.0, 8.0]);
184 assert_eq!(&batch.next_observations[k * 2..(k + 1) * 2], &[9.0, 10.0]);
185 }
186 }
187
188 mod burn_tests {
189 use burn::backend::NdArray;
190
191 use super::*;
192
193 type B = NdArray<f32>;
194
195 #[test]
196 fn test_to_burn_tensors_shapes_and_roundtrip() {
197 let mut buf = ReplayBuffer::new(8, 4);
198 for i in 0..6 {
199 buf.push(&[i as f32; 4], (i % 2) as i64, i as f32, &[i as f32 + 1.0; 4], i == 5);
200 }
201 let mut rng = StdRng::seed_from_u64(1);
202 let batch = sample(&buf, 3, &mut rng);
203 let device = crate::utils::cuda::default_burn_device::<B>();
204 let t = batch.to_burn_tensors::<B>(&device);
205
206 assert_eq!(t.observations.dims(), [3, 4]);
208 assert_eq!(t.next_observations.dims(), [3, 4]);
209 assert_eq!(t.actions.dims(), [3]);
210 assert_eq!(t.rewards.dims(), [3]);
211 assert_eq!(t.dones.dims(), [3]);
212
213 let obs_flat: Vec<f32> = t.observations.into_data().to_vec().unwrap();
218 assert_eq!(obs_flat, batch.observations);
219 let next_flat: Vec<f32> = t.next_observations.into_data().to_vec().unwrap();
220 assert_eq!(next_flat, batch.next_observations);
221 let acts: Vec<i64> = t.actions.into_data().to_vec().unwrap();
222 assert_eq!(acts, batch.actions);
223 let rews: Vec<f32> = t.rewards.into_data().to_vec().unwrap();
224 assert_eq!(rews, batch.rewards);
225 let dones_f: Vec<f32> = t.dones.into_data().to_vec().unwrap();
226 let expected_dones: Vec<f32> =
227 batch.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
228 assert_eq!(dones_f, expected_dones);
229 }
230
231 #[test]
232 fn test_to_burn_tensors_empty_batch_does_not_panic() {
233 let batch = ReplayBatch {
237 observations: vec![],
238 actions: vec![],
239 rewards: vec![],
240 next_observations: vec![],
241 dones: vec![],
242 obs_dim: 4,
243 };
244 let device = crate::utils::cuda::default_burn_device::<B>();
245 let t = batch.to_burn_tensors::<B>(&device);
246 assert_eq!(t.observations.dims(), [0, 4]);
247 assert_eq!(t.next_observations.dims(), [0, 4]);
248 assert_eq!(t.actions.dims(), [0]);
249 assert_eq!(t.rewards.dims(), [0]);
250 assert_eq!(t.dones.dims(), [0]);
251 }
252 }
253
254 #[test]
255 #[should_panic(expected = "ReplayBuffer is empty")]
256 fn test_sample_empty_panics() {
257 let buf = ReplayBuffer::new(4, 2);
258 let mut rng = StdRng::seed_from_u64(0);
259 let _ = sample(&buf, 2, &mut rng);
260 }
261
262 #[test]
263 #[should_panic(expected = "batch_size must be > 0")]
264 fn test_zero_batch_size_panics() {
265 let mut buf = ReplayBuffer::new(4, 2);
266 buf.push(&[0.0, 0.0], 0, 0.0, &[0.0, 0.0], false);
267 let mut rng = StdRng::seed_from_u64(0);
268 let _ = sample(&buf, 0, &mut rng);
269 }
270}