1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! ALICE-Analytics bridge: Physics simulation metrics
//!
//! Feeds per-step simulation metrics (step time, collision count,
//! energy drift) into ALICE-Analytics sketches for profiling and
//! anomaly detection.
use alice_analytics::sketch::{DDSketch256, HyperLogLog12};
/// Physics telemetry collector backed by ALICE-Analytics sketches.
pub struct PhysicsTelemetry {
/// Step execution time in microseconds (quantile estimation).
step_time: DDSketch256,
/// Collision / contact count per step.
contacts: DDSketch256,
/// Energy drift per step (|E_n - E_{n-1}|).
energy_drift: DDSketch256,
/// Unique body pair collisions (cardinality estimation).
collision_pairs: HyperLogLog12,
/// Total simulation steps.
total_steps: u64,
}
impl PhysicsTelemetry {
/// Create a new physics telemetry collector.
pub fn new() -> Self {
Self {
step_time: DDSketch256::new(0.01),
contacts: DDSketch256::new(0.01),
energy_drift: DDSketch256::new(0.01),
collision_pairs: HyperLogLog12::new(),
total_steps: 0,
}
}
/// Record a simulation step's timing.
pub fn record_step_time(&mut self, time_us: f64) {
self.step_time.insert(time_us);
self.total_steps += 1;
}
/// Record contact count for a step.
pub fn record_contacts(&mut self, count: f64) {
self.contacts.insert(count);
}
/// Record energy drift between consecutive steps.
pub fn record_energy_drift(&mut self, drift: f64) {
self.energy_drift.insert(drift.abs());
}
/// Record a collision pair observation (for unique pair counting).
///
/// `pair_hash` should be a deterministic hash of the two body IDs
/// (e.g. `min(a,b) << 32 | max(a,b)`).
pub fn record_collision_pair(&mut self, pair_hash: u64) {
self.collision_pairs.insert_hash(pair_hash);
}
/// Estimated p50 step time.
pub fn step_time_p50(&self) -> f64 {
self.step_time.quantile(0.5)
}
/// Estimated p99 step time.
pub fn step_time_p99(&self) -> f64 {
self.step_time.quantile(0.99)
}
/// Estimated p50 contact count.
pub fn contacts_p50(&self) -> f64 {
self.contacts.quantile(0.5)
}
/// Estimated p99 contact count.
pub fn contacts_p99(&self) -> f64 {
self.contacts.quantile(0.99)
}
/// Estimated p99 energy drift (should stay near zero for stable sims).
pub fn energy_drift_p99(&self) -> f64 {
self.energy_drift.quantile(0.99)
}
/// Estimated unique collision pairs observed.
pub fn unique_collision_pairs(&self) -> f64 {
self.collision_pairs.cardinality()
}
/// Total simulation steps recorded.
pub fn total_steps(&self) -> u64 {
self.total_steps
}
}
impl Default for PhysicsTelemetry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_physics_telemetry() {
let mut tel = PhysicsTelemetry::new();
for step in 0u64..200 {
tel.record_step_time(100.0 + (step % 20) as f64);
tel.record_contacts((step % 10) as f64);
tel.record_energy_drift(0.001 * step as f64);
let pair = (step % 5) << 32 | ((step + 1) % 5);
tel.record_collision_pair(pair);
}
assert_eq!(tel.total_steps(), 200);
assert!(tel.step_time_p50() > 0.0);
assert!(tel.contacts_p50() >= 0.0);
assert!(tel.energy_drift_p99() >= 0.0);
assert!(tel.unique_collision_pairs() >= 1.0);
}
}