bloqade_lanes_bytecode_core/arch/
metrics.rs1pub const FLAIR_MAX_RAMP_US: f64 = 0.2;
20pub const FLAIR_MAX_JERK_UM_PER_US3: f64 = 0.0004;
22pub const FLAIR_MAX_ACCEL_UM_PER_US2: f64 = 0.0015;
24
25const MIN_MOVE_DISTANCE_UM: f64 = 1e-8;
27
28#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct MotionModel {
35 pub max_ramp_us: f64,
37 pub max_jerk_um_per_us3: f64,
39 pub max_accel_um_per_us2: f64,
41}
42
43impl Default for MotionModel {
44 fn default() -> Self {
46 Self {
47 max_ramp_us: FLAIR_MAX_RAMP_US,
48 max_jerk_um_per_us3: FLAIR_MAX_JERK_UM_PER_US3,
49 max_accel_um_per_us2: FLAIR_MAX_ACCEL_UM_PER_US2,
50 }
51 }
52}
53
54impl MotionModel {
55 pub fn new(
63 max_ramp_us: f64,
64 max_jerk_um_per_us3: f64,
65 max_accel_um_per_us2: f64,
66 ) -> Option<Self> {
67 if !max_ramp_us.is_finite()
68 || !max_jerk_um_per_us3.is_finite()
69 || !max_accel_um_per_us2.is_finite()
70 || max_ramp_us <= 0.0
71 || max_jerk_um_per_us3 <= 0.0
72 || max_accel_um_per_us2 <= 0.0
73 {
74 return None;
75 }
76 Some(Self {
77 max_ramp_us,
78 max_jerk_um_per_us3,
79 max_accel_um_per_us2,
80 })
81 }
82
83 pub fn flair() -> Self {
85 Self::default()
86 }
87
88 pub fn const_jerk_min_duration_us(&self, max_dist_um: f64) -> f64 {
97 let max_dist_um = max_dist_um.abs();
98 if max_dist_um < MIN_MOVE_DISTANCE_UM {
99 return 0.0;
100 }
101
102 let t1 = self.max_accel_um_per_us2 / self.max_jerk_um_per_us3;
103 let a = self.max_jerk_um_per_us3 * t1;
104 let b = 3.0 * self.max_jerk_um_per_us3 * t1 * t1;
105 let c = 2.0 * self.max_jerk_um_per_us3 * t1 * t1 * t1 - max_dist_um;
106
107 if c >= 0.0 {
108 let t1_jerk = (max_dist_um / (2.0 * self.max_jerk_um_per_us3)).cbrt();
109 return 4.0 * t1_jerk;
110 }
111
112 let discriminant = b * b - 4.0 * a * c;
113 let t2 = (-b + discriminant.sqrt()) / (2.0 * a);
114 4.0 * t1 + 2.0 * t2
115 }
116
117 pub fn lane_duration_us(&self, waypoints: &[[f64; 2]], amplitude_delta: f64) -> f64 {
125 let ramp = amplitude_delta.abs() / self.max_ramp_us;
126 let segment_sum: f64 = waypoints
127 .windows(2)
128 .map(|w| {
129 let dx = w[1][0] - w[0][0];
130 let dy = w[1][1] - w[0][1];
131 self.const_jerk_min_duration_us((dx * dx + dy * dy).sqrt())
132 })
133 .sum();
134 ramp + segment_sum + ramp
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn default_matches_flair_constants() {
144 let m = MotionModel::default();
145 assert_eq!(m.max_ramp_us, FLAIR_MAX_RAMP_US);
146 assert_eq!(m.max_jerk_um_per_us3, FLAIR_MAX_JERK_UM_PER_US3);
147 assert_eq!(m.max_accel_um_per_us2, FLAIR_MAX_ACCEL_UM_PER_US2);
148 assert_eq!(m, MotionModel::flair());
149 }
150
151 #[test]
152 fn zero_and_tiny_distance_is_zero_duration() {
153 let m = MotionModel::default();
154 assert_eq!(m.const_jerk_min_duration_us(0.0), 0.0);
155 assert_eq!(m.const_jerk_min_duration_us(1e-12), 0.0);
156 }
157
158 #[test]
159 fn duration_is_even_in_sign_and_increasing() {
160 let m = MotionModel::default();
161 let d10 = m.const_jerk_min_duration_us(10.0);
162 assert_eq!(d10, m.const_jerk_min_duration_us(-10.0));
163 assert!(d10 > 0.0);
164 assert!(m.const_jerk_min_duration_us(50.0) > d10);
165 }
166
167 #[test]
168 fn both_trajectory_branches_are_exercised() {
169 let m = MotionModel::default();
170 let small = m.const_jerk_min_duration_us(0.01);
175 let large = m.const_jerk_min_duration_us(100.0);
176 assert!(small.is_finite() && small > 0.0);
177 assert!(large.is_finite() && large > small);
178 }
179
180 #[test]
181 fn lane_duration_is_ramp_plus_segments_plus_ramp() {
182 let m = MotionModel::default();
183 let waypoints = [[0.0, 0.0], [3.0, 4.0]]; let ramp = 1.0 / m.max_ramp_us;
185 let expected = ramp + m.const_jerk_min_duration_us(5.0) + ramp;
186 assert_eq!(m.lane_duration_us(&waypoints, 1.0), expected);
187 }
188
189 #[test]
190 fn lane_duration_of_pathless_move_is_two_ramps() {
191 let m = MotionModel::default();
192 let two_ramps = 2.0 * (1.0 / m.max_ramp_us);
194 assert_eq!(m.lane_duration_us(&[], 1.0), two_ramps);
195 assert_eq!(m.lane_duration_us(&[[1.0, 2.0]], 1.0), two_ramps);
196 }
197
198 #[test]
199 fn amplitude_scales_ramp_only() {
200 let m = MotionModel::default();
201 let waypoints = [[0.0, 0.0], [3.0, 4.0]];
202 let seg = m.const_jerk_min_duration_us(5.0);
203 assert_eq!(
205 m.lane_duration_us(&waypoints, 2.0),
206 2.0 * (2.0 / m.max_ramp_us) + seg
207 );
208 assert_eq!(
210 m.lane_duration_us(&waypoints, -1.0),
211 m.lane_duration_us(&waypoints, 1.0)
212 );
213 }
214
215 #[test]
216 fn new_rejects_non_physical_constants() {
217 assert!(MotionModel::new(0.0, 1.0, 1.0).is_none());
218 assert!(MotionModel::new(1.0, 0.0, 1.0).is_none());
219 assert!(MotionModel::new(1.0, 1.0, 0.0).is_none());
220 assert!(MotionModel::new(1.0, 1.0, -1.0).is_none());
221 assert!(MotionModel::new(-1.0, 1.0, 1.0).is_none());
222 assert!(MotionModel::new(f64::NAN, 1.0, 1.0).is_none());
223 assert!(MotionModel::new(0.2, 0.0004, 0.0015).is_some());
224 }
225}