qualia_core_db/specialized_libs/physics_simulation/
mechanics.rs1use super::*;
2
3fn 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 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 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 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]); 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 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 let h_target = analytic_period / 400.0;
123
124 let force = move |q: f64| -k_spring * q; let kinetic_velocity = move |p: f64| p / mass; 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(×, &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 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(×, &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}