1pub mod nn;
14pub mod snake;
15pub mod weights;
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ExportedModel {
22 pub input_dim: usize,
24 pub output_dim: usize,
26 pub hidden_dim: usize,
28 pub feature_extractor: LayerWeights,
30 pub policy_head: LayerWeights,
32 pub value_head: Option<LayerWeights>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LayerWeights {
39 pub weights: Vec<f32>,
41 pub biases: Vec<f32>,
43 pub in_features: usize,
45 pub out_features: usize,
47}
48
49impl ExportedModel {
50 pub fn new(
52 input_dim: usize,
53 output_dim: usize,
54 hidden_dim: usize,
55 feature_extractor: LayerWeights,
56 policy_head: LayerWeights,
57 value_head: Option<LayerWeights>,
58 ) -> Self {
59 Self { input_dim, output_dim, hidden_dim, feature_extractor, policy_head, value_head }
60 }
61
62 pub fn predict(&self, observation: &[f32]) -> Vec<f32> {
64 assert_eq!(observation.len(), self.input_dim, "Input dimension mismatch");
65
66 let features = self.feature_extractor.forward(observation);
68 let features_relu: Vec<f32> = features.iter().map(|&x| x.max(0.0)).collect();
69
70 let logits = self.policy_head.forward(&features_relu);
72
73 softmax(&logits)
75 }
76
77 pub fn get_action(&self, observation: &[f32]) -> usize {
79 let probs = self.predict(observation);
80 probs
81 .iter()
82 .enumerate()
83 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
84 .map(|(idx, _)| idx)
85 .unwrap()
86 }
87
88 pub fn get_action_probs(&self, observation: &[f32]) -> Vec<f32> {
90 self.predict(observation)
91 }
92}
93
94impl LayerWeights {
95 pub fn new(
97 weights: Vec<f32>,
98 biases: Vec<f32>,
99 in_features: usize,
100 out_features: usize,
101 ) -> Self {
102 assert_eq!(weights.len(), in_features * out_features, "Weight matrix size mismatch");
103 assert_eq!(biases.len(), out_features, "Bias vector size mismatch");
104
105 Self { weights, biases, in_features, out_features }
106 }
107
108 #[allow(clippy::needless_range_loop)]
112 pub fn forward(&self, input: &[f32]) -> Vec<f32> {
113 assert_eq!(input.len(), self.in_features, "Input size mismatch");
114
115 let mut output = vec![0.0; self.out_features];
116
117 for i in 0..self.out_features {
118 let mut sum = self.biases[i];
119 for j in 0..self.in_features {
120 sum += self.weights[i * self.in_features + j] * input[j];
122 }
123 output[i] = sum;
124 }
125
126 output
127 }
128}
129
130fn softmax(logits: &[f32]) -> Vec<f32> {
132 let max_logit = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
134 let exps: Vec<f32> = logits.iter().map(|&x| (x - max_logit).exp()).collect();
135 let sum: f32 = exps.iter().sum();
136 exps.iter().map(|&x| x / sum).collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_layer_forward() {
145 let weights = vec![
147 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ];
151 let biases = vec![0.1, 0.2, 0.3];
152 let layer = LayerWeights::new(weights, biases, 2, 3);
153
154 let input = vec![1.0, 2.0];
155 let output = layer.forward(&input);
156
157 assert!((output[0] - 5.1).abs() < 1e-5);
160 assert!((output[1] - 11.2).abs() < 1e-5);
161 assert!((output[2] - 17.3).abs() < 1e-5);
162 }
163
164 #[test]
165 fn test_softmax() {
166 let logits = vec![1.0, 2.0, 3.0];
167 let probs = softmax(&logits);
168
169 let sum: f32 = probs.iter().sum();
171 assert!((sum - 1.0).abs() < 1e-5);
172
173 assert!(probs[0] < probs[1]);
175 assert!(probs[1] < probs[2]);
176 }
177
178 #[test]
179 fn test_exported_model() {
180 let feature_weights = LayerWeights::new(
182 vec![1.0, 0.0, 0.0, 1.0], vec![0.0, 0.0],
184 2,
185 2,
186 );
187 let policy_weights = LayerWeights::new(
188 vec![1.0, -1.0, -1.0, 1.0], vec![0.0, 0.0],
190 2,
191 2,
192 );
193
194 let model = ExportedModel::new(2, 2, 2, feature_weights, policy_weights, None);
195
196 let obs = vec![1.0, 0.5];
197 let action = model.get_action(&obs);
198
199 assert!(action < 2);
201 }
202}