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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! 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);
}
/// p99 estimators: for a known distribution the 99th percentile is bracketed
/// by the sample values (DDSketch relative error), and it is monotone —
/// feeding a larger tail raises it, feeding only small values keeps it low.
#[test]
fn step_time_and_contacts_p99_track_the_upper_tail() {
let mut tel = PhysicsTelemetry::new();
// 990 steps at 1.0 ms and 10 at 50 ms: p99 sits at the boundary of the
// 50 ms tail (between 1 and 50, and >= the p50 of 1 ms)
for _ in 0..990 {
tel.record_step_time(1000.0);
tel.record_contacts(10.0);
}
for _ in 0..10 {
tel.record_step_time(50_000.0);
tel.record_contacts(400.0);
}
let st99 = tel.step_time_p99();
let ct99 = tel.contacts_p99();
assert!(
st99 >= tel.step_time_p50(),
"p99 {st99} below p50 {}",
tel.step_time_p50()
);
assert!(
(1000.0 * 0.95..=50_000.0 * 1.05).contains(&st99), // DDSketch relative error
"step p99 {st99} outside [1 ms, 50 ms]"
);
assert!(
ct99 >= tel.contacts_p50() * 0.95,
"contacts p99 {ct99} below p50"
);
assert!(
(10.0 * 0.95..=400.0 * 1.05).contains(&ct99),
"contacts p99 {ct99} outside [10, 400]"
);
// a uniform stream: p99 ≈ the (only) value within sketch error
let mut flat = PhysicsTelemetry::new();
for _ in 0..500 {
flat.record_step_time(2000.0);
flat.record_contacts(3.0);
}
assert!(
(flat.step_time_p99() - 2000.0).abs() < 2000.0 * 0.05,
"{}",
flat.step_time_p99()
);
assert!(
(flat.contacts_p99() - 3.0).abs() < 3.0 * 0.05,
"{}",
flat.contacts_p99()
);
// ordering: heavier tail → larger p99
assert!(tel.step_time_p99() > flat.step_time_p99());
assert!(tel.contacts_p99() > flat.contacts_p99());
}
}