mini_ode/
lib.rs

1use anyhow::anyhow;
2use tch::IndexOp;
3use tch::Tensor;
4
5pub mod optimizers;
6
7/// Solves ODE using Euler method
8pub fn solve_euler(
9    f: tch::CModule,
10    x_span: Tensor,
11    y0: Tensor,
12    step: Tensor,
13) -> anyhow::Result<(Tensor, Tensor)> {
14    if x_span.size() != [2] {
15        return Err(anyhow!("x_span must be of shape [2]"));
16    }
17    if y0.size().len() != 1 {
18        return Err(anyhow!("y0 must be a one-dimensional tensor"));
19    }
20    if step.size().len() != 0 {
21        return Err(anyhow!("step must be a zero-dimensional tensor"));
22    }
23
24    let x_start = x_span.i(0);
25    let x_end = x_span.i(1);
26
27    let mut x = x_start.unsqueeze(0);
28    let mut y = y0.unsqueeze(0);
29
30    let mut all_x = vec![x.copy()];
31    let mut all_y = vec![y.copy()];
32
33    let mut current_step = step;
34    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
35        let remaining = &x_end - &x.squeeze();
36        if remaining.lt_tensor(&current_step) == Tensor::from(true) {
37            current_step = remaining.copy();
38        }
39
40        let dy = f.forward_ts(&[x.squeeze().copy(), y.squeeze().copy()])?;
41        y = &y + &current_step * &dy;
42        x = &x + &current_step;
43
44        all_x.push(x.copy());
45        all_y.push(y.copy());
46    }
47
48    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
49}
50
51/// Solves ODE using Runge-Kutta 4th order method
52pub fn solve_rk4(
53    f: tch::CModule,
54    x_span: Tensor,
55    y0: Tensor,
56    step: Tensor,
57) -> anyhow::Result<(Tensor, Tensor)> {
58    if x_span.size() != [2] {
59        return Err(anyhow!("x_span must be of shape [2]"));
60    }
61    if y0.size().len() != 1 {
62        return Err(anyhow!("y0 must be a one-dimensional tensor"));
63    }
64    if step.size().len() != 0 {
65        return Err(anyhow!("step must be a zero-dimensional tensor"));
66    }
67
68    let x_start = x_span.i(0);
69    let x_end = x_span.i(1);
70
71    let mut x = x_start.unsqueeze(0);
72    let mut y = y0.unsqueeze(0);
73
74    let mut all_x = vec![x.copy()];
75    let mut all_y = vec![y.copy()];
76
77    let mut current_step = step;
78    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
79        let remaining = &x_end - &x.squeeze();
80        if remaining.lt_tensor(&current_step) == Tensor::from(true) {
81            current_step = remaining.copy();
82        }
83
84        let k1 = f.forward_ts(&[x.squeeze().copy(), y.squeeze().copy()])?;
85
86        let x_half: Tensor = &x + 0.5 * &current_step;
87        let y_half: Tensor = &y + 0.5 * &current_step * &k1;
88        let k2 = f.forward_ts(&[x_half.squeeze(), y_half.squeeze()])?;
89
90        let x_half_again: Tensor = &x + 0.5 * &current_step;
91        let y_half_again: Tensor = &y + 0.5 * &current_step * &k2;
92        let k3 = f.forward_ts(&[x_half_again.squeeze(), y_half_again.squeeze()])?;
93
94        let x_full = &x + &current_step;
95        let y_full = &y + &current_step * &k3;
96        let k4 = f.forward_ts(&[x_full.squeeze(), y_full.squeeze()])?;
97
98        let step_div_6 = &current_step / 6.0;
99        let y_next = &y + &step_div_6 * (&k1 + 2.0 * &k2 + 2.0 * &k3 + &k4);
100
101        x = &x + &current_step;
102        y = y_next;
103
104        all_x.push(x.copy());
105        all_y.push(y.copy());
106    }
107
108    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
109}
110
111/// Solves ODE using Implicit Euler method with gradient descent optimization
112pub fn solve_implicit_euler(
113    f: tch::CModule,
114    x_span: Tensor,
115    y0: Tensor,
116    step: Tensor,
117    optimizer: &dyn optimizers::Optimizer,
118) -> anyhow::Result<(Tensor, Tensor)> {
119    if x_span.size() != [2] {
120        return Err(anyhow!("x_span must be of shape [2]"));
121    }
122    if y0.size().len() != 1 {
123        return Err(anyhow!("y0 must be a one-dimensional tensor"));
124    }
125    if step.size().len() != 0 {
126        return Err(anyhow!("step must be a zero-dimensional tensor"));
127    }
128
129    let x_start = x_span.i(0);
130    let x_end = x_span.i(1);
131
132    let mut x = x_start.unsqueeze(0);
133    let mut y = y0.unsqueeze(0);
134
135    let mut all_x = vec![x.copy()];
136    let mut all_y = vec![y.copy()];
137
138    let mut current_step = step;
139    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
140        let remaining = &x_end - &x.squeeze();
141        if remaining.lt_tensor(&current_step) == Tensor::from(true) {
142            current_step = remaining.copy();
143        }
144
145        let x_next = &x + &current_step;
146        let y_prev = y.copy();
147
148        let y_next = optimizer.optimize(
149            &|y_next: &Tensor| {
150                let f_next = f
151                    .forward_ts(&[x_next.squeeze().copy(), y_next.squeeze().copy()])
152                    .unwrap();
153                let y_pred = &y_prev.squeeze() + &current_step * &f_next;
154                (y_next - &y_pred).pow_tensor_scalar(2).sum(y_next.kind())
155            },
156            &(&y_prev.detach().squeeze()
157                + &current_step * f.forward_ts(&[&x.squeeze(), &y_prev.squeeze()])?),
158        ).map_err( |err| {
159            anyhow!(format!("Optimizer failed with: {}", err))
160        })?;
161
162        y = y_next.unsqueeze(0);
163        x = x_next.copy();
164
165        all_x.push(x.copy());
166        all_y.push(y.copy());
167    }
168
169    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
170}
171
172/// Solves ODE using Gauss-Legendre-Runge-Kutta 4th order method
173pub fn solve_glrk4(
174    f: tch::CModule,
175    x_span: Tensor,
176    y0: Tensor,
177    step: Tensor,
178    optimizer: &dyn optimizers::Optimizer,
179) -> anyhow::Result<(Tensor, Tensor)> {
180    if x_span.size() != [2] {
181        return Err(anyhow!("x_span must be of shape [2]"));
182    }
183    if y0.size().len() != 1 {
184        return Err(anyhow!("y0 must be a one-dimensional tensor"));
185    }
186    if step.size().len() != 0 {
187        return Err(anyhow!("step must be a zero-dimensional tensor"));
188    }
189
190    let x_start = x_span.i(0);
191    let x_end = x_span.i(1);
192
193    let mut x = x_start.unsqueeze(0);
194    let mut y = y0.unsqueeze(0);
195
196    let mut all_x = vec![x.copy()];
197    let mut all_y = vec![y.copy()];
198
199    let mut current_step = step;
200    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
201        let remaining = &x_end - &x.squeeze();
202        if remaining.lt_tensor(&current_step) == Tensor::from(true) {
203            current_step = remaining.copy();
204        }
205
206        let k = f.forward_ts(&[x.squeeze().copy(), y.squeeze().copy()])?;
207
208        const C1: f64 = 0.2113248654f64;
209        const C2: f64 = 0.7886751346f64;
210        const A11: f64 = 0.25;
211        const A12: f64 = -0.03867513459f64;
212        const A21: f64 = 0.5386751346f64;
213        const A22: f64 = 0.25;
214
215        let first_k1k2_guess = Tensor::cat(
216            &[
217                f.forward_ts(&[
218                    &x.squeeze() + C1 * &current_step,
219                    &y.squeeze() + C1 * &current_step * &k,
220                ])?,
221                f.forward_ts(&[
222                    &x.squeeze() + C2 * &current_step,
223                    &y.squeeze() + C2 * &current_step * &k,
224                ])?,
225            ],
226            0,
227        );
228        let k1k2 = optimizer.optimize(
229            &|k1k2_guess| {
230                let diff1 = k1k2_guess.i(0..=1)
231                    - f.forward_ts(&[
232                        &x.squeeze() + C1 * &current_step,
233                        &y.squeeze()
234                            + (A11 * k1k2_guess.i(0..=1) + A12 * k1k2_guess.i(2..=3))
235                                * &current_step,
236                    ])
237                    .unwrap();
238                let diff2 = k1k2_guess.i(2..=3)
239                    - f.forward_ts(&[
240                        &x.squeeze() + C2 * &current_step,
241                        &y.squeeze()
242                            + (A21 * k1k2_guess.i(0..=1) + A22 * k1k2_guess.i(2..=3))
243                                * &current_step,
244                    ])
245                    .unwrap();
246
247                diff1.dot(&diff1) + diff2.dot(&diff2)
248            },
249            &first_k1k2_guess,
250        ).map_err( |err| {
251            anyhow!(format!("Optimizer failed with: {}", err))
252        })?;
253        assert!(k1k2.size().len() == 1);
254        assert!(k1k2.size()[0] == 4);
255
256        x = &x + &current_step;
257        y = &y + &current_step * (0.5 * k1k2.i(0..=1) + 0.5 * k1k2.i(2..=3));
258
259        all_x.push(x.copy());
260        all_y.push(y.copy());
261    }
262
263    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
264}
265
266/// Solves ODE using Runge-Kutta-Fehlberg 45 adaptive method
267pub fn solve_rkf45(
268    f: tch::CModule,
269    x_span: Tensor,
270    y0: Tensor,
271    rtol: Tensor,
272    atol: Tensor,
273    min_step: Tensor,
274    safety_factor: f64,
275) -> anyhow::Result<(Tensor, Tensor)> {
276    if x_span.size() != [2] {
277        return Err(anyhow!("x_span must be of shape [2]"));
278    }
279    if y0.size().len() != 1 {
280        return Err(anyhow!("y0 must be a one-dimensional tensor"));
281    }
282    if rtol.size().len() != 0 || atol.size().len() != 0 || min_step.size().len() != 0 {
283        return Err(anyhow!("rtol, atol, and min_step must be scalar tensors"));
284    }
285
286    let x_start = x_span.i(0);
287    let x_end = x_span.i(1);
288
289    let mut x = x_start.unsqueeze(0);
290    let mut y = y0.unsqueeze(0);
291
292    let mut all_x = vec![x.copy()];
293    let mut all_y = vec![y.copy()];
294
295    let mut step = (&x_end - &x_start) * 0.1;
296    let safety_factor_tensor = Tensor::from(safety_factor);
297
298    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
299        let remaining = &x_end - &x.squeeze();
300        if remaining.lt_tensor(&step) == Tensor::from(true) {
301            step = remaining.copy();
302        }
303
304        let k1 = f.forward_ts(&[x.squeeze().copy(), y.squeeze().copy()])?;
305
306        let k2 = {
307            let x_step: Tensor = &x + 0.25 * &step;
308            let y_step: Tensor = &y + 0.25 * &step * &k1;
309            f.forward_ts(&[x_step.squeeze(), y_step.squeeze()])?
310        };
311
312        let k3 = {
313            let x_step: Tensor = &x + 0.375 * &step;
314            let y_step: Tensor = &y + (0.09375 * &step * &k1) + (0.28125 * &step * &k2);
315            f.forward_ts(&[x_step.squeeze(), y_step.squeeze()])?
316        };
317
318        let k4 = {
319            let x_step: Tensor = &x + (12.0 / 13.0) * &step;
320            let y_step: Tensor = &y
321                + (1932.0 / 2197.0 * &step * &k1)
322                + (-7200.0 / 2197.0 * &step * &k2)
323                + (7296.0 / 2197.0 * &step * &k3);
324            f.forward_ts(&[x_step.squeeze(), y_step.squeeze()])?
325        };
326
327        let k5 = {
328            let x_step: Tensor = &x + &step;
329            let y_step: Tensor = &y
330                + (439.0 / 216.0 * &step * &k1)
331                + (-8.0 * &step * &k2)
332                + (3680.0 / 513.0 * &step * &k3)
333                + (-845.0 / 4104.0 * &step * &k4);
334            f.forward_ts(&[x_step.squeeze(), y_step.squeeze()])?
335        };
336
337        let k6 = {
338            let x_step: Tensor = &x + 0.5 * &step;
339            let y_step: Tensor = &y
340                + (-8.0 / 27.0 * &step * &k1)
341                + (2.0 * &step * &k2)
342                + (-3544.0 / 2565.0 * &step * &k3)
343                + (1859.0 / 4104.0 * &step * &k4)
344                + (-11.0 / 40.0 * &step * &k5);
345            f.forward_ts(&[x_step.squeeze(), y_step.squeeze()])?
346        };
347
348        let next_y4: Tensor = &y
349            + &step
350                * ((25.0 / 216.0 * &k1)
351                    + (1408.0 / 2565.0 * &k3)
352                    + (2197.0 / 4104.0 * &k4)
353                    + (-1.0 / 5.0 * &k5));
354        let next_y5: Tensor = &y
355            + &step
356                * ((16.0 / 135.0 * &k1)
357                    + (6656.0 / 12825.0 * &k3)
358                    + (28561.0 / 56430.0 * &k4)
359                    + (-9.0 / 50.0 * &k5)
360                    + (2.0 / 55.0 * &k6));
361
362        let d = (&next_y4 - &next_y5).abs();
363        let e = next_y5.abs() * &rtol + &atol;
364
365        let alpha_tensor = (e / d).sqrt().min();
366        let condition = &safety_factor_tensor * &alpha_tensor;
367
368        let condition_met = condition.lt(1.0);
369        let condition_met_bool: bool = condition_met == Tensor::from(true);
370
371        if condition_met_bool {
372            step = &step * &condition;
373            if step.lt_tensor(&min_step) == Tensor::from_slice(&[true]) {
374                return Err(anyhow!("Required step is smaller than minimal step"));
375            }
376        } else {
377            y = next_y4;
378            x = &x + &step;
379            all_x.push(x.copy());
380            all_y.push(y.copy());
381
382            let new_step = &step * &condition;
383            let max_step = &step * 5.0;
384            step = new_step.fmin(&max_step);
385        }
386    }
387
388    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
389}
390
391/// Solves ODE using first-order Rosenbrock method (Row1)
392pub fn solve_row1(
393    f: tch::CModule,
394    x_span: Tensor,
395    y0: Tensor,
396    step: Tensor,
397) -> anyhow::Result<(Tensor, Tensor)> {
398    if x_span.size() != [2] {
399        return Err(anyhow!("x_span must be of shape [2]"));
400    }
401    if y0.size().len() != 1 {
402        return Err(anyhow!("y0 must be a one-dimensional tensor"));
403    }
404    if step.size().len() != 0 {
405        return Err(anyhow!("step must be a zero-dimensional tensor"));
406    }
407
408    let x_start = x_span.i(0);
409    let x_end = x_span.i(1);
410
411    let mut x = x_start.unsqueeze(0);
412    let mut y = y0.unsqueeze(0);
413
414    let mut all_x = vec![x.copy()];
415    let mut all_y = vec![y.copy()];
416
417    while x.lt_tensor(&x_end) == Tensor::from_slice(&[true]) {
418        let remaining = &x_end - &x.squeeze();
419        let mut current_step = step.copy();
420        if remaining.lt_tensor(&current_step) == Tensor::from(true) {
421            current_step = remaining.copy();
422        }
423
424        let x_prev = x.copy();
425        let y_prev = y.copy().squeeze();
426
427        let jacobian = compute_jacobian(
428            |y| {
429                f.forward_ts(&[x_prev.squeeze().copy(), y.copy()])
430                    .unwrap()
431                    .squeeze()
432            },
433            &y_prev,
434        );
435        let f_current = f
436            .forward_ts(&[x_prev.squeeze().copy(), y_prev.copy()])?
437            .squeeze();
438
439        let n = jacobian.size()[0];
440        let eye = Tensor::eye(n, (tch::Kind::Float, jacobian.device()));
441        let step_j = &current_step * &jacobian;
442        let inv_matrix = (eye - step_j).inverse();
443
444        let delta_y = inv_matrix.matmul(&f_current);
445        let y_next = y_prev + &current_step.squeeze() * delta_y;
446
447        x = &x_prev + &current_step;
448        y = y_next.unsqueeze(0);
449
450        all_x.push(x.copy());
451        all_y.push(y.copy());
452    }
453
454    Ok((Tensor::cat(&all_x, 0), Tensor::cat(&all_y, 0)))
455}
456
457/// Computes the Jacobian matrix of a function f at point x
458fn compute_jacobian<F>(f: F, x: &Tensor) -> Tensor
459where
460    F: Fn(&Tensor) -> Tensor,
461{
462    assert_eq!(x.dim(), 1, "x must be 1-dimensional");
463    let mut x_with_grad = x.detach().copy().set_requires_grad(true);
464    let y = f(&x_with_grad);
465    assert_eq!(y.dim(), 1, "y must be 1-dimensional");
466
467    let y_size = y.size()[0];
468    let mut grads = Vec::new();
469
470    for i in 0..y_size {
471        let yi = y.i(i);
472        //yi.backward();
473        //let grad = x_with_grad.grad().copy();
474        let grad = Tensor::run_backward(&[yi], &[&x_with_grad], true, false)[0].copy();
475        grads.push(grad.unsqueeze(0));
476        x_with_grad.zero_grad();
477    }
478
479    Tensor::cat(&grads, 0)
480}