tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it./workspace/cubicspline/src/spline.rs
1 //! Cubic-spline interpolation with selectable boundary conditions.
2 //!
3 //! Given knots `(x_0, y_0), …, (x_{n-1}, y_{n-1})` with strictly increasing
4 //! abscissae, a *cubic spline* `S` is the piecewise-cubic function that
5 //! interpolates the data (`S(x_i) = y_i`) and is twice continuously
6 //! differentiable (`C²`) across the interior knots. Two extra equations close
7 //! the system; the [`BoundaryCondition`] selects them:
8 //!
9 //! - **Natural**: `S''(x_0) = S''(x_{n-1}) = 0`.
10 //! - **Clamped**: prescribed slopes `S'(x_0) = left`, `S'(x_{n-1}) = right`.
11 //! - **Not-a-knot**: `S'''` continuous across the first and last interior knots.
12 //!
13 //! # Method
14 //!
15 //! Write `M_i = S''(x_i)` for the second-derivative *moments* and
16 //! `h_i = x_{i+1} - x_i` for the segment widths. The `C¹` matching at each
17 //! interior knot `i = 1 … n-2` gives
18 //!
19 //! ```text
20 //! h_{i-1} M_{i-1} + 2 (h_{i-1} + h_i) M_i + h_i M_{i+1}
21 //! = 6 [ (y_{i+1} - y_i) / h_i - (y_i - y_{i-1}) / h_{i-1} ].
22 //! ```
23 //!
24 //! These `n - 2` equations plus the two boundary equations form an `n × n`
25 //! linear system `A M = d`. The boundary rows are:
26 //!
27 //! - **Natural**: `M_0 = 0` and `M_{n-1} = 0`.
28 //! - **Clamped**: `2 h_0 M_0 + h_0 M_1 = 6((y_1 - y_0)/h_0 - left)` and
29 //! `h_{n-2} M_{n-2} + 2 h_{n-2} M_{n-1} = 6(right - (y_{n-1} - y_{n-2})/h_{n-2})`.
30 //! - **Not-a-knot**: `h_1 M_0 - (h_0 + h_1) M_1 + h_0 M_2 = 0` and
31 //! `h_{n-2} M_{n-3} - (h_{n-3} + h_{n-2}) M_{n-2} + h_{n-3} M_{n-1} = 0`
32 //! (equal third derivative across the first / last interior knot).
33 //!
34 //! Once the moments are known, evaluation on `[x_i, x_{i+1}]` uses the standard
35 //! moment form (see [`CubicSpline::eval`]).
36 //!
37 //! The public entry point is [`CubicSpline::build`]. The numerical core,
38 //! [`solve_moments`], assembles `A M = d` for the chosen boundary condition and
39 //! solves it (with the provided [`solve_linear`]); it is invoked once per
40 //! construction.
41
42 use crate::config::{BoundaryCondition, Config, Extrapolation};
43 use crate::error::SplineError;
44 use crate::knots::Knots;
45
46 /// Solve the dense linear system `A x = b` by Gaussian elimination with partial
47 /// pivoting. `a` is an `n × n` matrix given row-major as `a[i][j]`; `b` has
48 /// length `n`. Returns the solution, or `None` if `A` is (numerically)
49 /// singular.
50 ///
51 /// This is the provided linear-algebra primitive used by the moment solve. The
52 /// spline systems are well-conditioned (diagonally dominant for the natural and
53 /// clamped rows), so a direct solve is robust.
54 ///
55 /// ```
56 /// use cubicspline::solve_linear;
57 /// // [[2,1],[1,3]] x = [3,5] => x = [0.8, 1.4].
58 /// let a = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
59 /// let x = solve_linear(&a, &[3.0, 5.0]).unwrap();
60 /// assert!((x[0] - 0.8).abs() < 1e-12 && (x[1] - 1.4).abs() < 1e-12);
61 /// ```
62 pub fn solve_linear(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
63 let n = b.len();
64 if a.len() != n {
65 return None;
66 }
67 let mut m: Vec<Vec<f64>> = Vec::with_capacity(n);
68 for row in a {
69 if row.len() != n {
70 return None;
71 }
72 m.push(row.clone());
73 }
74 let mut rhs = b.to_vec();
75
76 for col in 0..n {
77 let mut pivot_row = col;
78 let mut pivot_mag = m[col][col].abs();
79 for (r, mrow) in m.iter().enumerate().skip(col + 1) {
80 let mag = mrow[col].abs();
81 if mag > pivot_mag {
82 pivot_mag = mag;
83 pivot_row = r;
84 }
85 }
86 let pivot = m[pivot_row][col];
87 if !pivot.is_finite() || pivot == 0.0 {
88 return None;
89 }
90 if pivot_row != col {
91 m.swap(pivot_row, col);
92 rhs.swap(pivot_row, col);
93 }
94 for r in (col + 1)..n {
95 let factor = m[r][col] / m[col][col];
96 if factor != 0.0 {
97 for c in col..n {
98 let v = m[col][c];
99 m[r][c] -= factor * v;
100 }
101 rhs[r] -= factor * rhs[col];
102 }
103 }
104 }
105
106 let mut x = vec![0.0; n];
107 for i in (0..n).rev() {
108 let mut s = rhs[i];
109 for j in (i + 1)..n {
110 s -= m[i][j] * x[j];
111 }
112 let diag = m[i][i];
113 if !diag.is_finite() || diag == 0.0 {
114 return None;
115 }
116 x[i] = s / diag;
117 }
118 if x.iter().all(|v| v.is_finite()) {
119 Some(x)
120 } else {
121 None
122 }
123 }
124
125 /// A constructed cubic spline.
126 ///
127 /// Holds the validated knots, the second-derivative moments, the boundary
128 /// condition, and the out-of-domain policy. Evaluate it with
129 /// [`CubicSpline::eval`] (and its derivative variants); reuse a single spline
130 /// for any number of evaluations.
131 ///
132 /// ```
133 /// use cubicspline::{Config, CubicSpline};
134 /// let s = CubicSpline::build(&[0.0, 1.0, 2.0], &[0.0, 1.0, 4.0], &Config::new()).unwrap();
135 /// assert!((s.eval(1.0).unwrap() - 1.0).abs() < 1e-12);
136 /// ```
137 #[derive(Debug, Clone, PartialEq)]
138 pub struct CubicSpline {
139 knots: Knots,
140 moments: Vec<f64>,
141 boundary: BoundaryCondition,
142 extrapolation: Extrapolation,
143 }
144
145 impl CubicSpline {
146 /// Build the cubic spline interpolating `(x, y)` under the configured
147 /// boundary condition.
148 ///
149 /// # Errors
150 ///
151 /// - Any [`SplineError`] from knot validation (length mismatch, too few
152 /// knots, non-finite values, non-increasing abscissae).
153 /// - [`SplineError::BoundaryNeedsMoreKnots`] if not-a-knot is requested with
154 /// fewer than three knots.
155 /// - [`SplineError::NonFiniteBoundary`] if clamped slopes are non-finite.
156 pub fn build(x: &[f64], y: &[f64], config: &Config) -> Result<Self, SplineError> {
157 let knots = Knots::new(x, y)?;
158 let n = knots.len();
159 let boundary = config.boundary();
160
161 match boundary {
162 BoundaryCondition::NotAKnot if n < 3 => {
163 return Err(SplineError::BoundaryNeedsMoreKnots {
164 boundary: "not-a-knot",
165 required: 3,
166 got: n,
167 });
168 }
169 BoundaryCondition::Clamped { left, right } => {
170 if !left.is_finite() {
171 return Err(SplineError::NonFiniteBoundary {
172 which: "left",
173 value: left,
174 });
175 }
176 if !right.is_finite() {
177 return Err(SplineError::NonFiniteBoundary {
178 which: "right",
179 value: right,
180 });
181 }
182 }
183 _ => {}
184 }
185
186 let h = knots.widths();
187 let moments = solve_moments(&h, knots.ys(), boundary);
188 Ok(Self {
189 knots,
190 moments,
191 boundary,
192 extrapolation: config.extrapolation(),
193 })
194 }
195
196 /// The number of knots `n`.
197 #[inline]
198 pub fn len(&self) -> usize {
199 self.knots.len()
200 }
201
202 /// Whether the spline has no knots (never true for a built spline).
203 #[inline]
204 pub fn is_empty(&self) -> bool {
205 self.knots.is_empty()
206 }
207
208 /// The boundary condition used to build the spline.
209 pub fn boundary(&self) -> BoundaryCondition {
210 self.boundary
211 }
212
213 /// The second-derivative moments `M_i = S''(x_i)`.
214 pub fn moments(&self) -> &[f64] {
215 &self.moments
216 }
217
218 /// The domain `[x_first, x_last]` over which the spline interpolates.
219 pub fn domain(&self) -> (f64, f64) {
220 let xs = self.knots.xs();
221 (xs[0], xs[xs.len() - 1])
222 }
223
224 /// Resolve an evaluation point against the extrapolation policy, returning
225 /// the (possibly clamped) point and the index of the segment to use.
226 fn locate(&self, point: f64) -> Result<(f64, usize), SplineError> {
227 let xs = self.knots.xs();
228 let (lo, hi) = (xs[0], xs[xs.len() - 1]);
229 let p = if point < lo || point > hi {
230 match self.extrapolation {
231 Extrapolation::Error => {
232 return Err(SplineError::OutOfDomain { point, lo, hi });
233 }
234 Extrapolation::Clamp => point.clamp(lo, hi),
235 Extrapolation::Extend => point,
236 }
237 } else {
238 point
239 };
240
241 let n = xs.len();
242 let mut i = match xs.binary_search_by(|v| v.partial_cmp(&p).unwrap()) {
243 Ok(idx) => idx.min(n - 2),
244 Err(idx) => {
245 if idx == 0 {
246 0
247 } else {
248 (idx - 1).min(n - 2)
249 }
250 }
251 };
252 if i >= n - 1 {
253 i = n - 2;
254 }
255 Ok((p, i))
256 }
257
258 /// Evaluate the spline at `point`.
259 ///
260 /// # Errors
261 ///
262 /// [`SplineError::OutOfDomain`] if `point` lies outside the domain and the
263 /// configured policy is [`Extrapolation::Error`].
264 pub fn eval(&self, point: f64) -> Result<f64, SplineError> {
265 let (p, i) = self.locate(point)?;
266 let xs = self.knots.xs();
267 let ys = self.knots.ys();
268 let (xi, xi1) = (xs[i], xs[i + 1]);
269 let h = xi1 - xi;
270 let a = xi1 - p;
271 let b = p - xi;
272 let mi = self.moments[i];
273 let mi1 = self.moments[i + 1];
274
275 let term_cubic = mi * a * a * a / (6.0 * h) + mi1 * b * b * b / (6.0 * h);
276 let term_linear =
277 (ys[i] / h - mi * h / 6.0) * a + (ys[i + 1] / h - mi1 * h / 6.0) * b;
278 Ok(term_cubic + term_linear)
279 }
280
281 /// Evaluate the first derivative `S'(point)`.
282 pub fn derivative(&self, point: f64) -> Result<f64, SplineError> {
283 let (p, i) = self.locate(point)?;
284 let xs = self.knots.xs();
285 let ys = self.knots.ys();
286 let (xi, xi1) = (xs[i], xs[i + 1]);
287 let h = xi1 - xi;
288 let a = xi1 - p;
289 let b = p - xi;
290 let mi = self.moments[i];
291 let mi1 = self.moments[i + 1];
292
293 let d = -mi * a * a / (2.0 * h) + mi1 * b * b / (2.0 * h) + (ys[i + 1] - ys[i]) / h
294 - (mi1 - mi) * h / 6.0;
295 Ok(d)
296 }
297
298 /// Evaluate the second derivative `S''(point)` (linear in `x` on each
299 /// segment, interpolating the moments).
300 pub fn second_derivative(&self, point: f64) -> Result<f64, SplineError> {
301 let (p, i) = self.locate(point)?;
302 let xs = self.knots.xs();
303 let (xi, xi1) = (xs[i], xs[i + 1]);
304 let h = xi1 - xi;
305 let a = xi1 - p;
306 let b = p - xi;
307 Ok(self.moments[i] * a / h + self.moments[i + 1] * b / h)
308 }
309 }
310
311 /// Numerical core: compute the second-derivative moments `M_i = S''(x_i)` for
312 /// the chosen [`BoundaryCondition`].
313 ///
314 /// On entry:
315 /// - `h` holds the segment widths `h_i = x_{i+1} - x_i` (length `n - 1`, all
316 /// strictly positive);
317 /// - `y` holds the knot ordinates (length `n`);
318 /// - `bc` is the boundary condition.
319 ///
320 /// Returns the moment vector `M` of length `n`.
321 ///
322 /// Assemble the `n × n` tridiagonal moment system `A M = d` for the standard
323 /// `C²` cubic spline and solve it with [`solve_linear`]. The interior rows come
324 /// from first-derivative matching at each interior knot; the two boundary rows
325 /// are those of the selected [`BoundaryCondition`] (Natural: zero end second
326 /// derivative; Clamped: prescribed end slopes; NotAKnot: `S'''` continuous
327 /// across the first/last interior knot). Handle the degenerate `n == 2` case,
328 /// and the `n == 3` not-a-knot case where the two end conditions coincide,
329 /// consistently with those conditions. The behavioural contract is the suite.
330 pub fn solve_moments(h: &[f64], y: &[f64], bc: BoundaryCondition) -> Vec<f64> {
331 // TODO(sci-5102): implement the cubic-spline moment solve (see doc comment).
332 let _ = (h, y, bc, solve_linear as fn(&[Vec<f64>], &[f64]) -> Option<Vec<f64>>);
333 todo!("implement solve_moments (sci-5102)")
334 }
335
/workspace/cubicspline/src/spline.rs
pub fn solve_moments(h: &[f64], y: &[f64], bc: BoundaryCondition) -> Vec<f64> {
// TODO(sci-5102): implement the cubic-spline moment solve (see doc comment).
let _ = (h, y, bc, solve_linear as fn(&[Vec<f64>], &[f64]) -> Option<Vec<f64>>);
todo!("implement solve_moments (sci-5102)")
}pub fn solve_moments(h: &[f64], y: &[f64], bc: BoundaryCondition) -> Vec<f64> {
let n = y.len();
// Not-a-knot with exactly three knots: the two end conditions coincide, so
// the moment system is singular. The interpolant collapses to the single
// quadratic through the three points, whose second derivative is the
// constant `2 ·[y0,y1,y2]` (twice the second divided difference).
if matches!(bc, BoundaryCondition::NotAKnot) && n == 3 {
let dd = ((y[2] - y[1]) / h[1] - (y[1] - y[0]) / h[0]) / (h[0] + h[1]);
let m = 2.0 * dd;
return vec![m, m, m];
}
let mut a = vec![vec![0.0; n]; n];
let mut d = vec![0.0; n];
// Interior rows: first-derivative matching at each interior knot.
for i in 1..n - 1 {
a[i][i - 1] = h[i - 1];
a[i][i] = 2.0 * (h[i - 1] + h[i]);
a[i][i + 1] = h[i];
d[i] = 6.0 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1]);
}
// Boundary rows.
match bc {
BoundaryCondition::Natural => {
a[0][0] = 1.0;
d[0] = 0.0;
a[n - 1][n - 1] = 1.0;
d[n - 1] = 0.0;
}
BoundaryCondition::Clamped { left, right } => {
a[0][0] = 2.0 * h[0];
a[0][1] = h[0];
d[0] = 6.0 * ((y[1] - y[0]) / h[0] - left);
a[n - 1][n - 2] = h[n - 2];
a[n - 1][n - 1] = 2.0 * h[n - 2];
d[n - 1] = 6.0 * (right - (y[n - 1] - y[n - 2]) / h[n - 2]);
}
BoundaryCondition::NotAKnot => {
// S''' continuous across the first / last interior knot.
a[0][0] = h[1];
a[0][1] = -(h[0] + h[1]);
a[0][2] = h[0];
d[0] = 0.0;
a[n - 1][n - 3] = h[n - 2];
a[n - 1][n - 2] = -(h[n - 3] + h[n - 2]);
a[n - 1][n - 1] = h[n - 3];
d[n - 1] = 0.0;
}
}
solve_linear(&a, &d).expect("moment system is nonsingular")
}Run test suite
cargo test --release 2>&1 | tail -40
Compiling cubicspline v0.3.0 (/workspace/cubicspline)
Finished `release` profile [optimized] target(s) in 8.14s
Running unittests src/lib.rs (target/release/deps/cubicspline-720e3262dd09e6df)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-53b7728d6bb4a97b)
running 2 tests
test interpolate_helper_runs ... ok
test natural_spline_interpolates_its_knots ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests cubicspline
running 9 tests
test src/config.rs - config::Config (line 51) ... ok
test src/knots.rs - knots::Knots::new (line 31) ... ok
test src/lib.rs - (line 29) ... ok
test src/lib.rs - (line 16) ... ok
test src/lib.rs - interpolate (line 60) ... ok
test src/monotone.rs - monotone::MonotoneSpline::build (line 43) ... ok
test src/parametric.rs - parametric::ParametricSpline2D::build (line 46) ... ok
test src/spline.rs - spline::CubicSpline (line 132) ... ok
test src/spline.rs - spline::solve_linear (line 55) ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s
[stdout]
Compiling cubicspline v0.3.0 (/workspace/cubicspline)
Finished `release` profile [optimized] target(s) in 8.14s
Running unittests src/lib.rs (target/release/deps/cubicspline-720e3262dd09e6df)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-53b7728d6bb4a97b)
running 2 tests
test interpolate_helper_runs ... ok
test natural_spline_interpolates_its_knots ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests cubicspline
running 9 tests
test src/config.rs - config::Config (line 51) ... ok
test src/knots.rs - knots::Knots::new (line 31) ... ok
test src/lib.rs - (line 29) ... ok
test src/lib.rs - (line 16) ... ok
test src/lib.rs - interpolate (line 60) ... ok
test src/monotone.rs - monotone::MonotoneSpline::build (line 43) ... ok
test src/parametric.rs - parametric::ParametricSpline2D::build (line 46) ... ok
test src/spline.rs - spline::CubicSpline (line 132) ... ok
test src/spline.rs - spline::solve_linear (line 55) ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60sSummarize test results and warnings
cargo test --release 2>&1 | grep -E "test result|Running|error|warning"
Running unittests src/lib.rs (target/release/deps/cubicspline-720e3262dd09e6df)
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-53b7728d6bb4a97b)
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58s
[stdout]
Running unittests src/lib.rs (target/release/deps/cubicspline-720e3262dd09e6df)
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-53b7728d6bb4a97b)
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58sList tests and check build warnings
ls tests/ 2>/dev/null; echo "---"; cargo build --release 2>&1 | grep -iE "warning|error" || echo "no warnings/errors"
smoke.rs --- no warnings/errors [stdout] smoke.rs --- no warnings/errors
No file changes, the agent left the workspace untouched.
[test] injected hidden conformance suite
[test] running cargo test --release (offline)
Compiling cubicspline v0.3.0 (/workspace/cubicspline)
Finished `release` profile [optimized] target(s) in 0.97s
Running unittests src/lib.rs (target/release/deps/cubicspline-720e3262dd09e6df)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/conformance.rs (target/release/deps/conformance-4420975f7d4dd771)
running 33 tests
test clamp_policy_returns_endpoint_values ... ok
test c2_continuity_holds_for_every_boundary ... ok
test clamped_reproduces_a_cubic_exactly ... ok
test clamped_reproduces_prescribed_end_slopes ... ok
test clamped_quadratic_has_constant_second_derivative ... ok
test convenience_interpolate_matches_build_then_eval ... ok
test default_policy_rejects_out_of_domain ... ok
test extend_policy_continues_boundary_cubic ... ok
test interpolates_all_knots_for_every_boundary ... ok
test knots_validate_independently ... ok
test known_moments_and_value_for_symmetric_hat ... ok
test monotone_does_not_overshoot_a_step ... ok
test monotone_is_monotone_on_increasing_data ... ok
test monotone_reproduces_a_line ... ok
test natural_boundary_moments_are_zero ... ok
test natural_does_not_reproduce_a_generic_cubic ... ok
test not_a_knot_has_nonzero_end_moments_and_smooth_third_derivative ... ok
test not_a_knot_converges_to_sin_under_refinement ... ok
test not_a_knot_on_three_points_is_the_parabola ... ok
test not_a_knot_reproduces_a_cubic_exactly ... ok
test parametric_arc_length_of_half_circle ... ok
test parametric_passes_through_its_points ... ok
test parametric_rejects_coincident_points ... ok
test parametric_straight_segment_arc_length_is_chord ... ok
test rejects_length_mismatch ... ok
test rejects_non_finite_clamped_slope ... ok
test rejects_non_finite_x ... ok
test rejects_non_increasing_abscissae ... ok
test rejects_not_a_knot_with_two_knots ... ok
test rejects_too_few_knots ... ok
test second_derivative_equals_moment_at_each_knot ... ok
test reproduces_linear_data_exactly_natural ... ok
test two_knots_make_a_line ... ok
test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-53b7728d6bb4a97b)
running 2 tests
test interpolate_helper_runs ... ok
test natural_spline_interpolates_its_knots ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Finished `release` profile [optimized] target(s) in 0.01s
Doc-tests cubicspline
running 9 tests
test src/knots.rs - knots::Knots::new (line 31) ... ok
test src/config.rs - config::Config (line 51) ... ok
test src/lib.rs - (line 16) ... ok
test src/lib.rs - (line 29) ... ok
test src/lib.rs - interpolate (line 60) ... ok
test src/monotone.rs - monotone::MonotoneSpline::build (line 43) ... ok
test src/parametric.rs - parametric::ParametricSpline2D::build (line 46) ... ok
test src/spline.rs - spline::CubicSpline (line 132) ... ok
test src/spline.rs - spline::solve_linear (line 55) ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s
[test] all tests passed
[test] reward = 1Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_943ae028c8a74912. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_943ae028c8a74912 · verifier authoritative; classifier explanatory.