Skip to main content

qualia_core_db/specialized_libs/physics_simulation/
population.rs

1use super::*;
2
3impl PhysicsSimulationLibrary {
4    /// Biophysics — logistic population dynamics `dN/dt = r·N·(1 − N/K)`, integrated by
5    /// `integrate_dopri5`. Matches the analytic logistic curve
6    /// `N(t) = K / (1 + ((K−N₀)/N₀)·e^{−r·t})`.
7    pub fn run_logistic_growth(
8        &self,
9        n0: f64,
10        growth_rate: f64,
11        carrying_capacity: f64,
12        total_time: f64,
13        num_samples: usize,
14    ) -> Result<PopulationDynamicsResult, PhysicsError> {
15        if !(n0 >= 0.0 && carrying_capacity > 0.0 && total_time > 0.0) {
16            return Err(PhysicsError::InvalidConfiguration(
17                "require n0 >= 0, carrying_capacity > 0, total_time > 0".to_string(),
18            ));
19        }
20        let r = growth_rate;
21        let k = carrying_capacity;
22        let deriv = move |_t: f64, y: &[f64], dy: &mut [f64]| -> Result<(), OdeError> {
23            dy[0] = r * y[0] * (1.0 - y[0] / k);
24            Ok(())
25        };
26        let (_final, snapshots, accepted, rejected) =
27            self.integrate_ode_samples(vec![n0], total_time, num_samples, deriv)?;
28        let n_pts = snapshots.len();
29        let times: Vec<f64> = (0..n_pts)
30            .map(|kk| total_time * kk as f64 / (n_pts - 1).max(1) as f64)
31            .collect();
32        let population: Vec<f64> = snapshots.iter().map(|s| s[0]).collect();
33        Ok(PopulationDynamicsResult {
34            times,
35            population,
36            carrying_capacity: k,
37            growth_rate: r,
38            steps_accepted: accepted,
39            steps_rejected: rejected,
40        })
41    }
42}