1use anyhow::Result;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SnakeCNNInference {
18 pub grid_width: usize,
20 pub grid_height: usize,
22 pub input_channels: usize,
24 pub num_actions: usize,
26
27 pub conv1_weight: Vec<Vec<Vec<Vec<f32>>>>,
30 pub conv1_bias: Vec<f32>,
32
33 pub conv2_weight: Vec<Vec<Vec<Vec<f32>>>>,
36 pub conv2_bias: Vec<f32>,
38
39 pub conv3_weight: Vec<Vec<Vec<Vec<f32>>>>,
42 pub conv3_bias: Vec<f32>,
44
45 pub fc_common_weight: Vec<Vec<f32>>,
49 pub fc_common_bias: Vec<f32>,
51
52 pub fc_policy_weight: Vec<Vec<f32>>,
56 pub fc_policy_bias: Vec<f32>,
58
59 pub fc_value_weight: Vec<Vec<f32>>,
62 pub fc_value_bias: Vec<f32>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub metadata: Option<TrainingMetadata>,
68}
69
70impl SnakeCNNInference {
71 pub fn save_json<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
73 let json = serde_json::to_string_pretty(self)?;
74 std::fs::write(path, json)?;
75 Ok(())
76 }
77
78 pub fn load_json<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
80 let json = std::fs::read_to_string(path)?;
81 let model = serde_json::from_str(&json)?;
82 Ok(model)
83 }
84
85 #[allow(clippy::needless_range_loop)]
90 fn conv2d(
91 &self,
92 input: &[Vec<Vec<f32>>], weight: &[Vec<Vec<Vec<f32>>>], bias: &[f32],
95 ) -> Vec<Vec<Vec<f32>>> {
96 let out_channels = weight.len();
97 let in_channels = input.len();
98 let height = input[0].len();
99 let width = input[0][0].len();
100
101 let mut output = vec![vec![vec![0.0; width]; height]; out_channels];
102
103 for out_c in 0..out_channels {
104 for h in 0..height {
105 for w in 0..width {
106 let mut sum = bias[out_c];
107
108 for in_c in 0..in_channels {
110 for kh in 0..3 {
111 for kw in 0..3 {
112 let ih = h as i32 + kh as i32 - 1;
113 let iw = w as i32 + kw as i32 - 1;
114
115 if ih >= 0 && ih < height as i32 && iw >= 0 && iw < width as i32 {
116 sum += input[in_c][ih as usize][iw as usize]
117 * weight[out_c][in_c][kh][kw];
118 }
119 }
120 }
121 }
122
123 output[out_c][h][w] = sum;
124 }
125 }
126 }
127
128 output
129 }
130
131 fn relu(&self, input: &mut [Vec<Vec<f32>>]) {
133 for channel in input.iter_mut() {
134 for row in channel.iter_mut() {
135 for val in row.iter_mut() {
136 if *val < 0.0 {
137 *val = 0.0;
138 }
139 }
140 }
141 }
142 }
143
144 #[allow(clippy::needless_range_loop)]
156 pub fn forward(&self, grid: &[f32]) -> (Vec<f32>, f32) {
157 let grid_size = self.grid_width * self.grid_height;
158 assert_eq!(grid.len(), self.input_channels * grid_size);
159
160 let mut input =
162 vec![vec![vec![0.0; self.grid_width]; self.grid_height]; self.input_channels];
163 for c in 0..self.input_channels {
164 for h in 0..self.grid_height {
165 for w in 0..self.grid_width {
166 let idx = c * grid_size + h * self.grid_width + w;
167 input[c][h][w] = grid[idx];
168 }
169 }
170 }
171
172 let mut x = self.conv2d(&input, &self.conv1_weight, &self.conv1_bias);
174 self.relu(&mut x);
175
176 x = self.conv2d(&x, &self.conv2_weight, &self.conv2_bias);
178 self.relu(&mut x);
179
180 x = self.conv2d(&x, &self.conv3_weight, &self.conv3_bias);
182 self.relu(&mut x);
183
184 let flat_size = 64 * grid_size;
186 let mut flattened = Vec::with_capacity(flat_size);
187 for channel in &x {
188 for row in channel {
189 for &val in row {
190 flattened.push(val);
191 }
192 }
193 }
194
195 let mut features = vec![0.0; 256];
197 for (i, row) in self.fc_common_weight.iter().enumerate() {
198 for (j, &val) in flattened.iter().enumerate() {
199 features[i] += row[j] * val;
200 }
201 features[i] += self.fc_common_bias[i];
202 if features[i] < 0.0 {
203 features[i] = 0.0;
204 }
205 }
206
207 let mut logits = vec![0.0; self.num_actions];
209 for (i, row) in self.fc_policy_weight.iter().enumerate() {
210 for (j, &val) in features.iter().enumerate() {
211 logits[i] += row[j] * val;
212 }
213 logits[i] += self.fc_policy_bias[i];
214 }
215
216 let mut value = 0.0;
218 for (j, &val) in features.iter().enumerate() {
219 value += self.fc_value_weight[0][j] * val;
220 }
221 value += self.fc_value_bias[0];
222
223 (logits, value)
224 }
225
226 pub fn get_action(&self, grid: &[f32]) -> usize {
228 let (logits, _value) = self.forward(grid);
229
230 logits
231 .iter()
232 .enumerate()
233 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
234 .map(|(idx, _)| idx)
235 .unwrap()
236 }
237}
238
239#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
245pub enum InferenceActivation {
246 ReLU,
249 Tanh,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct TrainingMetadata {
258 pub total_steps: usize,
260 pub total_episodes: usize,
262 pub final_performance: f64,
264 pub training_time_secs: f64,
266 pub device: String,
268 pub environment: String,
270 pub algorithm: String,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub timestamp: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub hyperparameters: Option<std::collections::HashMap<String, serde_json::Value>>,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub notes: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct InferenceModel {
289 pub obs_dim: usize,
291 pub action_dim: usize,
293 pub hidden_dim: usize,
295 #[serde(default = "default_activation")]
297 pub activation: InferenceActivation,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub metadata: Option<TrainingMetadata>,
302
303 pub shared_fc1_weight: Vec<Vec<f32>>,
305 pub shared_fc1_bias: Vec<f32>,
307
308 pub shared_fc2_weight: Vec<Vec<f32>>,
310 pub shared_fc2_bias: Vec<f32>,
312
313 pub policy_weight: Vec<Vec<f32>>,
315 pub policy_bias: Vec<f32>,
317
318 pub value_weight: Vec<Vec<f32>>,
320 pub value_bias: Vec<f32>,
322}
323
324fn default_activation() -> InferenceActivation {
325 InferenceActivation::Tanh
326}
327
328impl InferenceModel {
329 pub fn save_json<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
331 let json = serde_json::to_string_pretty(self)?;
332 std::fs::write(path, json)?;
333 Ok(())
334 }
335
336 pub fn load_json<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
338 let json = std::fs::read_to_string(path)?;
339 let model = serde_json::from_str(&json)?;
340 Ok(model)
341 }
342
343 #[inline]
345 fn activate(&self, x: f32) -> f32 {
346 match self.activation {
347 InferenceActivation::ReLU => {
348 if x < 0.0 {
349 0.0
350 } else {
351 x
352 }
353 }
354 InferenceActivation::Tanh => x.tanh(),
355 }
356 }
357
358 pub fn forward(&self, obs: &[f32]) -> (Vec<f32>, f32) {
367 assert_eq!(obs.len(), self.obs_dim, "Observation dimension mismatch");
368
369 let mut hidden1 = vec![0.0; self.hidden_dim];
371 for (i, row) in self.shared_fc1_weight.iter().enumerate() {
372 for (j, &val) in obs.iter().enumerate() {
373 hidden1[i] += row[j] * val;
374 }
375 hidden1[i] = self.activate(hidden1[i] + self.shared_fc1_bias[i]);
376 }
377
378 let mut hidden2 = vec![0.0; self.hidden_dim];
380 for (i, row) in self.shared_fc2_weight.iter().enumerate() {
381 for (j, &val) in hidden1.iter().enumerate() {
382 hidden2[i] += row[j] * val;
383 }
384 hidden2[i] = self.activate(hidden2[i] + self.shared_fc2_bias[i]);
385 }
386
387 let mut logits = vec![0.0; self.action_dim];
389 for (i, row) in self.policy_weight.iter().enumerate() {
390 for (j, &val) in hidden2.iter().enumerate() {
391 logits[i] += row[j] * val;
392 }
393 logits[i] += self.policy_bias[i];
394 }
395
396 let mut value = 0.0;
398 for row in self.value_weight.iter() {
399 for (j, &val) in hidden2.iter().enumerate() {
400 value += row[j] * val;
401 }
402 }
403 value += self.value_bias[0];
404
405 (logits, value)
406 }
407
408 pub fn get_action(&self, obs: &[f32]) -> usize {
416 let (logits, _value) = self.forward(obs);
417
418 let max_logit = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
420 let exp_logits: Vec<f32> = logits.iter().map(|&x| (x - max_logit).exp()).collect();
421 let sum_exp: f32 = exp_logits.iter().sum();
422 let probs: Vec<f32> = exp_logits.iter().map(|&x| x / sum_exp).collect();
423
424 probs
426 .iter()
427 .enumerate()
428 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
429 .map(|(idx, _)| idx)
430 .unwrap()
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_forward_pass() {
440 let model = InferenceModel {
442 obs_dim: 2,
443 action_dim: 2,
444 hidden_dim: 4,
445 activation: InferenceActivation::Tanh,
446 metadata: None,
447 shared_fc1_weight: vec![vec![1.0, 0.0]; 4],
448 shared_fc1_bias: vec![0.0; 4],
449 shared_fc2_weight: vec![vec![1.0, 0.0, 0.0, 0.0]; 4],
450 shared_fc2_bias: vec![0.0; 4],
451 policy_weight: vec![vec![1.0, 0.0, 0.0, 0.0]; 2],
452 policy_bias: vec![0.0; 2],
453 value_weight: vec![vec![1.0, 0.0, 0.0, 0.0]],
454 value_bias: vec![0.0],
455 };
456
457 let obs = vec![1.0, 2.0];
458 let (logits, value) = model.forward(&obs);
459
460 assert_eq!(logits.len(), 2);
461 assert!(value.is_finite());
462 }
463
464 #[test]
465 fn test_get_action() {
466 let model = InferenceModel {
467 obs_dim: 2,
468 action_dim: 2,
469 hidden_dim: 4,
470 activation: InferenceActivation::ReLU,
471 metadata: None,
472 shared_fc1_weight: vec![vec![1.0, 0.0]; 4],
473 shared_fc1_bias: vec![0.0; 4],
474 shared_fc2_weight: vec![vec![1.0, 0.0, 0.0, 0.0]; 4],
475 shared_fc2_bias: vec![0.0; 4],
476 policy_weight: vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 0.0, 0.0, 0.0]],
477 policy_bias: vec![0.0, 0.0],
478 value_weight: vec![vec![1.0, 0.0, 0.0, 0.0]],
479 value_bias: vec![0.0],
480 };
481
482 let obs = vec![1.0, 2.0];
483 let action = model.get_action(&obs);
484
485 assert!(action < 2);
486 }
487
488 #[test]
489 fn test_save_load_json() {
490 let model = InferenceModel {
491 obs_dim: 4,
492 action_dim: 2,
493 hidden_dim: 64,
494 activation: InferenceActivation::Tanh,
495 metadata: None,
496 shared_fc1_weight: vec![vec![0.0; 4]; 64],
497 shared_fc1_bias: vec![0.0; 64],
498 shared_fc2_weight: vec![vec![0.0; 64]; 64],
499 shared_fc2_bias: vec![0.0; 64],
500 policy_weight: vec![vec![0.0; 64]; 2],
501 policy_bias: vec![0.0; 2],
502 value_weight: vec![vec![0.0; 64]],
503 value_bias: vec![0.0],
504 };
505
506 let temp_path = "/tmp/test_inference_model.json";
507 model.save_json(temp_path).unwrap();
508
509 let loaded = InferenceModel::load_json(temp_path).unwrap();
510 assert_eq!(loaded.obs_dim, model.obs_dim);
511 assert_eq!(loaded.action_dim, model.action_dim);
512 assert_eq!(loaded.hidden_dim, model.hidden_dim);
513
514 std::fs::remove_file(temp_path).ok();
515 }
516}