1use crate::autodiff::{Tape, Var};
17
18pub struct NeuralOde {
20 sizes: Vec<usize>, params: Vec<f64>,
22 m: Vec<f64>,
23 v: Vec<f64>,
24 t: u64,
25}
26
27fn splitmix64(state: &mut u64) -> u64 {
28 *state = state.wrapping_add(0x9E3779B97F4A7C15);
29 let mut z = *state;
30 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
31 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
32 z ^ (z >> 31)
33}
34
35impl NeuralOde {
36 pub fn new(hidden: usize, seed: u64) -> Self {
38 let sizes = vec![2, hidden, hidden, 2];
39 let mut state = seed ^ 0x7777_1111_3333_9999;
40 let mut params = Vec::new();
41 for l in 0..sizes.len() - 1 {
42 let (ind, outd) = (sizes[l], sizes[l + 1]);
43 let r = (6.0 / (ind + outd) as f64).sqrt();
44 for _ in 0..ind * outd {
45 let u = (splitmix64(&mut state) as f64 / u64::MAX as f64) * 2.0 - 1.0;
46 params.push(u * r);
47 }
48 params.extend(std::iter::repeat_n(0.0, outd));
49 }
50 let n = params.len();
51 NeuralOde { sizes, params, m: vec![0.0; n], v: vec![0.0; n], t: 0 }
52 }
53
54 fn field_var<'t>(&self, pv: &[Var<'t>], x: [Var<'t>; 2]) -> [Var<'t>; 2] {
56 let mut a = vec![x[0], x[1]];
57 let mut off = 0;
58 let layers = self.sizes.len() - 1;
59 for l in 0..layers {
60 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
61 let mut z = Vec::with_capacity(outd);
62 for o in 0..outd {
63 let mut s = pv[off + ind * outd + o];
64 for (i, &ai) in a.iter().enumerate() {
65 s = s + pv[off + o * ind + i] * ai;
66 }
67 z.push(if l + 1 < layers { s.tanh() } else { s });
68 }
69 off += ind * outd + outd;
70 a = z;
71 }
72 [a[0], a[1]]
73 }
74
75 fn rk4_var<'t>(&self, pv: &[Var<'t>], x: [Var<'t>; 2], dt: f64) -> [Var<'t>; 2] {
77 let add = |a: [Var<'t>; 2], b: [Var<'t>; 2], s: f64| [a[0] + b[0] * s, a[1] + b[1] * s];
78 let k1 = self.field_var(pv, x);
79 let k2 = self.field_var(pv, add(x, k1, 0.5 * dt));
80 let k3 = self.field_var(pv, add(x, k2, 0.5 * dt));
81 let k4 = self.field_var(pv, add(x, k3, dt));
82 [
83 x[0] + (k1[0] + k2[0] * 2.0 + k3[0] * 2.0 + k4[0]) * (dt / 6.0),
84 x[1] + (k1[1] + k2[1] * 2.0 + k3[1] * 2.0 + k4[1]) * (dt / 6.0),
85 ]
86 }
87
88 pub fn field(&self, x: &[f64]) -> [f64; 2] {
90 let mut a = vec![x[0], x[1]];
91 let mut off = 0;
92 let layers = self.sizes.len() - 1;
93 for l in 0..layers {
94 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
95 let mut z = vec![0.0; outd];
96 for (o, zo) in z.iter_mut().enumerate() {
97 let mut s = self.params[off + ind * outd + o];
98 for (i, &ai) in a.iter().enumerate() {
99 s += self.params[off + o * ind + i] * ai;
100 }
101 *zo = if l + 1 < layers { s.tanh() } else { s };
102 }
103 off += ind * outd + outd;
104 a = z;
105 }
106 [a[0], a[1]]
107 }
108
109 pub fn rollout(&self, x0: &[f64], n: usize, dt: f64) -> Vec<[f64; 2]> {
111 let mut x = [x0[0], x0[1]];
112 let mut out = vec![x];
113 for _ in 0..n {
114 let f = |x: [f64; 2]| self.field(&x);
115 let add = |a: [f64; 2], b: [f64; 2], s: f64| [a[0] + b[0] * s, a[1] + b[1] * s];
116 let k1 = f(x);
117 let k2 = f(add(x, k1, 0.5 * dt));
118 let k3 = f(add(x, k2, 0.5 * dt));
119 let k4 = f(add(x, k3, dt));
120 x = [
121 x[0] + dt / 6.0 * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
122 x[1] + dt / 6.0 * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1]),
123 ];
124 out.push(x);
125 }
126 out
127 }
128
129 pub fn train_step(&mut self, obs: &[[f64; 2]], dt: f64, lr: f64) -> f64 {
132 let tape = Tape::new();
133 let pv: Vec<Var> = self.params.iter().map(|&x| tape.var(x)).collect();
134 let mut x = [tape.constant(obs[0][0]), tape.constant(obs[0][1])];
135 let mut loss = tape.constant(0.0);
136 for o in obs.iter().skip(1) {
137 x = self.rk4_var(&pv, x, dt);
138 let e0 = x[0] - o[0];
139 let e1 = x[1] - o[1];
140 loss = loss + e0 * e0 + e1 * e1;
141 }
142 loss = loss * (1.0 / (obs.len() - 1) as f64);
143 let g = loss.backward();
144 let grad: Vec<f64> = pv.iter().map(|&x| g.wrt(x)).collect();
145
146 self.t += 1;
147 let (b1, b2, eps) = (0.9_f64, 0.999_f64, 1e-8);
148 let bc1 = 1.0 - b1.powi(self.t as i32);
149 let bc2 = 1.0 - b2.powi(self.t as i32);
150 for (i, &gi) in grad.iter().enumerate() {
151 self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
152 self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
153 self.params[i] -= lr * (self.m[i] / bc1) / ((self.v[i] / bc2).sqrt() + eps);
154 }
155 loss.value()
156 }
157
158 pub fn train(&mut self, obs: &[[f64; 2]], dt: f64, epochs: usize, lr: f64) -> f64 {
160 let mut l = f64::INFINITY;
161 for _ in 0..epochs {
162 l = self.train_step(obs, dt, lr);
163 }
164 l
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn true_field(x: f64, y: f64) -> (f64, f64) {
174 (y, -x - 0.15 * y)
175 }
176
177 fn trajectory(x0: f64, y0: f64, n: usize, dt: f64) -> Vec<[f64; 2]> {
178 let (mut x, mut y) = (x0, y0);
179 let mut out = vec![[x, y]];
180 for _ in 0..n {
181 let f = |x: f64, y: f64| true_field(x, y);
182 let (k1x, k1y) = f(x, y);
183 let (k2x, k2y) = f(x + 0.5 * dt * k1x, y + 0.5 * dt * k1y);
184 let (k3x, k3y) = f(x + 0.5 * dt * k2x, y + 0.5 * dt * k2y);
185 let (k4x, k4y) = f(x + dt * k3x, y + dt * k3y);
186 x += dt / 6.0 * (k1x + 2.0 * k2x + 2.0 * k3x + k4x);
187 y += dt / 6.0 * (k1y + 2.0 * k2y + 2.0 * k3y + k4y);
188 out.push([x, y]);
189 }
190 out
191 }
192
193 #[test]
194 fn neural_ode_learns_the_field_and_predicts_the_future() {
195 let dt = 0.12;
199 let full = trajectory(1.6, 0.0, 40, dt); let train = &full[..25]; let mut node = NeuralOde::new(16, 4);
202 node.train(train, dt, 1500, 5e-3);
203
204 let pred = node.rollout(&full[0], 40, dt);
205 let mut e_in = 0.0;
207 for i in 0..25 {
208 e_in += (pred[i][0] - full[i][0]).powi(2) + (pred[i][1] - full[i][1]).powi(2);
209 }
210 let rmse_in = (e_in / 25.0).sqrt();
211 assert!(rmse_in < 0.1, "rollout should match the training window: rmse {rmse_in}");
212 let mut e_out = 0.0;
214 for i in 25..40 {
215 e_out += (pred[i][0] - full[i][0]).powi(2) + (pred[i][1] - full[i][1]).powi(2);
216 }
217 let rmse_out = (e_out / 15.0).sqrt();
218 assert!(rmse_out < 0.25, "should extrapolate past the training window: rmse {rmse_out}");
219 }
220}