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]
103 pub fn per_us(&self, u_us: f64) -> f64 {
104 self.basis(u_us)
105 .iter()
106 .map(|&(i, w)| w * self.coefficients[i])
107 .sum::<f64>()
108 .exp()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn wavy() -> BeamSpline {
117 let spline = BeamSpline::constant(280.0, 470.0, 1.0).refined().refined();
118 let c: Vec<f64> = (0..spline.coefficients().len())
119 .map(|i| (i as f64 * 0.7).sin())
120 .collect();
121 spline.with_coefficients(&c)
122 }
123
124 fn ln_beam(spline: &BeamSpline, u: f64) -> f64 {
125 spline.per_us(u).ln()
126 }
127
128 #[test]
129 fn the_coefficients_of_a_cubic_in_ln_u_reproduce_it() {
130 let (u_low, u_high) = (280.0, 470.0);
131 let [a, b, c, d] = [3.0, -0.7, 1.9, -2.3];
132 let coefficients = [-1.0, 0.0, 1.0, 2.0]
133 .map(|k: f64| a + b * k + c * (k * k - 1.0 / 3.0) + d * (k.powi(3) - k));
134 let beam = BeamSpline::constant(u_low, u_high, 1.0).with_coefficients(&coefficients);
135 for step in 0..=40 {
136 let u = u_low * (u_high / u_low).powf(f64::from(step) / 40.0);
137 let t = (u / u_low).ln() / (u_high / u_low).ln();
138 let expected = a + b * t + c * t * t + d * t.powi(3);
139 assert!((ln_beam(&beam, u) - expected).abs() < 1e-12, "{u}");
140 }
141 }
142
143 #[test]
144 fn refining_keeps_the_curve_inside_and_below_the_knots() {
145 let spline = wavy();
146 let refined = spline.refined();
147 assert_eq!(refined.intervals(), 2 * spline.intervals());
148 for k in 0..400 {
149 let u = 150.0 * (600.0_f64 / 150.0).powf(f64::from(k) / 399.0);
150 let (a, b) = (ln_beam(&spline, u), ln_beam(&refined, u));
151 assert!((a - b).abs() < 1e-12, "at {u} µs: {a} became {b}");
152 }
153 }
154
155 fn slope_at_first_node(f: &dyn Fn(f64) -> f64, nodes: [f64; 4]) -> f64 {
156 let mut d = nodes.map(f);
157 for k in 1..4 {
158 for i in (k..4).rev() {
159 d[i] = (d[i] - d[i - 1]) / (nodes[i] - nodes[i - k]);
160 }
161 }
162 d[1] + (nodes[0] - nodes[1]) * (d[2] + d[3] * (nodes[0] - nodes[2]))
163 }
164
165 #[test]
166 fn below_the_knots_the_curve_continues_with_the_mean_curvature() {
167 let (x_low, x_high) = (280.0_f64.ln(), 470.0_f64.ln());
168 let mut spline = BeamSpline::constant(280.0, 470.0, 1.0);
169 for _ in 0..3 {
170 let c: Vec<f64> = (0..spline.coefficients().len())
171 .map(|i| (i as f64 * 0.7).sin())
172 .collect();
173 let wavy = spline.with_coefficients(&c);
174 let f = |x: f64| ln_beam(&wavy, x.exp());
175 let h = (x_high - x_low) / wavy.intervals() as f64;
176 let within = |end: f64, toward: f64| {
177 [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0].map(|k| end + toward * k * h)
178 };
179 let slope_low = slope_at_first_node(&f, within(x_low, 1.0));
180 let slope_high = slope_at_first_node(&f, within(x_high, -1.0));
181 let curvature = (slope_high - slope_low) / (x_high - x_low);
182 for dx in [-0.05, -0.2, -0.4] {
183 let expected = f(x_low) + slope_low * dx + curvature * dx * dx / 2.0;
184 assert!(
185 (f(x_low + dx) - expected).abs() < 1e-9 * (1.0 + expected.abs()),
186 "{} intervals, {dx}: {} vs {expected}",
187 wavy.intervals(),
188 f(x_low + dx)
189 );
190 }
191 spline = spline.refined();
192 }
193 }
194}