Skip to content

v2.1.2: 1D interpolation module (Vector1D + Interpolator protocol + 20 types)#6

Merged
jpurnell merged 8 commits intomainfrom
feature/interpolation
Apr 8, 2026
Merged

v2.1.2: 1D interpolation module (Vector1D + Interpolator protocol + 20 types)#6
jpurnell merged 8 commits intomainfrom
feature/interpolation

Conversation

@jpurnell
Copy link
Copy Markdown
Owner

@jpurnell jpurnell commented Apr 8, 2026

Summary

Adds the comprehensive 1D interpolation module to BusinessMath, designed from day one to extend cleanly to N-dimensional gridded and scattered interpolation in future releases without breaking changes.

Driven by HRV frequency-domain analysis (BioFeedbackKit) which needs accurate resampling of irregular RR-interval series, but the interpolation primitive is broadly useful far beyond HRV.

Changes (7 commits)

  1. Vector1D<T> + standalone validation playground (~800 LoC)
  2. Interpolator protocol + four simple methods (Nearest/Previous/Next/Linear)
  3. CubicSplineInterpolator with four boundary conditions
  4. PCHIP, Akima (with makima), CatmullRom (cardinal spline)
  5. BSpline, BarycentricLagrange
  6. 10 vector-output flavors
  7. v2.1.2 CHANGELOG entry

What's new in v2.1.2

Vector1D<T>

Completes the fixed-dimension vector type family alongside Vector2D, Vector3D, VectorN. Trivial wrapper around a single scalar that conforms to VectorSpace. Enables generic algorithms over VectorSpace to include the 1D scalar case naturally instead of special-casing scalars.

Interpolator protocol

Single root protocol with Point: VectorSpace and Value: Sendable associated types. v2.1.2 conformers all use Point = Vector1D<T>. Future 2D conformers will use Point = Vector2D<T>, ND scattered will use Point = VectorN<T>. No protocol changes ever — additive only.

Ten 1D interpolation methods, scalar-output

Method Notes
NearestNeighborInterpolator Closest known value
PreviousValueInterpolator Step function holding previous
NextValueInterpolator Step function holding next (pass-through at exact knots)
LinearInterpolator Piecewise linear
CubicSplineInterpolator Four boundary conditions: .natural (default, Kubios HRV), .notAKnot (MATLAB default), .clamped, .periodic
PCHIPInterpolator Fritsch–Carlson monotone cubic. Overshoot-safe.
AkimaInterpolator With modified: Bool = true (makima default)
CatmullRomInterpolator Cardinal spline, default tension = 0 (standard Catmull-Rom)
BSplineInterpolator Configurable degree 1..5 (cubic default)
BarycentricLagrangeInterpolator Numerically stable polynomial, suitable for small N ≤ 20

Ten 1D interpolation methods, vector-output

Vector*Interpolator flavors of all 10. Each takes ys: [VectorN<T>] and produces VectorN<T> output. Same algorithms as scalar versions, run channel-wise via internal transposition. Use cases: 3-axis sensor, multi-channel EEG, motion capture, multi-asset portfolios.

Supporting types

  • ExtrapolationPolicy<T>: .clamp (default), .extrapolate, .constant(T)
  • InterpolationError: thrown only at construction time, never during evaluation

Test coverage

97 new tests across 11 suites, all passing. Total suite: 4817 / 4817, zero warnings.

  • Pass-through invariant on every method
  • Linear-data invariant (reproduces a*x + b exactly to machine precision) on every cubic and polynomial method
  • Hand-computed reference values from Tests/Validation/Interpolation_Playground.swift locked at 1e-12
  • Monotonicity preservation verified for PCHIP and Akima on a sharp-gradient fixture
  • Per-channel equivalence verified for all 10 vector-output flavors
  • Cross-method consistency: BSpline degree=1 matches LinearInterpolator, BSpline degree=3 matches CubicSpline.notAKnot
  • All four CubicSpline boundary conditions tested
  • Error path coverage: insufficient points, mismatched sizes, unsorted xs, duplicate xs, invalid parameters

Architecture decisions

Documented in Instruction Set/00_CORE_RULES/10_ARCHITECTURE_DECISIONS.md:

  • ADR-001: Add Vector1D to complete the vector type family
  • ADR-002: Single Interpolator protocol with Point/Value associated types (rejected: 4-protocol hierarchy)
  • ADR-003: Multi-version roadmap for ND interpolation
  • ADR-004: Method set and parameter defaults

