1use std::collections::HashMap;
8
9use anyhow::{Result, anyhow};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
18pub enum Activation {
19 ReLU,
21 Tanh,
23 Sigmoid,
25 Identity,
27 #[serde(rename = "GELU")]
30 Gelu,
31 #[serde(rename = "Swish")]
33 Swish,
34}
35
36impl Activation {
37 #[inline]
39 pub fn apply(&self, x: f32) -> f32 {
40 match self {
41 Activation::ReLU => x.max(0.0),
42 Activation::Tanh => x.tanh(),
43 Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
44 Activation::Identity => x,
45 Activation::Gelu => {
46 const SQRT_2_OVER_PI: f32 = 0.797_884_6;
48 0.5 * x * (1.0 + (SQRT_2_OVER_PI * (x + 0.044715 * x.powi(3))).tanh())
49 }
50 Activation::Swish => x / (1.0 + (-x).exp()), }
52 }
53
54 #[inline]
56 pub fn apply_vec(&self, vec: &mut [f32]) {
57 for val in vec.iter_mut() {
58 *val = self.apply(*val);
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(tag = "type")]
66pub enum Layer {
67 #[serde(rename = "linear")]
69 Linear {
70 name: String,
73 in_features: usize,
76 out_features: usize,
78 weight: Vec<Vec<f32>>,
80 bias: Vec<f32>,
82 },
83
84 #[serde(rename = "conv2d")]
86 Conv2d {
87 name: String,
89 in_channels: usize,
91 out_channels: usize,
93 kernel_size: usize,
95 padding: usize,
98 stride: usize,
100 weight: Vec<Vec<Vec<Vec<f32>>>>,
103 bias: Vec<f32>,
105 },
106
107 #[serde(rename = "activation")]
109 Activation {
110 name: String,
112 activation: Activation,
114 },
115
116 #[serde(rename = "flatten")]
118 Flatten {
119 name: String,
122 },
123
124 #[serde(rename = "reshape")]
126 Reshape {
127 name: String,
129 shape: Vec<usize>,
133 },
134
135 #[serde(rename = "residual")]
137 Residual {
138 name: String,
140 layers: Vec<Layer>,
144 },
145}
146
147impl Layer {
148 pub fn name(&self) -> &str {
150 match self {
151 Layer::Linear { name, .. } => name,
152 Layer::Conv2d { name, .. } => name,
153 Layer::Activation { name, .. } => name,
154 Layer::Flatten { name } => name,
155 Layer::Reshape { name, .. } => name,
156 Layer::Residual { name, .. } => name,
157 }
158 }
159
160 #[allow(clippy::needless_range_loop)]
164 pub fn forward(&self, input: &Tensor) -> Result<Tensor> {
165 match self {
166 Layer::Linear { weight, bias, out_features, .. } => {
167 let in_vec = input.as_1d()?;
169 let mut output = vec![0.0; *out_features];
170
171 for (i, row) in weight.iter().enumerate() {
172 for (j, &val) in in_vec.iter().enumerate() {
173 output[i] += row[j] * val;
174 }
175 output[i] += bias[i];
176 }
177
178 Ok(Tensor::D1(output))
179 }
180
181 Layer::Conv2d { weight, bias, padding, stride, .. } => {
182 let input_3d = input.as_3d()?;
184 let output = Self::conv2d_forward(input_3d, weight, bias, *padding, *stride)?;
185 Ok(Tensor::D3(output))
186 }
187
188 Layer::Activation { activation, .. } => {
189 let mut output = input.clone();
190 match &mut output {
191 Tensor::D1(v) => activation.apply_vec(v),
192 Tensor::D3(v) => {
193 for channel in v.iter_mut() {
194 for row in channel.iter_mut() {
195 activation.apply_vec(row);
196 }
197 }
198 }
199 }
200 Ok(output)
201 }
202
203 Layer::Flatten { .. } => {
204 let flat = match input {
206 Tensor::D1(v) => v.clone(),
207 Tensor::D3(v) => {
208 let mut flat = Vec::new();
209 for channel in v {
210 for row in channel {
211 flat.extend_from_slice(row);
212 }
213 }
214 flat
215 }
216 };
217 Ok(Tensor::D1(flat))
218 }
219
220 Layer::Reshape { shape, .. } => {
221 let flat = match input {
223 Tensor::D1(v) => v.clone(),
224 Tensor::D3(v) => {
225 let mut flat = Vec::new();
226 for channel in v {
227 for row in channel {
228 flat.extend_from_slice(row);
229 }
230 }
231 flat
232 }
233 };
234
235 if shape.len() == 1 {
237 if flat.len() != shape[0] {
238 return Err(anyhow!(
239 "Reshape size mismatch: {} != {}",
240 flat.len(),
241 shape[0]
242 ));
243 }
244 Ok(Tensor::D1(flat))
245 } else if shape.len() == 3 {
246 let (c, h, w) = (shape[0], shape[1], shape[2]);
247 if flat.len() != c * h * w {
248 return Err(anyhow!(
249 "Reshape size mismatch: {} != {}",
250 flat.len(),
251 c * h * w
252 ));
253 }
254
255 let mut reshaped = vec![vec![vec![0.0; w]; h]; c];
256 for ch in 0..c {
257 for row in 0..h {
258 for col in 0..w {
259 let idx = ch * h * w + row * w + col;
260 reshaped[ch][row][col] = flat[idx];
261 }
262 }
263 }
264 Ok(Tensor::D3(reshaped))
265 } else {
266 Err(anyhow!("Unsupported reshape dimensions: {:?}", shape))
267 }
268 }
269
270 Layer::Residual { layers, .. } => {
271 let mut x = input.clone();
273 for layer in layers {
274 x = layer.forward(&x)?;
275 }
276
277 let mut result = input.clone();
279 match (&mut result, &x) {
280 (Tensor::D1(r), Tensor::D1(x)) => {
281 for (r_val, &x_val) in r.iter_mut().zip(x.iter()) {
282 *r_val += x_val;
283 }
284 }
285 (Tensor::D3(r), Tensor::D3(x)) => {
286 for (r_ch, x_ch) in r.iter_mut().zip(x.iter()) {
287 for (r_row, x_row) in r_ch.iter_mut().zip(x_ch.iter()) {
288 for (r_val, &x_val) in r_row.iter_mut().zip(x_row.iter()) {
289 *r_val += x_val;
290 }
291 }
292 }
293 }
294 _ => return Err(anyhow!("Residual connection dimension mismatch")),
295 }
296 Ok(result)
297 }
298 }
299 }
300
301 #[allow(clippy::needless_range_loop)]
305 fn conv2d_forward(
306 input: &[Vec<Vec<f32>>],
307 weight: &[Vec<Vec<Vec<f32>>>],
308 bias: &[f32],
309 padding: usize,
310 stride: usize,
311 ) -> Result<Vec<Vec<Vec<f32>>>> {
312 let out_channels = weight.len();
313 let in_channels = input.len();
314 let in_height = input[0].len();
315 let in_width = input[0][0].len();
316 let kernel_size = weight[0][0].len();
317
318 let out_height = (in_height + 2 * padding - kernel_size) / stride + 1;
319 let out_width = (in_width + 2 * padding - kernel_size) / stride + 1;
320
321 let mut output = vec![vec![vec![0.0; out_width]; out_height]; out_channels];
322
323 for out_c in 0..out_channels {
324 for h in 0..out_height {
325 for w in 0..out_width {
326 let mut sum = bias[out_c];
327
328 for in_c in 0..in_channels {
329 for kh in 0..kernel_size {
330 for kw in 0..kernel_size {
331 let ih = h * stride + kh;
332 let iw = w * stride + kw;
333
334 let ih = ih as i32 - padding as i32;
336 let iw = iw as i32 - padding as i32;
337
338 if ih >= 0
339 && ih < in_height as i32
340 && iw >= 0
341 && iw < in_width as i32
342 {
343 sum += input[in_c][ih as usize][iw as usize]
344 * weight[out_c][in_c][kh][kw];
345 }
346 }
347 }
348 }
349
350 output[out_c][h][w] = sum;
351 }
352 }
353 }
354
355 Ok(output)
356 }
357}
358
359#[derive(Debug, Clone)]
361pub enum Tensor {
362 D1(Vec<f32>),
364 D3(Vec<Vec<Vec<f32>>>),
366}
367
368impl Tensor {
369 pub fn as_1d(&self) -> Result<&Vec<f32>> {
371 match self {
372 Tensor::D1(v) => Ok(v),
373 _ => Err(anyhow!("Expected 1D tensor")),
374 }
375 }
376
377 pub fn as_3d(&self) -> Result<&Vec<Vec<Vec<f32>>>> {
379 match self {
380 Tensor::D3(v) => Ok(v),
381 _ => Err(anyhow!("Expected 3D tensor")),
382 }
383 }
384
385 pub fn size(&self) -> usize {
387 match self {
388 Tensor::D1(v) => v.len(),
389 Tensor::D3(v) => v.len() * v[0].len() * v[0][0].len(),
390 }
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(tag = "type")]
397pub enum InputSpec {
398 #[serde(rename = "vector")]
400 Vector {
401 size: usize,
404 },
405
406 #[serde(rename = "grid")]
408 Grid {
409 channels: usize,
412 height: usize,
414 width: usize,
418 },
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct OutputSpec {
424 pub num_actions: usize,
427 pub has_value: bool,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct ModelMetadata {
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub total_steps: Option<usize>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub total_episodes: Option<usize>,
445 #[serde(skip_serializing_if = "Option::is_none")]
448 pub final_performance: Option<f64>,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub training_time_secs: Option<f64>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub device: Option<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub environment: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub algorithm: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub timestamp: Option<String>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub notes: Option<String>,
467 #[serde(skip_serializing_if = "Option::is_none")]
471 pub hyperparameters: Option<HashMap<String, serde_json::Value>>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct UniversalModel {
477 pub version: String,
479
480 pub input: InputSpec,
482
483 pub output: OutputSpec,
485
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub metadata: Option<ModelMetadata>,
489
490 pub shared_layers: Vec<Layer>,
492
493 pub policy_head: Vec<Layer>,
495
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub value_head: Option<Vec<Layer>>,
499}
500
501impl UniversalModel {
502 pub fn save_json<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
504 let json = serde_json::to_string_pretty(self)?;
505 std::fs::write(path, json)?;
506 Ok(())
507 }
508
509 pub fn load_json<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
511 let json = std::fs::read_to_string(path)?;
512 let model = serde_json::from_str(&json)?;
513 Ok(model)
514 }
515
516 pub fn from_json_str(json: &str) -> Result<Self> {
518 let model = serde_json::from_str(json)?;
519 Ok(model)
520 }
521
522 #[allow(clippy::needless_range_loop)]
526 fn input_to_tensor(&self, input: &[f32]) -> Result<Tensor> {
527 match &self.input {
528 InputSpec::Vector { size } => {
529 if input.len() != *size {
530 return Err(anyhow!(
531 "Input size mismatch: expected {}, got {}",
532 size,
533 input.len()
534 ));
535 }
536 Ok(Tensor::D1(input.to_vec()))
537 }
538 InputSpec::Grid { channels, height, width } => {
539 let grid_size = channels * height * width;
540 if input.len() != grid_size {
541 return Err(anyhow!(
542 "Input size mismatch: expected {}, got {}",
543 grid_size,
544 input.len()
545 ));
546 }
547
548 let mut grid = vec![vec![vec![0.0; *width]; *height]; *channels];
550 for c in 0..*channels {
551 for h in 0..*height {
552 for w in 0..*width {
553 let idx = c * height * width + h * width + w;
554 grid[c][h][w] = input[idx];
555 }
556 }
557 }
558 Ok(Tensor::D3(grid))
559 }
560 }
561 }
562
563 pub fn forward(&self, input: &[f32]) -> Result<(Vec<f32>, Option<f32>)> {
568 let mut x = self.input_to_tensor(input)?;
570
571 for layer in &self.shared_layers {
573 x = layer.forward(&x)?;
574 }
575
576 let mut policy_features = x.clone();
578 for layer in &self.policy_head {
579 policy_features = layer.forward(&policy_features)?;
580 }
581 let logits = policy_features.as_1d()?.clone();
582
583 let value = if let Some(value_head) = &self.value_head {
585 let mut value_features = x;
586 for layer in value_head {
587 value_features = layer.forward(&value_features)?;
588 }
589 let value_vec = value_features.as_1d()?;
590 if value_vec.len() != 1 {
591 return Err(anyhow!("Value head output should be size 1, got {}", value_vec.len()));
592 }
593 Some(value_vec[0])
594 } else {
595 None
596 };
597
598 Ok((logits, value))
599 }
600
601 pub fn get_action(&self, input: &[f32]) -> Result<usize> {
603 let (logits, _value) = self.forward(input)?;
604
605 let action = logits
606 .iter()
607 .enumerate()
608 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
609 .map(|(idx, _)| idx)
610 .ok_or_else(|| anyhow!("No actions available"))?;
611
612 Ok(action)
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_activation_functions() {
622 assert_eq!(Activation::ReLU.apply(-1.0), 0.0);
623 assert_eq!(Activation::ReLU.apply(1.0), 1.0);
624
625 let tanh_result = Activation::Tanh.apply(0.0);
626 assert!((tanh_result - 0.0).abs() < 1e-6);
627
628 let sigmoid_result = Activation::Sigmoid.apply(0.0);
629 assert!((sigmoid_result - 0.5).abs() < 1e-6);
630 }
631
632 #[test]
633 fn test_simple_mlp() -> Result<()> {
634 let model = UniversalModel {
636 version: "1.0".to_string(),
637 input: InputSpec::Vector { size: 4 },
638 output: OutputSpec { num_actions: 2, has_value: true },
639 metadata: None,
640 shared_layers: vec![
641 Layer::Linear {
642 name: "fc1".to_string(),
643 in_features: 4,
644 out_features: 8,
645 weight: vec![vec![0.1; 4]; 8],
646 bias: vec![0.0; 8],
647 },
648 Layer::Activation { name: "relu1".to_string(), activation: Activation::ReLU },
649 Layer::Linear {
650 name: "fc2".to_string(),
651 in_features: 8,
652 out_features: 8,
653 weight: vec![vec![0.1; 8]; 8],
654 bias: vec![0.0; 8],
655 },
656 Layer::Activation { name: "relu2".to_string(), activation: Activation::ReLU },
657 ],
658 policy_head: vec![Layer::Linear {
659 name: "policy".to_string(),
660 in_features: 8,
661 out_features: 2,
662 weight: vec![vec![0.1; 8]; 2],
663 bias: vec![0.0; 2],
664 }],
665 value_head: Some(vec![Layer::Linear {
666 name: "value".to_string(),
667 in_features: 8,
668 out_features: 1,
669 weight: vec![vec![0.1; 8]],
670 bias: vec![0.0],
671 }]),
672 };
673
674 let input = vec![1.0, 2.0, 3.0, 4.0];
675 let (logits, value) = model.forward(&input)?;
676
677 assert_eq!(logits.len(), 2);
678 assert!(value.is_some());
679
680 Ok(())
681 }
682
683 #[test]
684 fn test_get_action() -> Result<()> {
685 let model = UniversalModel {
686 version: "1.0".to_string(),
687 input: InputSpec::Vector { size: 2 },
688 output: OutputSpec { num_actions: 2, has_value: false },
689 metadata: None,
690 shared_layers: vec![],
691 policy_head: vec![Layer::Linear {
692 name: "policy".to_string(),
693 in_features: 2,
694 out_features: 2,
695 weight: vec![vec![1.0, 0.0], vec![0.0, 1.0]],
696 bias: vec![0.0, 0.0],
697 }],
698 value_head: None,
699 };
700
701 let input = vec![2.0, 1.0];
702 let action = model.get_action(&input)?;
703
704 assert_eq!(action, 0); Ok(())
707 }
708}