1use super::functions::*;
6use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
7
8#[derive(Debug, Clone)]
10pub struct SecondVariation {
11 pub functional: String,
13 pub is_positive_definite: bool,
15}
16impl SecondVariation {
17 pub fn new(functional: impl Into<String>, is_positive_definite: bool) -> Self {
19 Self {
20 functional: functional.into(),
21 is_positive_definite,
22 }
23 }
24 pub fn legendre_condition(&self) -> bool {
27 self.is_positive_definite
28 }
29 pub fn jacobi_condition(&self) -> bool {
32 self.is_positive_definite
33 }
34}
35#[derive(Debug, Clone)]
37pub struct ConservedQuantity {
38 pub name: String,
40 pub symmetry: Symmetry,
42 pub expression: String,
44}
45impl ConservedQuantity {
46 pub fn new(name: impl Into<String>, symmetry: Symmetry, expression: impl Into<String>) -> Self {
48 Self {
49 name: name.into(),
50 symmetry,
51 expression: expression.into(),
52 }
53 }
54 pub fn is_conserved_on_shell(&self) -> bool {
56 self.symmetry.is_continuous
57 }
58}
59#[allow(dead_code)]
61#[derive(Debug, Clone, Default)]
62pub struct MorseData {
63 pub critical_points: Vec<MorseCriticalPointData>,
65}
66impl MorseData {
67 pub fn new() -> Self {
69 Self::default()
70 }
71 pub fn add_critical_point(&mut self, cp: MorseCriticalPointData) {
73 self.critical_points.push(cp);
74 }
75 pub fn morse_count_by_index(&self) -> Vec<usize> {
77 let max_idx = self
78 .critical_points
79 .iter()
80 .map(|cp| cp.morse_index)
81 .max()
82 .unwrap_or(0);
83 let mut counts = vec![0usize; max_idx + 1];
84 for cp in &self.critical_points {
85 counts[cp.morse_index] += 1;
86 }
87 counts
88 }
89 pub fn euler_characteristic(&self) -> i64 {
91 self.critical_points
92 .iter()
93 .map(|cp| cp.euler_contribution())
94 .sum()
95 }
96 pub fn check_weak_morse_inequality(&self, betti: &[usize]) -> bool {
98 let counts = self.morse_count_by_index();
99 betti
100 .iter()
101 .enumerate()
102 .all(|(k, &b)| counts.get(k).copied().unwrap_or(0) >= b)
103 }
104}
105#[allow(dead_code)]
107#[derive(Debug, Clone)]
108pub struct NoetherSymmetryData {
109 pub name: String,
111 pub generator: Vec<String>,
113 pub conserved_current: Vec<String>,
115 pub conserved_charge: String,
117 pub is_gauge: bool,
119}
120impl NoetherSymmetryData {
121 pub fn new(name: &str, generator: Vec<String>, conserved_charge: &str, is_gauge: bool) -> Self {
123 Self {
124 name: name.to_string(),
125 generator,
126 conserved_current: Vec::new(),
127 conserved_charge: conserved_charge.to_string(),
128 is_gauge,
129 }
130 }
131 pub fn set_current(&mut self, current: Vec<String>) {
133 self.conserved_current = current;
134 }
135 pub fn is_on_shell_conserved(&self) -> bool {
137 !self.is_gauge
138 }
139 pub fn divergence_free_condition(&self) -> String {
141 let n = self.conserved_current.len();
142 if n == 0 {
143 return format!("div J = 0 [{}]", self.name);
144 }
145 let terms: Vec<String> = (0..n).map(|mu| format!("∂_{mu}(J^{mu})")).collect();
146 format!("{} = 0", terms.join(" + "))
147 }
148 pub fn energy_string(&self) -> String {
150 format!("E = ∫ T^{{00}} d^3x [symmetry: {}]", self.name)
151 }
152}
153#[derive(Debug, Clone)]
160pub struct MinimalSurfaceFinder {
161 pub resolution: usize,
163 pub boundary: Vec<f64>,
165}
166impl MinimalSurfaceFinder {
167 pub fn new(resolution: usize) -> Self {
169 let n = resolution;
170 let mut boundary = vec![0.0_f64; 4 * n];
171 for k in 0..n {
172 let t = k as f64 / (n - 1) as f64;
173 let angle = t * std::f64::consts::PI * 0.5;
174 boundary[k] = angle.cos();
175 boundary[n + k] = angle.sin();
176 boundary[2 * n + k] = -(angle.cos());
177 boundary[3 * n + k] = -(angle.sin());
178 }
179 Self {
180 resolution,
181 boundary,
182 }
183 }
184 pub fn solve(&self, max_iter: usize, omega: f64) -> Vec<Vec<f64>> {
187 let n = self.resolution;
188 let ni = if n > 2 { n - 2 } else { 0 };
189 let mut u = vec![vec![0.0_f64; n]; n];
190 let bv = 1.0_f64;
191 for j in 0..n {
192 u[0][j] = bv;
193 u[n - 1][j] = bv;
194 }
195 for i in 0..n {
196 u[i][0] = bv;
197 u[i][n - 1] = bv;
198 }
199 for _ in 0..max_iter {
200 for i in 1..=ni {
201 for j in 1..=ni {
202 let avg = (u[i - 1][j] + u[i + 1][j] + u[i][j - 1] + u[i][j + 1]) / 4.0;
203 u[i][j] = (1.0 - omega) * u[i][j] + omega * avg;
204 }
205 }
206 }
207 u
208 }
209 pub fn dirichlet_energy(&self, u: &[Vec<f64>]) -> f64 {
211 let n = u.len();
212 if n < 2 {
213 return 0.0;
214 }
215 let mut energy = 0.0;
216 for i in 0..n {
217 for j in 0..n {
218 if i + 1 < n {
219 energy += (u[i + 1][j] - u[i][j]).powi(2);
220 }
221 if j + 1 < n {
222 energy += (u[i][j + 1] - u[i][j]).powi(2);
223 }
224 }
225 }
226 energy * 0.5
227 }
228}
229#[derive(Debug, Clone)]
231pub struct ActionFunctional {
232 pub lagrangian: String,
234 pub time_start: f64,
236 pub time_end: f64,
238}
239impl ActionFunctional {
240 pub fn new(lagrangian: impl Into<String>, time_start: f64, time_end: f64) -> Self {
242 Self {
243 lagrangian: lagrangian.into(),
244 time_start,
245 time_end,
246 }
247 }
248 pub fn is_bounded_below(&self) -> bool {
251 self.time_end > self.time_start
252 }
253}
254#[derive(Debug, Clone)]
262pub struct GeodesicOnSurface {
263 pub profile: Vec<(f64, f64)>,
265 pub start: (f64, f64),
267 pub end: (f64, f64),
269 pub n_nodes: usize,
271}
272impl GeodesicOnSurface {
273 pub fn new(
275 profile: Vec<(f64, f64)>,
276 start: (f64, f64),
277 end: (f64, f64),
278 n_nodes: usize,
279 ) -> Self {
280 Self {
281 profile,
282 start,
283 end,
284 n_nodes,
285 }
286 }
287 pub fn r_at(&self, z: f64) -> f64 {
289 if self.profile.len() < 2 {
290 return 1.0;
291 }
292 let z0 = self.profile[0].0;
293 let z1 = self.profile[self.profile.len() - 1].0;
294 let t = (z - z0) / (z1 - z0).max(f64::EPSILON);
295 let t = t.clamp(0.0, 1.0);
296 let idx = ((t * (self.profile.len() - 1) as f64) as usize).min(self.profile.len() - 2);
297 let (za, ra) = self.profile[idx];
298 let (zb, rb) = self.profile[idx + 1];
299 let u = (z - za) / (zb - za).max(f64::EPSILON);
300 ra + u * (rb - ra)
301 }
302 pub fn solve(&self, max_iter: usize) -> Vec<(f64, f64)> {
309 let n = self.n_nodes;
310 let (z0, theta0) = self.start;
311 let (z1, theta1) = self.end;
312 let hz = (z1 - z0) / (n as f64 + 1.0);
313 let mut theta = vec![0.0_f64; n];
314 for i in 0..n {
315 let t = (i + 1) as f64 / (n as f64 + 1.0);
316 theta[i] = theta0 + t * (theta1 - theta0);
317 }
318 let tau = 1e-4_f64;
319 for _ in 0..max_iter {
320 let mut grad = vec![0.0_f64; n];
321 for i in 0..n {
322 let z = z0 + (i + 1) as f64 * hz;
323 let r = self.r_at(z);
324 let th_left = if i == 0 { theta0 } else { theta[i - 1] };
325 let th_right = if i == n - 1 { theta1 } else { theta[i + 1] };
326 let s_r = (th_right - theta[i]) / hz;
327 let s_l = (theta[i] - th_left) / hz;
328 let w = r * r;
329 grad[i] = -(w * s_r / (1.0 + w * s_r * s_r).sqrt()
330 - w * s_l / (1.0 + w * s_l * s_l).sqrt())
331 / hz;
332 }
333 for i in 0..n {
334 theta[i] -= tau * grad[i];
335 }
336 }
337 let mut result = Vec::with_capacity(n + 2);
338 result.push((z0, theta0));
339 for i in 0..n {
340 result.push((z0 + (i + 1) as f64 * hz, theta[i]));
341 }
342 result.push((z1, theta1));
343 result
344 }
345}
346#[derive(Debug, Clone)]
351pub struct OptimalTransportCost {
352 pub p: f64,
354 pub source: Vec<f64>,
356 pub target: Vec<f64>,
358}
359impl OptimalTransportCost {
360 pub fn new(p: f64, mut source: Vec<f64>, mut target: Vec<f64>) -> Self {
362 source.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
363 target.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
364 Self { p, source, target }
365 }
366 pub fn compute(&self) -> f64 {
370 let n = self.source.len().min(self.target.len());
371 if n == 0 {
372 return 0.0;
373 }
374 self.source[..n]
375 .iter()
376 .zip(self.target[..n].iter())
377 .map(|(x, y)| (x - y).abs().powf(self.p))
378 .sum::<f64>()
379 / n as f64
380 }
381 pub fn w1_distance(&self) -> f64 {
383 let n = self.source.len().min(self.target.len());
384 if n == 0 {
385 return 0.0;
386 }
387 self.source[..n]
388 .iter()
389 .zip(self.target[..n].iter())
390 .map(|(x, y)| (x - y).abs())
391 .sum::<f64>()
392 / n as f64
393 }
394 pub fn w2_distance(&self) -> f64 {
396 let n = self.source.len().min(self.target.len());
397 if n == 0 {
398 return 0.0;
399 }
400 let w2sq = self.source[..n]
401 .iter()
402 .zip(self.target[..n].iter())
403 .map(|(x, y)| (x - y).powi(2))
404 .sum::<f64>()
405 / n as f64;
406 w2sq.sqrt()
407 }
408}
409#[derive(Debug, Clone)]
411pub struct LagrangianFunction {
412 pub name: String,
414 pub vars: Vec<String>,
416 pub n_dof: usize,
418}
419impl LagrangianFunction {
420 pub fn new(n_dof: usize) -> Self {
422 let vars = (0..n_dof).map(|i| format!("q_{i}")).collect();
423 Self {
424 name: format!("L_{n_dof}dof"),
425 vars,
426 n_dof,
427 }
428 }
429 pub fn euler_lagrange_equation_string(&self) -> String {
433 let eqs: Vec<String> = (0..self.n_dof)
434 .map(|i| format!("d/dt(∂L/∂q̇_{i}) - ∂L/∂q_{i} = 0"))
435 .collect();
436 eqs.join("; ")
437 }
438}
439#[derive(Debug, Clone)]
444pub struct EulerLagrangeSolver {
445 pub a: f64,
447 pub b: f64,
449 pub ya: f64,
451 pub yb: f64,
453 pub n_interior: usize,
455 pub step_size: f64,
457}
458impl EulerLagrangeSolver {
459 pub fn new(a: f64, b: f64, ya: f64, yb: f64, n_interior: usize, step_size: f64) -> Self {
461 Self {
462 a,
463 b,
464 ya,
465 yb,
466 n_interior,
467 step_size,
468 }
469 }
470 pub fn h(&self) -> f64 {
472 (self.b - self.a) / (self.n_interior as f64 + 1.0)
473 }
474 pub fn linear_initial_guess(&self) -> Vec<f64> {
476 let n = self.n_interior;
477 let h = self.h();
478 (1..=(n as u64))
479 .map(|k| {
480 let x = self.a + k as f64 * h;
481 let t = (x - self.a) / (self.b - self.a);
482 self.ya + t * (self.yb - self.ya)
483 })
484 .collect()
485 }
486 pub fn solve_arc_length(&self, max_iter: usize) -> Vec<f64> {
491 let n = self.n_interior;
492 let h = self.h();
493 let mut y = self.linear_initial_guess();
494 for _ in 0..max_iter {
495 let mut grad = vec![0.0_f64; n];
496 for i in 0..n {
497 let y_left = if i == 0 { self.ya } else { y[i - 1] };
498 let y_right = if i == n - 1 { self.yb } else { y[i + 1] };
499 let s_r = (y_right - y[i]) / h;
500 let s_l = (y[i] - y_left) / h;
501 grad[i] = -(s_r / (1.0 + s_r * s_r).sqrt() - s_l / (1.0 + s_l * s_l).sqrt()) / h;
502 }
503 for i in 0..n {
504 y[i] -= self.step_size * grad[i];
505 }
506 }
507 y
508 }
509 pub fn arc_length(&self, y_interior: &[f64]) -> f64 {
511 let h = self.h();
512 let n = y_interior.len();
513 let mut total = 0.0;
514 for i in 0..=n {
515 let y_left = if i == 0 { self.ya } else { y_interior[i - 1] };
516 let y_right = if i == n { self.yb } else { y_interior[i] };
517 let slope = (y_right - y_left) / h;
518 total += h * (1.0 + slope * slope).sqrt();
519 }
520 total
521 }
522}
523#[derive(Debug, Clone)]
525pub struct MinimalSurfaceProblem {
526 pub boundary: String,
528}
529impl MinimalSurfaceProblem {
530 pub fn new(boundary: impl Into<String>) -> Self {
532 Self {
533 boundary: boundary.into(),
534 }
535 }
536 pub fn euler_lagrange_equation(&self) -> String {
539 "(1 + u_y^2) * u_xx - 2 * u_x * u_y * u_xy + (1 + u_x^2) * u_yy = 0 (H = 0)".to_string()
540 }
541 pub fn minimal_surface_area_lower_bound(&self) -> f64 {
543 0.0
544 }
545}
546#[derive(Debug, Clone)]
548pub struct LowerSemicontinuous {
549 pub functional: String,
551 pub topology: String,
553}
554impl LowerSemicontinuous {
555 pub fn new(functional: impl Into<String>, topology: impl Into<String>) -> Self {
557 Self {
558 functional: functional.into(),
559 topology: topology.into(),
560 }
561 }
562 pub fn direct_method_applicable(&self) -> bool {
565 true
566 }
567}
568#[derive(Debug, Clone)]
574pub struct WassersteinGradientFlow {
575 pub tau: f64,
577 pub positions: Vec<f64>,
579 pub n_particles: usize,
581 pub potential_alpha: f64,
583}
584impl WassersteinGradientFlow {
585 pub fn new(n_particles: usize, tau: f64, potential_alpha: f64) -> Self {
587 let positions: Vec<f64> = (0..n_particles)
588 .map(|i| {
589 let p = (i as f64 + 0.5) / n_particles as f64;
590 let t = (-2.0 * (p * (1.0 - p)).ln()).sqrt();
591 let c0 = 2.515517_f64;
592 let c1 = 0.802853_f64;
593 let c2 = 0.010328_f64;
594 let d1 = 1.432788_f64;
595 let d2 = 0.189269_f64;
596 let d3 = 0.001308_f64;
597 let num = c0 + c1 * t + c2 * t * t;
598 let den = 1.0 + d1 * t + d2 * t * t + d3 * t * t * t;
599 if p < 0.5 {
600 -(t - num / den)
601 } else {
602 t - num / den
603 }
604 })
605 .collect();
606 Self {
607 tau,
608 positions,
609 n_particles,
610 potential_alpha,
611 }
612 }
613 pub fn step(&mut self) {
619 let factor = 1.0 / (1.0 + self.tau * self.potential_alpha);
620 for x in self.positions.iter_mut() {
621 *x *= factor;
622 }
623 }
624 pub fn run(&mut self, n_steps: usize) {
626 for _ in 0..n_steps {
627 self.step();
628 }
629 }
630 pub fn mean(&self) -> f64 {
632 self.positions.iter().sum::<f64>() / self.n_particles as f64
633 }
634 pub fn variance(&self) -> f64 {
636 let mu = self.mean();
637 self.positions.iter().map(|x| (x - mu).powi(2)).sum::<f64>() / self.n_particles as f64
638 }
639 pub fn steady_state_variance(&self) -> f64 {
641 if self.potential_alpha.abs() < f64::EPSILON {
642 f64::INFINITY
643 } else {
644 1.0 / self.potential_alpha
645 }
646 }
647}
648#[derive(Debug, Clone)]
650pub struct GeodesicEquation {
651 pub metric: String,
653 pub dimension: usize,
655}
656impl GeodesicEquation {
657 pub fn new(dimension: usize) -> Self {
659 Self {
660 metric: format!("g_{{ij}} on R^{dimension}"),
661 dimension,
662 }
663 }
664 pub fn christoffel_symbols_string(&self) -> String {
666 format!(
667 "Gamma^k_{{ij}} = (1/2) g^{{kl}} (partial_i g_{{jl}} + partial_j g_{{il}} - partial_l g_{{ij}}) \
668 for i,j,k,l in 1..{}",
669 self.dimension
670 )
671 }
672 pub fn is_straight_line_in_flat_space(&self) -> bool {
674 true
675 }
676}
677#[derive(Debug, Clone)]
679pub struct BrachistochroneProblem {
680 pub start: (f64, f64),
682 pub end: (f64, f64),
684}
685impl BrachistochroneProblem {
686 pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
688 Self { start, end }
689 }
690 pub fn cycloid_solution(&self) -> String {
692 "x(theta) = R * (theta - sin(theta)), \
693 y(theta) = R * (1 - cos(theta)), \
694 where R is chosen so the cycloid passes through the endpoint."
695 .to_string()
696 }
697 pub fn time_on_cycloid(&self) -> f64 {
700 let dy = (self.end.1 - self.start.1).abs();
701 if dy < f64::EPSILON {
702 return 0.0;
703 }
704 let g = 9.81_f64;
705 let r = dy / 2.0;
706 std::f64::consts::PI * (r / g).sqrt()
707 }
708}
709#[derive(Debug, Clone, PartialEq, Eq)]
711pub enum SymmetryType {
712 TimeTranslation,
714 SpaceTranslation,
716 Rotation,
718 Boost,
720 Internal,
722}
723#[allow(dead_code)]
728#[derive(Debug, Clone)]
729pub struct YangMillsEnergy {
730 pub n: usize,
732 pub links: Vec<[f64; 2]>,
735}
736impl YangMillsEnergy {
737 pub fn new(n: usize) -> Self {
739 Self {
740 n,
741 links: vec![[0.0; 2]; n * n],
742 }
743 }
744 pub fn set_link(&mut self, i: usize, j: usize, dir: usize, theta: f64) {
746 if i < self.n && j < self.n && dir < 2 {
747 self.links[i * self.n + j][dir] = theta;
748 }
749 }
750 pub fn get_link(&self, i: usize, j: usize, dir: usize) -> f64 {
752 if i < self.n && j < self.n && dir < 2 {
753 self.links[i * self.n + j][dir]
754 } else {
755 0.0
756 }
757 }
758 pub fn plaquette_angle(&self, i: usize, j: usize) -> f64 {
760 let n = self.n;
761 let i1 = (i + 1) % n;
762 let j1 = (j + 1) % n;
763 let a = self.get_link(i, j, 0);
764 let b = self.get_link(i1, j, 1);
765 let c = self.get_link(i, j1, 0);
766 let d = self.get_link(i, j, 1);
767 a + b - c - d
768 }
769 pub fn total_energy(&self) -> f64 {
771 let n = self.n;
772 (0..n)
773 .flat_map(|i| (0..n).map(move |j| (i, j)))
774 .map(|(i, j)| 1.0 - self.plaquette_angle(i, j).cos())
775 .sum()
776 }
777 pub fn gradient_descent_step(&mut self, step_size: f64) {
779 let n = self.n;
780 let mut grad = vec![[0.0_f64; 2]; n * n];
781 for i in 0..n {
782 for j in 0..n {
783 let theta_p = self.plaquette_angle(i, j);
784 let sin_p = theta_p.sin();
785 let j_prev = if j == 0 { n - 1 } else { j - 1 };
786 let theta_p_prev = self.plaquette_angle(i, j_prev);
787 grad[i * n + j][0] += sin_p - theta_p_prev.sin();
788 let i_prev = if i == 0 { n - 1 } else { i - 1 };
789 let theta_p_iprev = self.plaquette_angle(i_prev, j);
790 grad[i * n + j][1] += sin_p - theta_p_iprev.sin();
791 }
792 }
793 for k in 0..(n * n) {
794 self.links[k][0] -= step_size * grad[k][0];
795 self.links[k][1] -= step_size * grad[k][1];
796 }
797 }
798}
799#[allow(dead_code)]
801#[derive(Debug, Clone)]
802pub struct MorseCriticalPointData {
803 pub label: String,
805 pub critical_value: f64,
807 pub morse_index: usize,
809 pub non_degenerate: bool,
811}
812impl MorseCriticalPointData {
813 pub fn new(label: &str, critical_value: f64, morse_index: usize, non_degenerate: bool) -> Self {
815 Self {
816 label: label.to_string(),
817 critical_value,
818 morse_index,
819 non_degenerate,
820 }
821 }
822 pub fn is_local_minimum(&self) -> bool {
824 self.non_degenerate && self.morse_index == 0
825 }
826 pub fn is_saddle_point(&self) -> bool {
828 self.non_degenerate && self.morse_index > 0
829 }
830 pub fn euler_contribution(&self) -> i64 {
832 if self.morse_index % 2 == 0 {
833 1
834 } else {
835 -1
836 }
837 }
838}
839#[derive(Debug, Clone)]
841pub struct FirstVariation {
842 pub functional: String,
844 pub direction: String,
846}
847impl FirstVariation {
848 pub fn new(functional: impl Into<String>, direction: impl Into<String>) -> Self {
850 Self {
851 functional: functional.into(),
852 direction: direction.into(),
853 }
854 }
855 pub fn vanishes_at_critical_point(&self) -> bool {
858 true
859 }
860}
861#[derive(Debug, Clone)]
863pub struct SobolevSpace {
864 pub domain: String,
866 pub k: u32,
868 pub p: f64,
870}
871impl SobolevSpace {
872 pub fn new(domain: impl Into<String>, k: u32, p: f64) -> Self {
874 Self {
875 domain: domain.into(),
876 k,
877 p,
878 }
879 }
880 pub fn norm_formula(&self) -> String {
882 format!(
883 "||u||_{{W^{{{},{}}}({})}} = (sum_{{|alpha|<={}}} ||D^alpha u||_{{L^{}}}^{})^{{1/{}}}",
884 self.k, self.p as u32, self.domain, self.k, self.p as u32, self.p as u32, self.p as u32
885 )
886 }
887 pub fn is_hilbert_space(&self) -> bool {
889 self.k >= 1 && (self.p - 2.0).abs() < f64::EPSILON
890 }
891}
892#[derive(Debug, Clone)]
894pub struct Symmetry {
895 pub name: String,
897 pub transformation_type: SymmetryType,
899 pub is_continuous: bool,
901}
902impl Symmetry {
903 pub fn new(
905 name: impl Into<String>,
906 transformation_type: SymmetryType,
907 is_continuous: bool,
908 ) -> Self {
909 Self {
910 name: name.into(),
911 transformation_type,
912 is_continuous,
913 }
914 }
915}
916#[derive(Debug, Clone)]
918pub struct NoetherCorrespondence {
919 pub symmetry: Symmetry,
921 pub conserved_charge: ConservedQuantity,
923}
924impl NoetherCorrespondence {
925 pub fn new(symmetry: Symmetry, conserved_charge: ConservedQuantity) -> Self {
927 Self {
928 symmetry,
929 conserved_charge,
930 }
931 }
932 pub fn noether_current(&self) -> String {
934 format!(
935 "j^mu = (∂L/∂(∂_mu phi)) * delta_phi [symmetry: {}]",
936 self.symmetry.name
937 )
938 }
939}
940#[derive(Debug, Clone)]
942pub struct ControlSystem {
943 pub state_dim: usize,
945 pub control_dim: usize,
947 pub dynamics: String,
949}
950impl ControlSystem {
951 pub fn new(state_dim: usize, control_dim: usize) -> Self {
953 Self {
954 state_dim,
955 control_dim,
956 dynamics: format!("dx/dt = f(x in R^{state_dim}, u in R^{control_dim}, t)"),
957 }
958 }
959 pub fn is_controllable(&self) -> bool {
961 self.control_dim >= 1 && self.state_dim >= 1
962 }
963 pub fn is_observable(&self) -> bool {
965 self.state_dim >= 1
966 }
967}
968#[derive(Debug, Clone)]
970pub struct IsoperimetricProblem {
971 pub constraint: String,
973 pub objective: String,
975}
976impl IsoperimetricProblem {
977 pub fn new(constraint: impl Into<String>, objective: impl Into<String>) -> Self {
979 Self {
980 constraint: constraint.into(),
981 objective: objective.into(),
982 }
983 }
984 pub fn lagrange_multiplier_method(&self) -> String {
986 format!(
987 "Extremize F[y] = {} subject to G[y] = {}. \
988 Form augmented functional H[y] = F[y] - lambda * G[y] \
989 and apply Euler-Lagrange to H.",
990 self.objective, self.constraint
991 )
992 }
993}
994#[allow(dead_code)]
998#[derive(Debug, Clone, Default)]
999pub struct LyusternikSchnirelmannData {
1000 pub ls_category: usize,
1002 pub minimax_values: Vec<f64>,
1004 pub critical_point_labels: Vec<String>,
1006}
1007impl LyusternikSchnirelmannData {
1008 pub fn new(ls_category: usize) -> Self {
1010 Self {
1011 ls_category,
1012 minimax_values: Vec::new(),
1013 critical_point_labels: Vec::new(),
1014 }
1015 }
1016 pub fn add_minimax_value(&mut self, value: f64, label: &str) {
1018 self.minimax_values.push(value);
1019 self.critical_point_labels.push(label.to_string());
1020 }
1021 pub fn all_critical_points_found(&self) -> bool {
1023 self.minimax_values.len() >= self.ls_category
1024 }
1025 pub fn lower_bound_on_critical_points(&self) -> usize {
1027 self.ls_category
1028 }
1029 pub fn is_ordered(&self) -> bool {
1031 self.minimax_values.windows(2).all(|w| w[0] <= w[1] + 1e-12)
1032 }
1033 pub fn mountain_pass_value(&self) -> Option<f64> {
1035 self.minimax_values.get(1).copied()
1036 }
1037}
1038#[allow(non_snake_case)]
1040#[derive(Debug, Clone)]
1041pub struct HamiltonianMechanics {
1042 pub n_dof: usize,
1044 #[allow(non_snake_case)]
1046 pub H: String,
1047}
1048impl HamiltonianMechanics {
1049 pub fn new(n: usize) -> Self {
1051 let coords: Vec<String> = (0..n).map(|i| format!("q_{i}")).collect();
1052 let momenta: Vec<String> = (0..n).map(|i| format!("p_{i}")).collect();
1053 let all_vars = [coords.as_slice(), momenta.as_slice()].concat().join(", ");
1054 Self {
1055 n_dof: n,
1056 H: format!("H({all_vars}, t)"),
1057 }
1058 }
1059 pub fn hamilton_equations_string(&self) -> Vec<String> {
1061 (0..self.n_dof)
1062 .flat_map(|i| {
1063 vec![
1064 format!("dq_{i}/dt = ∂H/∂p_{i}"),
1065 format!("dp_{i}/dt = -∂H/∂q_{i}"),
1066 ]
1067 })
1068 .collect()
1069 }
1070 pub fn phase_space_dim(&self) -> usize {
1072 2 * self.n_dof
1073 }
1074}
1075#[derive(Debug, Clone)]
1077pub struct HamiltonJacobiBellman {
1078 pub value_function: String,
1080 pub dimension: usize,
1082}
1083impl HamiltonJacobiBellman {
1084 pub fn new(dimension: usize) -> Self {
1086 Self {
1087 value_function: format!("V : R^{dimension} x [0,T] -> R"),
1088 dimension,
1089 }
1090 }
1091 pub fn verification_theorem(&self) -> String {
1094 format!(
1095 "If V : R^{dim} x [0,T] -> R is C^1 and satisfies \
1096 -∂V/∂t = min_u H(x, u, ∇_x V, t) with V(x,T) = phi(x), \
1097 then V is the optimal value function and u*(x,t) = argmin_u H is optimal.",
1098 dim = self.dimension
1099 )
1100 }
1101}
1102#[derive(Debug, Clone)]
1108pub struct GammaLimit {
1109 pub sequence_name: String,
1111 pub limit_name: String,
1113}
1114impl GammaLimit {
1115 pub fn new(sequence_name: impl Into<String>, limit_name: impl Into<String>) -> Self {
1117 Self {
1118 sequence_name: sequence_name.into(),
1119 limit_name: limit_name.into(),
1120 }
1121 }
1122 pub fn lsc_envelope(&self) -> String {
1124 format!(
1125 "Gamma-lim F_n = lsc envelope of (pointwise lim F_n) \
1126 [sequence: {}, limit: {}]",
1127 self.sequence_name, self.limit_name
1128 )
1129 }
1130 pub fn recovery_sequence_exists(&self) -> bool {
1132 true
1133 }
1134}
1135#[derive(Debug, Clone)]
1137pub struct WeakConvergence {
1138 pub sequence: String,
1140 pub limit: String,
1142 pub space: SobolevSpace,
1144}
1145impl WeakConvergence {
1146 pub fn new(sequence: impl Into<String>, limit: impl Into<String>, space: SobolevSpace) -> Self {
1148 Self {
1149 sequence: sequence.into(),
1150 limit: limit.into(),
1151 space,
1152 }
1153 }
1154 pub fn is_weakly_convergent(&self) -> bool {
1156 self.space.p > 1.0
1157 }
1158}
1159#[derive(Debug, Clone)]
1161pub struct PontryaginPrinciple {
1162 pub system: ControlSystem,
1164 pub cost: String,
1166}
1167impl PontryaginPrinciple {
1168 pub fn new(system: ControlSystem, cost: impl Into<String>) -> Self {
1170 Self {
1171 system,
1172 cost: cost.into(),
1173 }
1174 }
1175 pub fn hamiltonian_string(&self) -> String {
1177 format!(
1178 "H(x, u, p, t) = L(x, u, t) + p^T * f(x, u, t) \
1179 where p in R^{} is the costate (adjoint variable)",
1180 self.system.state_dim
1181 )
1182 }
1183}
1184#[derive(Debug, Clone)]
1186pub struct Functional {
1187 pub name: String,
1189 pub domain: String,
1191 pub codomain: String,
1193}
1194impl Functional {
1195 pub fn new(
1197 name: impl Into<String>,
1198 domain: impl Into<String>,
1199 codomain: impl Into<String>,
1200 ) -> Self {
1201 Self {
1202 name: name.into(),
1203 domain: domain.into(),
1204 codomain: codomain.into(),
1205 }
1206 }
1207}
1208#[derive(Debug, Clone)]
1210pub struct ELSolution {
1211 pub lagrangian: LagrangianFunction,
1213 pub solution: String,
1215 pub is_extremal: bool,
1217}
1218impl ELSolution {
1219 pub fn new() -> Self {
1221 Self {
1222 lagrangian: LagrangianFunction::new(1),
1223 solution: "q(t) = q_0 + (q_1 - q_0) * t".to_string(),
1224 is_extremal: true,
1225 }
1226 }
1227 pub fn is_local_minimum(&self) -> bool {
1229 self.is_extremal
1230 }
1231 pub fn is_local_maximum(&self) -> bool {
1233 false
1234 }
1235 pub fn is_saddle(&self) -> bool {
1237 !self.is_extremal
1238 }
1239}
1240#[derive(Debug, Clone)]
1242pub struct LegendreTransform {
1243 pub function: String,
1245 pub variable: String,
1247 pub conjugate: String,
1249}
1250impl LegendreTransform {
1251 pub fn new(
1253 function: impl Into<String>,
1254 variable: impl Into<String>,
1255 conjugate: impl Into<String>,
1256 ) -> Self {
1257 Self {
1258 function: function.into(),
1259 variable: variable.into(),
1260 conjugate: conjugate.into(),
1261 }
1262 }
1263 pub fn is_involution(&self) -> bool {
1265 true
1266 }
1267}
1268#[allow(dead_code)]
1275#[derive(Debug, Clone, Default)]
1276pub struct PalaisSmaleChecker {
1277 pub values: Vec<f64>,
1279 pub gradient_norms: Vec<f64>,
1281 pub step_sizes: Vec<f64>,
1283}
1284impl PalaisSmaleChecker {
1285 pub fn new() -> Self {
1287 Self::default()
1288 }
1289 pub fn record(&mut self, value: f64, grad_norm: f64, step_size: f64) {
1291 self.values.push(value);
1292 self.gradient_norms.push(grad_norm);
1293 self.step_sizes.push(step_size);
1294 }
1295 pub fn values_bounded(&self, bound: f64) -> bool {
1297 self.values.iter().all(|v| v.abs() <= bound)
1298 }
1299 pub fn gradients_converge_to_zero(&self, tol: f64) -> bool {
1301 self.gradient_norms
1302 .last()
1303 .map(|&g| g < tol)
1304 .unwrap_or(false)
1305 }
1306 pub fn satisfies_approximate_ps(&self, value_bound: f64, grad_tol: f64) -> bool {
1309 if !self.values_bounded(value_bound) || !self.gradients_converge_to_zero(grad_tol) {
1310 return false;
1311 }
1312 if self.step_sizes.len() < 2 {
1313 return true;
1314 }
1315 self.step_sizes.windows(2).all(|w| w[1] <= w[0] + 1e-12)
1316 }
1317 pub fn min_gradient_norm(&self) -> f64 {
1319 self.gradient_norms
1320 .iter()
1321 .cloned()
1322 .fold(f64::INFINITY, f64::min)
1323 }
1324}
1325#[derive(Debug, Clone)]
1327pub struct LagrangianMechanics {
1328 pub n_dof: usize,
1330 pub constraints: Vec<String>,
1332}
1333impl LagrangianMechanics {
1334 pub fn new(n: usize) -> Self {
1336 Self {
1337 n_dof: n,
1338 constraints: vec![],
1339 }
1340 }
1341 pub fn kinetic_energy_string(&self) -> String {
1343 let terms: Vec<String> = (0..self.n_dof)
1344 .map(|i| format!("m_{i} * q̇_{i}^2"))
1345 .collect();
1346 format!("T = (1/2) * ({})", terms.join(" + "))
1347 }
1348 pub fn potential_energy_string(&self) -> String {
1350 let args: Vec<String> = (0..self.n_dof).map(|i| format!("q_{i}")).collect();
1351 format!("V = V({})", args.join(", "))
1352 }
1353}