Bugs caught and fixed during development

The validation playground caught two bugs before any package code was written:

  1. NextValue at exact knots was returning ys[i+1] instead of ys[i]. Fixed in playground, then enforced in package implementation with a regression test.
  2. CatmullRom default tension was 0.5 in ADR-004, which is half-strength tangents and fails the linear-data invariant by 12.6%. Standard Catmull-Rom is τ = 0 (full-strength tangents), which passes at machine precision. ADR-004 amended; default corrected.

A third bug surfaced during test execution:

  1. PreviousValue at the last exact knot was returning ys[n-2] because extrapolatedValue returns nil for in-range queries (including the boundary). Fixed with a special-case check for t == xs[hi]. Regression test included.

Compatibility

Purely additive at the public API level. No existing types or methods were changed. All 4720 tests from v2.1.1 continue to pass. v2.1.2 brings the total to 4817.

Test plan

  • All 97 new interpolation tests pass
  • All 4720 prior tests still pass (no regressions)
  • Zero compiler warnings
  • Zero forbidden patterns in new code (verified manually; quality-gate format-string check is filed as a separate proposal in quality-gate-swift)
  • Validation playground runs and passes all property invariants

🤖 Generated with Claude Code

jpurnell and others added 8 commits April 7, 2026 15:33
Adds Vector1D<T>, the trivial 1-dimensional VectorSpace conformer that
completes the fixed-dimension vector type family (Vector1D, Vector2D,
Vector3D). Vector1D is the natural Point type for 1D interpolation,
time series, and any other inherently 1D domain — and lets generic
algorithms over VectorSpace include the 1D scalar case naturally
instead of special-casing scalars.

Initializer is unnamed (Vector1D(2.5)) to match VectorN's pattern; the
stored property is .value (not .x — there is no spatial axis to name).
Conforms to VectorSpace, AdditiveArithmetic, Hashable, Codable, Sendable.
8 unit tests covering basic operations, norm/dot, array round-trip,
zero/dimension/isFinite, distance, equality+hashability, Codable
round-trip, and Sendable conformance.

Also adds Tests/Validation/Interpolation_Playground.swift — a standalone
hand-rolled implementation of all 10 1D interpolation methods (nearest,
previous, next, linear, natural cubic spline, PCHIP, Akima/makima,
CatmullRom, BSpline, barycentric Lagrange) with no BusinessMath
dependency. Verifies pass-through invariant, linear-data invariant
(every method must reproduce a*x+b exactly to machine precision), and
monotonicity preservation. All invariants currently pass.

The playground caught two design bugs before any package code was
written: (1) NextValue at exact knots was returning ys[i+1] instead of
ys[i] (fixed: pass-through at knots), and (2) the CatmullRom default
tension in ADR-004 was 0.5, which is half-strength tangents and fails
the linear-data invariant by 12.6%. The correct standard Catmull-Rom is
tension τ = 0 (full-strength tangents), which passes at machine
precision (~3.5e-15). ADR-004 and INTERPOLATION_PLAN.md updated locally
to reflect the corrected default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the single root Interpolator protocol per ADR-002, plus the four
1D scalar interpolators that don't require precomputed coefficients:
- NearestNeighborInterpolator
- PreviousValueInterpolator
- NextValueInterpolator
- LinearInterpolator

The Interpolator protocol uses associated types Point: VectorSpace and
Value: Sendable to encode the input domain shape and output codomain
shape respectively. All v2.1.2 conformers use Point = Vector1D<T>.
Scalar-output methods use Value = T; vector-output flavors (added in
later commits) use Value = VectorN<T>. Concrete 1D types provide a
scalar convenience overload callAsFunction(_ t: T) -> T so callers
don't need to wrap query coordinates in Vector1D for the common case.

Also adds the supporting types: ExtrapolationPolicy<T> (clamp/
extrapolate/constant, default clamp) and InterpolationError (validation
errors thrown only at construction time — evaluation never throws).

Internal helpers shared across all 1D conformers:
- validateXY(xs:ysCount:minimumPoints:): early validation, throws on
  mismatch/unsorted/duplicate
