1#[derive(Debug, Clone, PartialEq)]
12pub struct BeamSpline {
13 x_low: f64,
14 x_high: f64,
15 coefficients: Vec<f64>,
16}
17
18impl BeamSpline {
19 pub(crate) fn constant(u_low: f64, u_high: f64, per_us: f64) -> Self {
20 Self {
21 x_low: u_low.ln(),
22 x_high: u_high.ln(),
23 coefficients: vec![per_us.ln(); 4],
24 }
25 }
26
27 pub(crate) fn with_coefficients(&self, coefficients: &[f64]) -> Self {
28 Self {
29 coefficients: coefficients.to_vec(),
30 ..self.clone()
31 }
32 }
33
34 pub(crate) fn refined(&self) -> Self {
35 let c = &self.coefficients;
36 let coefficients = (0..2 * c.len() - 3)
37 .map(|p| {
38 let i = p / 2;
39 if p % 2 == 0 {
40 0.5 * (c[i] + c[i + 1])
41 } else {
42 (c[i] + 6.0 * c[i + 1] + c[i + 2]) / 8.0
43 }
44 })
45 .collect();
46 Self {
47 coefficients,
48 ..self.clone()
49 }
50 }
51
52 #[must_use]
54 pub fn intervals(&self) -> usize {
55 self.coefficients.len() - 3
56 }
57
58 #[must_use]
60 pub fn coefficients(&self) -> &[f64] {
61 &self.coefficients
62 }
63
64 #[must_use]
66 pub fn knot_span_us(&self) -> (f64, f64) {
67 (self.x_low.exp(), self.x_high.exp())
68 }
69
70 #[must_use]
73 pub fn basis(&self, u_us: f64) -> [(usize, f64); 5] {
74 let n = self.intervals();
75 let h = (self.x_high - self.x_low) / n as f64;
76 let s = (u_us.ln() - self.x_low) / h;
77 if s < 0.0 {
78 let mean_curvature = s * s / (4.0 * n as f64);
79 return [
80 (0, 1.0 / 6.0 - s / 2.0 + mean_curvature),
81 (1, 2.0 / 3.0),
82 (2, 1.0 / 6.0 + s / 2.0 - mean_curvature),
83 (n, -mean_curvature),
84 (n + 2, mean_curvature),
85 ];
86 }
87 let first = (s.floor() as usize).min(n - 1);
88 let t = s - first as f64;
89 [
90 (first, (1.0 - t).powi(3) / 6.0),
91 (first + 1, (3.0 * t.powi(3) - 6.0 * t * t + 4.0) / 6.0),
92 (
93 first + 2,
94 (-3.0 * t.powi(3) + 3.0 * t * t + 3.0 * t + 1.0) / 6.0,
95 ),
96 (first + 3, t.powi(3) / 6.0),
97 (first, 0.0),
98 ]
99 }
100
101 #[must_use]
104 pub fn basis_slope(&self, u_us: f64) -> [(usize, f64); 5] {
105 let n = self.intervals();
106 let h = (self.x_high - self.x_low) / n as f64;
107 let ds_du = 1.0 / (h * u_us);
108 let s = (u_us.ln() - self.x_low) / h;
109 if s < 0.0 {
110 let curvature_slope = s / (2.0 * n as f64);
111 return [
112 (0, (-0.5 + curvature_slope) * ds_du),
113 (1, 0.0),
114 (2, (0.5 - curvature_slope) * ds_du),
115 (n, -curvature_slope * ds_du),
116 (n + 2, curvature_slope * ds_du),
117 ];
118 }
119 let first = (s.floor() as usize).min(n - 1);
120 let t = s - first as f64;
121 [
122 (first, -(1.0 - t).powi(2) / 2.0 * ds_du),
123 (first + 1, (3.0 * t * t - 4.0 * t) / 2.0 * ds_du),
124 (first + 2, (-3.0 * t * t + 2.0 * t + 1.0) / 2.0 * ds_du),
125 (first + 3, t * t / 2.0 * ds_du),
126 (first, 0.0),
127 ]
128 }
129
130 #[must_use]
132 pub fn per_us(&self, u_us: f64) -> f64 {
133 self.basis(u_us)
134 .iter()
135 .map(|&(i, w)| w * self.coefficients[i])
136 .sum::<f64>()
137 .exp()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn wavy() -> BeamSpline {
146 let spline = BeamSpline::constant(280.0, 470.0, 1.0).refined().refined();
147 let c: Vec<f64> = (0..spline.coefficients().len())
148 .map(|i| (i as f64 * 0.7).sin())
149 .collect();
150 spline.with_coefficients(&c)
151 }
152
153 fn ln_beam(spline: &BeamSpline, u: f64) -> f64 {
154 spline.per_us(u).ln()
155 }
156
157 #[test]
158 fn the_basis_slope_is_the_derivative_of_ln_beam() {
159 let spline = wavy();
160 for u in [250.0, 279.0, 300.0, 333.3, 400.0, 469.0, 500.0] {
161 let h = 1e-4;
162 let central = (ln_beam(&spline, u + h) - ln_beam(&spline, u - h)) / (2.0 * h);
163 let slope: f64 = spline
164 .basis_slope(u)
165 .iter()
166 .map(|&(i, w)| w * spline.coefficients()[i])
167 .sum();
168 assert!(
169 (slope - central).abs() <= 1e-8 * central.abs().max(1e-3),
170 "{u}: {slope} vs {central}"
171 );
172 }
173 }
174
175 #[test]
176 fn the_coefficients_of_a_cubic_in_ln_u_reproduce_it() {
177 let (u_low, u_high) = (280.0, 470.0);
178 let [a, b, c, d] = [3.0, -0.7, 1.9, -2.3];
179 let coefficients = [-1.0, 0.0, 1.0, 2.0]
180 .map(|k: f64| a + b * k + c * (k * k - 1.0 / 3.0) + d * (k.powi(3) - k));
181 let beam = BeamSpline::constant(u_low, u_high, 1.0).with_coefficients(&coefficients);
182 for step in 0..=40 {
183 let u = u_low * (u_high / u_low).powf(f64::from(step) / 40.0);
184 let t = (u / u_low).ln() / (u_high / u_low).ln();
185 let expected = a + b * t + c * t * t + d * t.powi(3);
186 assert!((ln_beam(&beam, u) - expected).abs() < 1e-12, "{u}");
187 }
188 }
189
190 #[test]
191 fn refining_keeps_the_curve_inside_and_below_the_knots() {
192 let spline = wavy();
193 let refined = spline.refined();
194 assert_eq!(refined.intervals(), 2 * spline.intervals());
195 for k in 0..400 {
196 let u = 150.0 * (600.0_f64 / 150.0).powf(f64::from(k) / 399.0);
197 let (a, b) = (ln_beam(&spline, u), ln_beam(&refined, u));
198 assert!((a - b).abs() < 1e-12, "at {u} µs: {a} became {b}");
199 }
200 }
201
202 fn slope_at_first_node(f: &dyn Fn(f64) -> f64, nodes: [f64; 4]) -> f64 {
203 let mut d = nodes.map(f);
204 for k in 1..4 {
205 for i in (k..4).rev() {
206 d[i] = (d[i] - d[i - 1]) / (nodes[i] - nodes[i - k]);
207 }
208 }
209 d[1] + (nodes[0] - nodes[1]) * (d[2] + d[3] * (nodes[0] - nodes[2]))
210 }
211
212 #[test]
213 fn below_the_knots_the_curve_continues_with_the_mean_curvature() {
214 let (x_low, x_high) = (280.0_f64.ln(), 470.0_f64.ln());
215 let mut spline = BeamSpline::constant(280.0, 470.0, 1.0);
216 for _ in 0..3 {
217 let c: Vec<f64> = (0..spline.coefficients().len())
218 .map(|i| (i as f64 * 0.7).sin())
219 .collect();
220 let wavy = spline.with_coefficients(&c);
221 let f = |x: f64| ln_beam(&wavy, x.exp());
222 let h = (x_high - x_low) / wavy.intervals() as f64;
223 let within = |end: f64, toward: f64| {
224 [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0].map(|k| end + toward * k * h)
225 };
226 let slope_low = slope_at_first_node(&f, within(x_low, 1.0));
227 let slope_high = slope_at_first_node(&f, within(x_high, -1.0));
228 let curvature = (slope_high - slope_low) / (x_high - x_low);
229 for dx in [-0.05, -0.2, -0.4] {
230 let expected = f(x_low) + slope_low * dx + curvature * dx * dx / 2.0;
231 assert!(
232 (f(x_low + dx) - expected).abs() < 1e-9 * (1.0 + expected.abs()),
233 "{} intervals, {dx}: {} vs {expected}",
234 wavy.intervals(),
235 f(x_low + dx)
236 );
237 }
238 spline = spline.refined();
239 }
240 }
241}