Skip to main content

oxiphysics_python/world_api/
stats.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Simulation statistics.
5
6#![allow(missing_docs)]
7
8use super::PyPhysicsWorld;
9
10// ===========================================================================
11// Simulation Statistics
12// ===========================================================================
13
14/// Per-step simulation performance and state statistics.
15///
16/// Retrieved via `PyPhysicsWorld::stats()` after each `step()` call.
17#[derive(Debug, Clone, Default)]
18#[allow(dead_code)]
19pub struct SimStats {
20    /// Total number of active (non-removed) bodies.
21    pub body_count: usize,
22    /// Number of bodies currently sleeping.
23    pub sleeping_count: usize,
24    /// Number of bodies that are awake (body_count - sleeping_count).
25    pub awake_count: usize,
26    /// Number of contacts detected in the most recent step.
27    pub contact_count: usize,
28    /// Accumulated simulation time (seconds).
29    pub simulation_time: f64,
30    /// Total kinetic energy summed over all dynamic bodies (½mv²).
31    pub total_kinetic_energy: f64,
32    /// Largest linear speed among all dynamic bodies.
33    pub max_linear_speed: f64,
34    /// Largest angular speed among all dynamic bodies.
35    pub max_angular_speed: f64,
36}
37
38impl PyPhysicsWorld {
39    /// Return simulation statistics for the current state.
40    pub fn stats(&self) -> SimStats {
41        let mut s = SimStats {
42            body_count: self.body_count(),
43            sleeping_count: self.sleeping_count(),
44            contact_count: self.contacts.len(),
45            simulation_time: self.time,
46            ..SimStats::default()
47        };
48        s.awake_count = s.body_count.saturating_sub(s.sleeping_count);
49
50        for slot in &self.slots {
51            if let Some(body) = slot.body.as_ref() {
52                if body.is_static || body.is_kinematic {
53                    continue;
54                }
55                let v2 =
56                    body.velocity[0].powi(2) + body.velocity[1].powi(2) + body.velocity[2].powi(2);
57                let speed = v2.sqrt();
58                s.total_kinetic_energy += 0.5 * body.mass * v2;
59                if speed > s.max_linear_speed {
60                    s.max_linear_speed = speed;
61                }
62                let w = (body.angular_velocity[0].powi(2)
63                    + body.angular_velocity[1].powi(2)
64                    + body.angular_velocity[2].powi(2))
65                .sqrt();
66                if w > s.max_angular_speed {
67                    s.max_angular_speed = w;
68                }
69            }
70        }
71        s
72    }
73
74    /// Return the kinetic energy of a single body, or `None` if not found.
75    pub fn body_kinetic_energy(&self, handle: u32) -> Option<f64> {
76        let body = self.get_body(handle)?;
77        let v2 = body.velocity[0].powi(2) + body.velocity[1].powi(2) + body.velocity[2].powi(2);
78        Some(0.5 * body.mass * v2)
79    }
80
81    /// Return the closest active body handle to `point`, or `None` if empty.
82    pub fn closest_body(&self, point: [f64; 3]) -> Option<(u32, f64)> {
83        let mut best_handle: Option<u32> = None;
84        let mut best_dist = f64::MAX;
85        for (i, slot) in self.slots.iter().enumerate() {
86            if let Some(body) = slot.body.as_ref() {
87                let dx = body.position[0] - point[0];
88                let dy = body.position[1] - point[1];
89                let dz = body.position[2] - point[2];
90                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
91                if dist < best_dist {
92                    best_dist = dist;
93                    best_handle = Some(i as u32);
94                }
95            }
96        }
97        best_handle.map(|h| (h, best_dist))
98    }
99
100    /// Return handles of all bodies within `radius` of `center`.
101    pub fn bodies_in_sphere(&self, center: [f64; 3], radius: f64) -> Vec<u32> {
102        let r2 = radius * radius;
103        self.slots
104            .iter()
105            .enumerate()
106            .filter_map(|(i, slot)| {
107                let body = slot.body.as_ref()?;
108                let dx = body.position[0] - center[0];
109                let dy = body.position[1] - center[1];
110                let dz = body.position[2] - center[2];
111                if dx * dx + dy * dy + dz * dz <= r2 {
112                    Some(i as u32)
113                } else {
114                    None
115                }
116            })
117            .collect()
118    }
119
120    /// Find the pair of bodies with the smallest center-to-center distance.
121    ///
122    /// Returns `None` if fewer than 2 bodies are present.
123    pub fn closest_pair(&self) -> Option<(u32, u32, f64)> {
124        let active: Vec<(u32, [f64; 3])> = self
125            .slots
126            .iter()
127            .enumerate()
128            .filter_map(|(i, s)| s.body.as_ref().map(|b| (i as u32, b.position)))
129            .collect();
130        if active.len() < 2 {
131            return None;
132        }
133        let mut best = (0u32, 0u32, f64::MAX);
134        for i in 0..active.len() {
135            for j in (i + 1)..active.len() {
136                let (h_a, p_a) = active[i];
137                let (h_b, p_b) = active[j];
138                let dx = p_a[0] - p_b[0];
139                let dy = p_a[1] - p_b[1];
140                let dz = p_a[2] - p_b[2];
141                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
142                if dist < best.2 {
143                    best = (h_a, h_b, dist);
144                }
145            }
146        }
147        Some(best)
148    }
149}