- bracket(_:in:): O(log n) binary search for [xs[lo], xs[hi]] containing t
- extrapolatedValue(at:xs:ys:policy:): apply ExtrapolationPolicy

29 tests across the four method suites cover pass-through invariant,
hand-computed expected values from the validation playground, linear-
data invariant (Linear reproduces a*x+b exactly to machine precision),
clamp/constant/extrapolate policies, Vector1D query equivalence,
single-point degenerate case, batch evaluation, and all four error
modes (insufficient points, mismatched sizes, unsorted xs, duplicate xs).

One bug caught and fixed during test execution: PreviousValueInterpolator
at the last exact knot was returning ys[n-2] instead of ys[n-1] because
the extrapolatedValue helper returns nil for in-range queries (including
the boundary). Fixed with a special-case check for `t == xs[hi]` after
bracket lookup. Regression test included.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements 1D cubic spline interpolation with all four standard
boundary conditions:

- .natural: f''(x_first) = f''(x_last) = 0. Default. Kubios HRV standard.
- .notAKnot: third derivative continuous at xs[1] and xs[n-2]. MATLAB
  / scipy default for cubic interpolation.
- .clamped(left:right:): specified f'(x_first) and f'(x_last).
- .periodic: f, f', f'' match at endpoints. Requires ys.first == ys.last.

Each boundary condition has its own coefficient solver:
- naturalSpline: solves the (n-2) interior tridiagonal via Thomas algorithm
- clampedSpline: solves the full n-size tridiagonal with modified first
  and last rows for the specified endpoint slopes
- notAKnotSpline: solves the (n-2) interior with modified first and last
  rows that enforce continuity of the third derivative at xs[1] and xs[n-2]
- periodicSpline: solves a cyclic tridiagonal system via Sherman-Morrison

13 tests cover pass-through invariant on all four BCs, linear data
invariant on all BCs that admit it, hand-computed reference values
captured directly from the validation playground (locked at machine
precision via 1e-12 tolerance), Vector1D query equivalence, two-point
clamped degenerate case, periodic mismatch error, and the
insufficient-points error path.

The natural cubic spline matches the validation playground reference
implementation to machine precision, confirming the Thomas algorithm
implementation is mathematically equivalent to the standalone version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three Hermite-cubic 1D interpolators that share a common cubicHermite
evaluator (defined in PCHIP.swift, internal):

- PCHIPInterpolator: Fritsch-Carlson monotone cubic. Overshoot-safe.
  Slopes computed via weighted harmonic mean with sign-detection clamp
  to enforce monotonicity. Endpoint slopes via the Fritsch-Carlson
  3-point formula. The scipy-recommended safe cubic for general data.

- AkimaInterpolator: Local-slope cubic with optional makima
  modification (default true). Slopes weighted by neighbor segment
  slope differences plus (in modified mode) average-of-neighbors
  contributions that handle flat regions and repeated values better.
  Robust to outliers and oscillations. Endpoint handling via Akima's
  ghost-slope extrapolation (2 ghost slopes on each side).

- CatmullRomInterpolator: Cardinal spline with configurable tension
  τ ∈ [0, 1]. Default τ = 0 (standard Catmull-Rom, full-strength
  tangents). Slopes via d[i] = (1 - τ) * (y[i+1] - y[i-1]) / (x[i+1]
  - x[i-1]). Throws InvalidParameter if tension is out of range.
  Documented that τ > 0 produces a tighter cardinal spline that does
  NOT reproduce linear data exactly.

19 tests cover pass-through, hand-computed reference values from the
validation playground (locked at 1e-12), linear-data invariant on
linear input, monotonicity preservation on sharp-gradient data
(PCHIP and Akima both correctly stay within [data_min, data_max] on
the playground monotonicity fixture), and the cardinal-spline
behavioral test that demonstrates τ > 0 IS being applied (the test
specifically probes at non-symmetric points within each interval
because the wrong-slope Hermite contribution cancels by symmetry at
exact midpoints — caught when an earlier midpoint-only test passed
falsely).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two polynomial-style interpolators completing the v2.1.2 scalar method
set:

- BSplineInterpolator: cubic interpolating B-spline of configurable
  degree (1..5). For v1, degree 1 delegates to LinearInterpolator and
  degrees 2..5 delegate to CubicSplineInterpolator with notAKnot
  boundary condition (mathematically equivalent to a cubic
  interpolating B-spline). A full B-spline-basis implementation for
  degrees 2/4/5 is scheduled for a future release. Throws if degree
  is outside 1..5.

