Skip to main content

qualia_core_db/specialized_libs/physics_simulation/
mechanics.rs

1use super::*;
2
3/// Estimate a period from a sampled oscillatory signal by timing successive upward
4/// crossings of its mean. Pure post-hoc analysis of the integrated result — not an
5/// integrator. Returns 0.0 if fewer than two crossings were captured.
6fn estimate_period_from_crossings(times: &[f64], values: &[f64]) -> f64 {
7    if values.is_empty() {
8        return 0.0;
9    }
10    let mean = values.iter().sum::<f64>() / values.len() as f64;
11    let mut crossings: Vec<f64> = Vec::new();
12    for i in 1..values.len() {
13        let a = values[i - 1] - mean;
14        let b = values[i] - mean;
15        if a <= 0.0 && b > 0.0 {
16            let denom = b - a;
17            let frac = if denom.abs() > f64::MIN_POSITIVE {
18                -a / denom
19            } else {
20                0.0
21            };
22            crossings.push(times[i - 1] + frac * (times[i] - times[i - 1]));
23        }
24    }
25    if crossings.len() >= 2 {
26        let total: f64 = crossings.windows(2).map(|w| w[1] - w[0]).sum();
27        total / (crossings.len() - 1) as f64
28    } else {
29        0.0
30    }
31}
32
33impl PhysicsSimulationLibrary {
34    /// ParticlePhysics — 2D projectile / ballistic motion with optional quadratic drag.
35    ///
36    /// State `[x, y, vx, vy]`; `dvx = -k·|v|·vx`, `dvy = -g - k·|v|·vy` where `k = drag`
37    /// (drag per unit mass). Integrated by `integrate_dopri5`. With `drag = 0` the range
38    /// recovers the analytic `v0²·sin(2θ)/g`.
39    pub fn run_projectile_motion(
40        &self,
41        v0: f64,
42        angle_rad: f64,
43        g: f64,
44        drag: f64,
45        num_samples: usize,
46        max_time: f64,
47    ) -> Result<ProjectileResult, PhysicsError> {
48        if !(v0.is_finite() && angle_rad.is_finite() && g > 0.0) {
49            return Err(PhysicsError::InvalidConfiguration(
50                "require finite v0, angle and g > 0".to_string(),
51            ));
52        }
53        let state = vec![0.0, 0.0, v0 * angle_rad.cos(), v0 * angle_rad.sin()];
54        let deriv = move |_t: f64, y: &[f64], dy: &mut [f64]| -> Result<(), OdeError> {
55            let (vx, vy) = (y[2], y[3]);
56            let speed = (vx * vx + vy * vy).sqrt();
57            dy[0] = vx;
58            dy[1] = vy;
59            dy[2] = -drag * speed * vx;
60            dy[3] = -g - drag * speed * vy;
61            Ok(())
62        };
63        let (_final_state, snapshots, accepted, rejected) =
64            self.integrate_ode_samples(state, max_time, num_samples, deriv)?;
65
66        // Sample times are uniform across [0, max_time].
67        let n = snapshots.len();
68        let mut trajectory: Vec<[f64; 5]> = Vec::with_capacity(n);
69        for (k, s) in snapshots.iter().enumerate() {
70            let t = max_time * k as f64 / (n - 1).max(1) as f64;
71            trajectory.push([t, s[0], s[1], s[2], s[3]]);
72        }
73        let max_height = trajectory.iter().map(|r| r[2]).fold(f64::MIN, f64::max);
74
75        // Landing: first downward crossing of y = 0 after launch (skip the launch point).
76        let mut landed = false;
77        let mut range = trajectory.last().map(|r| r[1]).unwrap_or(0.0);
78        let mut time_of_flight = max_time;
79        for w in trajectory.windows(2) {
80            let (a, b) = (w[0], w[1]);
81            if a[0] > 0.0 && a[2] >= 0.0 && b[2] < 0.0 {
82                let frac = a[2] / (a[2] - b[2]); // linear interp to y=0
83                range = a[1] + frac * (b[1] - a[1]);
84                time_of_flight = a[0] + frac * (b[0] - a[0]);
85                landed = true;
86                break;
87            }
88        }
89        Ok(ProjectileResult {
90            trajectory,
91            range,
92            max_height,
93            time_of_flight,
94            landed,
95            steps_accepted: accepted,
96            steps_rejected: rejected,
97        })
98    }
99    /// StructuralDynamics — 1D spring–mass harmonic oscillator, integrated by the
100    /// symplectic `integrate_symplectic` (Störmer–Verlet). Hamiltonian
101    /// `H = p²/(2m) + ½k·q²`, so `dq/dt = p/m`, `dp/dt = -k·q`. Reports both the analytic
102    /// period `2π√(m/k)` and the one measured from the integrated trajectory, plus the
103    /// bounded energy drift that is the hallmark of a symplectic integrator.
104    pub fn run_harmonic_oscillator(
105        &self,
106        mass: f64,
107        k_spring: f64,
108        x0: f64,
109        v0: f64,
110        total_time: f64,
111        num_samples: usize,
112    ) -> Result<OscillatorResult, PhysicsError> {
113        if !(mass > 0.0 && k_spring > 0.0 && total_time > 0.0) {
114            return Err(PhysicsError::InvalidConfiguration(
115                "require mass > 0, k_spring > 0, total_time > 0".to_string(),
116            ));
117        }
118        let num_samples = num_samples.max(1);
119        let omega = (k_spring / mass).sqrt();
120        let analytic_period = 2.0 * std::f64::consts::PI / omega;
121        // Micro-step fine enough to resolve the period (~400 steps/period).
122        let h_target = analytic_period / 400.0;
123
124        let force = move |q: f64| -k_spring * q; // dp/dt
125        let kinetic_velocity = move |p: f64| p / mass; // dq/dt
126        let hamiltonian = move |q: f64, p: f64| 0.5 * p * p / mass + 0.5 * k_spring * q * q;
127
128        let mut q = x0;
129        let mut p = mass * v0;
130        let energy_initial = hamiltonian(q, p);
131        let mut times: Vec<f64> = Vec::with_capacity(num_samples + 1);
132        let mut positions: Vec<f64> = Vec::with_capacity(num_samples + 1);
133        let mut velocities: Vec<f64> = Vec::with_capacity(num_samples + 1);
134        times.push(0.0);
135        positions.push(q);
136        velocities.push(p / mass);
137
138        let dt_sample = total_time / num_samples as f64;
139        let mut max_drift = 0.0f64;
140        for i in 0..num_samples {
141            let steps = (dt_sample / h_target).ceil().max(1.0) as u64;
142            let h = dt_sample / steps as f64;
143            let res = integrate_symplectic(
144                q,
145                p,
146                h,
147                steps,
148                &force,
149                &kinetic_velocity,
150                &hamiltonian,
151                SymplecticMethod::Yoshida4,
152            );
153            q = res.q;
154            p = res.p;
155            if res.max_energy_drift > max_drift {
156                max_drift = res.max_energy_drift;
157            }
158            times.push((i + 1) as f64 * dt_sample);
159            positions.push(q);
160            velocities.push(p / mass);
161        }
162        let energy_final = hamiltonian(q, p);
163        let measured_period = estimate_period_from_crossings(&times, &positions);
164        Ok(OscillatorResult {
165            times,
166            positions,
167            velocities,
168            analytic_period,
169            measured_period,
170            energy_initial,
171            energy_final,
172            max_energy_drift: max_drift,
173        })
174    }
175    /// Nonlinear rigid-body dynamics — a simple gravity pendulum (point mass on a rigid
176    /// rod). State `[θ, ω]`; `dθ/dt = ω`, `dω/dt = -(g/L)·sin θ`. Integrated by
177    /// `integrate_dopri5`. Energy `E = ½L²ω² + gL(1−cos θ)` (unit mass) is conserved; the
178    /// small-angle period is `2π√(L/g)`.
179    pub fn run_pendulum(
180        &self,
181        length: f64,
182        g: f64,
183        theta0: f64,
184        omega0: f64,
185        total_time: f64,
186        num_samples: usize,
187    ) -> Result<PendulumResult, PhysicsError> {
188        if !(length > 0.0 && g > 0.0 && total_time > 0.0) {
189            return Err(PhysicsError::InvalidConfiguration(
190                "require length > 0, g > 0, total_time > 0".to_string(),
191            ));
192        }
193        let l = length;
194        let energy =
195            move |theta: f64, omega: f64| 0.5 * l * l * omega * omega + g * l * (1.0 - theta.cos());
196        let energy_initial = energy(theta0, omega0);
197        let state = vec![theta0, omega0];
198        let deriv = move |_t: f64, y: &[f64], dy: &mut [f64]| -> Result<(), OdeError> {
199            dy[0] = y[1];
200            dy[1] = -(g / l) * y[0].sin();
201            Ok(())
202        };
203        let (final_state, snapshots, accepted, rejected) =
204            self.integrate_ode_samples(state, total_time, num_samples, deriv)?;
205        let n = snapshots.len();
206        let times: Vec<f64> = (0..n)
207            .map(|k| total_time * k as f64 / (n - 1).max(1) as f64)
208            .collect();
209        let angles: Vec<f64> = snapshots.iter().map(|s| s[0]).collect();
210        let angular_velocities: Vec<f64> = snapshots.iter().map(|s| s[1]).collect();
211        let energy_final = energy(final_state[0], final_state[1]);
212        let measured_period = estimate_period_from_crossings(&times, &angles);
213        Ok(PendulumResult {
214            times,
215            angles,
216            angular_velocities,
217            small_angle_period: 2.0 * std::f64::consts::PI * (l / g).sqrt(),
218            measured_period,
219            energy_initial,
220            energy_final,
221            steps_accepted: accepted,
222            steps_rejected: rejected,
223        })
224    }
225}