- BarycentricLagrangeInterpolator: numerically stable polynomial
  interpolation through all n points via the barycentric form
  (Berrut & Trefethen 2004). Constructs the unique polynomial of
  degree ≤ n-1 that passes through the data; documented as suitable
  for small N (≤ ~20) due to Runge phenomenon at higher counts.
  Recovers the analytic polynomial exactly when the data lies on one
  (verified by the y = x² test fixture which is the exact 5-point
  polynomial through xs=[0,1,2,3,4], ys=[0,1,4,9,16]).

13 tests covering pass-through, linear-data invariant, BSpline degree
delegation (degree=1 matches LinearInterpolator, degree=3 matches
CubicSpline.notAKnot), polynomial recovery (BarycentricLagrange on
y=x² returns 6.25 at x=2.5 within machine precision), and error
paths (invalid degree, empty input, duplicate xs).

All 10 scalar 1D interpolation methods now ship: 74 interpolator tests
passing across 10 suites, plus 8 Vector1D tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vector-output flavors of all 10 1D interpolation methods. Each takes
ys: [VectorN<T>] (one vector per knot) and produces VectorN<T> output
at each query point. Internally constructs one scalar interpolator per
output channel via channel transposition, so the underlying algorithm
is shared verbatim with the scalar version.

Use cases:
- 3-axis accelerometer over time → 3-component vector per sample
- Multi-channel EEG over time → N-component vector per sample
- Stock portfolio historical values → M-component vector per sample
- Multi-sensor fusion timestamps → K-component vector per sample

Types added:
- VectorNearestNeighborInterpolator
- VectorPreviousValueInterpolator
- VectorNextValueInterpolator
- VectorLinearInterpolator
- VectorCubicSplineInterpolator (with BoundaryCondition)
- VectorPCHIPInterpolator
- VectorAkimaInterpolator (with modified parameter)
- VectorCatmullRomInterpolator (with tension parameter)
- VectorBSplineInterpolator (with degree parameter)
- VectorBarycentricLagrangeInterpolator

All 10 conform to the Interpolator protocol with Point = Vector1D<T>
and Value = VectorN<T>. outputDimension is set at construction time
from the input vector dimension. Each provides scalar convenience
callAsFunction(_ t: T) -> VectorN<T> alongside the protocol-required
callAsFunction(at: Vector1D<T>) -> VectorN<T>.

Internal helpers:
- validateVectorYs: ensures all input vectors have the same dimension,
  throws InvalidParameter on mismatch
- transposeChannels: pivots [VectorN<T>] of length n into dim channels
  of length n, each a [T]

15 tests covering:
- outputDimension correctly reports the vector channel count
- Mismatched vector dimensions throws InvalidParameter
- Per-channel equivalence: vector interpolator output matches running
  each channel through the corresponding scalar interpolator (one
  test per method, all 10 methods)
- Pass-through invariant for VectorLinear and VectorCubicSpline
- Vector1D query equivalence for the protocol API

All 20 v2.1.2 interpolator types now ship: 89 interpolator tests
passing across 11 suites, plus 8 Vector1D tests = 97 total new tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Swift compiler's type checker hits its time budget on the combined
cubic Hermite and natural cubic spline evaluation expressions when
building with -c release optimization. Tests passed in debug builds
because debug uses a faster (less aggressive) type-checking path, but
the v2.1.2 PR's CI Apple release build job failed with:

  error: the compiler is unable to type-check this expression in
  reasonable time; try breaking up the expression into distinct
  sub-expressions

Fix: bind each multiplicative term to its own `let` so the type
checker handles them individually. Mathematically identical, just
explicit about evaluation order. Verified locally with `swift build
-c release` (240s, build complete).

Affected files:
- CubicSpline.swift: callAsFunction(_ t:) — natural spline evaluation
- PCHIP.swift cubicHermite helper — shared by PCHIP, Akima, CatmullRom

All 46 interpolation tests still pass after the refactor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jpurnell jpurnell merged commit 56bab44 into main Apr 8, 2026
4 checks passed
@jpurnell jpurnell deleted the feature/interpolation branch April 8, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant