Skip to main content

ph_surfaces/
lib.rs

1//! Deterministic no-std, no-alloc integer surface mappings for embedded Rust.
2//!
3//! # Status
4//!
5//! **Lifecycle:** Active. **Distribution:** published at version `0.1.0` on
6//! [crates.io](https://crates.io/crates/ph-surfaces), with API documentation on
7//! [docs.rs](https://docs.rs/ph-surfaces). The API is intentionally narrow.
8//! There is no 1.0 compatibility promise.
9//!
10//! This crate provides the validated static representation [`BilinearSurface`],
11//! its evaluator [`BilinearSurface::evaluate`], the boundary policy vocabulary
12//! ([`Boundary`], [`BoundaryPolicy`]), the out-of-domain outcome type
13//! ([`SurfaceError`]), and the four compile-time axis lookup strategies
14//! ([`LinearAxis`], [`BinaryAxis`], [`UniformAxis`], [`BucketedAxis`]) behind
15//! the sealed [`AxisLookup`] and [`KnotArray`] traits. Scalar interpolation is
16//! private.
17//!
18//! Firmware-first usage lives in the packaged README ("Start here") and the
19//! Cargo examples `firmware_quickstart`, `uniform_sensor_compensation`,
20//! `mixed_calibration_map`, `fail_safe_boundaries`, and
21//! `firmware_cost_budget`. The repository also carries task-oriented guides
22//! that are not part of the crate artifact:
23//! [usage](https://github.com/photon-circus/ph-surfaces/blob/v0.1.0/docs/usage-guide.md),
24//! [interpolation walkthrough](https://github.com/photon-circus/ph-surfaces/blob/v0.1.0/docs/interpolation-walkthrough.md),
25//! and [choosing a strategy](https://github.com/photon-circus/ph-surfaces/blob/v0.1.0/docs/choosing-a-strategy.md).
26//!
27//! A `BilinearSurface` evaluates a static rectilinear `u16 × u16 → i32`
28//! bilinear surface with deterministic X-then-Y interpolation and four
29//! independent Error/Clamp boundary sides. Each axis selects its lookup
30//! strategy at compile time.
31//!
32//! # Evaluation contract
33//!
34//! [`BilinearSurface::evaluate`] resolves X before Y, so the X-side error wins
35//! when both coordinates leave the domain on Error sides, and a clamped X is
36//! still followed by a Y resolved under its own selections. It then
37//! interpolates along X on the lower-Y row, along X on the upper-Y row, and
38//! finally interpolates those two already-rounded results along Y.
39//!
40//! That order is normative rather than incidental: every step rounds to nearest
41//! with exact half-way values away from zero, so a Y-then-X composition returns
42//! different values. Evaluation is stateless, allocation-free, and integer
43//! only.
44//!
45//! ```
46//! use ph_surfaces::BilinearSurface;
47//!
48//! static AXIS: [u16; 2] = [0, 2];
49//! static VALUES: [[i32; 2]; 2] = [[0, 0], [1, 3]];
50//! static SURFACE: BilinearSurface<2, 2> = BilinearSurface::new(&AXIS, &AXIS, &VALUES);
51//!
52//! assert_eq!(SURFACE.evaluate(1, 1), Ok(1));
53//! ```
54//!
55//! # Contract
56//!
57//! **Representation.** [`BilinearSurface<NX, NY>`](BilinearSurface) references
58//! `&'static [u16; NX]` X knots, `&'static [u16; NY]` Y knots, and a row-major
59//! `&'static [[i32; NX]; NY]` value grid addressed as `values[y][x]`. Swapping
60//! unequal X/Y dimensions is a compile-time type error; a square transpose
61//! preserves the type, so callers remain responsible for row-major orientation.
62//! [`BilinearSurface::new`] is a
63//! `const fn` that asserts at least two knots per axis and strict increase of
64//! both axes, so an invalid `static` definition fails to compile. The handle
65//! carries no units, provenance, or other metadata.
66//!
67//! **Lookup strategies.** Each axis selects how it locates a coordinate, in the
68//! type, and the two axes select independently. [`BinaryAxis`] is the default,
69//! so `BilinearSurface<NX, NY>` and [`BilinearSurface::new`] are the
70//! binary-knotted surface. [`LinearAxis`] scans a small
71//! axis, [`UniformAxis`] describes evenly spaced knots by origin, step, and
72//! count instead of storing them, and [`BucketedAxis`] adds a static bucket
73//! index — built at compile time by [`bucket_index`] — that bounds the local
74//! scan of a long irregular axis. A surface that names strategies is built with
75//! [`BilinearSurface::from_axes`].
76//!
77//! A surface hands out its axes: [`BilinearSurface::x`] and
78//! [`BilinearSurface::y`] return each axis with its strategy, so generic code
79//! bounded on [`AxisLookup`] (or [`KnotArray`] for the stored strategies) can
80//! read domain bounds, knots, and cost constants from any surface.
81//!
82//! [`AxisLookup`] and [`KnotArray`] are sealed: the four strategies above are
83//! the only implementations, each validating its own invariants in a `const fn`
84//! constructor. Selection is type-level, so there is no runtime discriminant
85//! and no branch among strategies; a firmware that names one combination
86//! compiles that one. Choose [`LinearAxis`] for a tiny axis when the minimum
87//! auxiliary structure is what matters; [`BinaryAxis`] as the general default;
88//! [`UniformAxis`] when knots are evenly spaced, so the knot arrays can be
89//! dropped and location is constant work; [`BucketedAxis`] for a long
90//! irregular axis when `2*B` extra index bytes buy a smaller local bound.
91//! Every strategy locates the same cell, evaluates the same value, and reports
92//! the same errors — only stored bytes and search work differ.
93//!
94//! **Boundaries.** [`Boundary`] is `Error` or `Clamp`. [`BoundaryPolicy`]
95//! selects one of those independently for X-below, X-above, Y-below, and
96//! Y-above; every side defaults to `Error`. [`SurfaceError`] has exactly four
97//! variants, one per side, each carrying the supplied coordinate and the
98//! applicable bound. `Clamp` substitutes the nearest endpoint knot; nothing is
99//! ever extrapolated.
100//!
101//! **Precedence.** X is resolved before Y. When both coordinates leave the
102//! domain on `Error` sides the X-side error is reported; when X clamps, Y is
103//! still resolved under its own selections.
104//!
105//! **Rounding.** Each scalar segment computes the exact rational
106//! `(y0 * (span - offset) + y1 * offset) / span` in `i64` and rounds to
107//! nearest, with exact half-way values rounded away from zero. One private
108//! helper implements that rule and every interpolated value passes through it.
109//!
110//! **Order.** Bilinear evaluation interpolates along X on the lower-Y row,
111//! along X on the upper-Y row, and then along Y between those two
112//! already-rounded values. Because every step rounds, that order is observable
113//! and normative; see the locked fixture under [Evaluation
114//! contract](#evaluation-contract) above.
115//!
116//! **Panics.** [`BilinearSurface::evaluate`] cannot panic for any surface
117//! that can exist: every index it computes is bounded by the located cell's
118//! invariant, its one division is by a validated positive span, and its
119//! arithmetic cannot overflow (see below). That is a structural argument,
120//! exercised by the exhaustive conformance sweeps — not a claim that the
121//! compiled artifact contains no panic branches: the compiler keeps the
122//! bounds checks it cannot prove dead, and the repository's committed
123//! per-target emitted-instruction snapshots record exactly what is
124//! generated. The panicking paths in this crate's API are confined to the
125//! `const fn` constructors and to index accessors with documented `# Panics`
126//! sections (the knot accessors and [`AxisLookup::search`]); in
127//! `static`/`const` position those assertions are compile errors, and at
128//! runtime they fire only on a violated caller precondition, never on data.
129//!
130//! **Cross-target determinism.** Evaluation is integer-only with one fixed
131//! rounding rule, so a given surface and coordinate pair produces the
132//! bit-identical `i32` on every supported target — host, ARM, and RISC-V.
133//! There is no floating-point rounding mode, target-width, or build-profile
134//! dependence to vary the result. Floating point never participates: the
135//! crate declares no features, and any future hardware-specific fast path
136//! (for example an FPU path on Cortex-M4F/M7) would have to arrive as an
137//! off-by-default feature gate that leaves default-build results untouched,
138//! with its determinism trade-offs documented — it is excluded today
139//! precisely because per-target float rounding would break this guarantee.
140//!
141//! # No arithmetic-overflow variant
142//!
143//! [`SurfaceError`] has no overflow variant because none is reachable. Both
144//! segment weights are nonnegative and sum to `span <= 65_535`, so the `i64`
145//! numerator has magnitude below `2^31 * 65_535 < 2^47`. The rounded quotient
146//! lies in the closed hull of the two endpoints, so it fits `i32`; the Y step
147//! receives two such values and returns one from the hull of the four corners.
148//! This holds for knots at `0` and `u16::MAX` and for grids containing
149//! `i32::MIN` and `i32::MAX`, and the conformance suite asserts it on those
150//! extremes against an `i128` reference.
151//!
152//! # Statelessness
153//!
154//! Evaluation is a pure function of the handle and the two coordinates. There
155//! is no reset, warm-up, cache, clock, I/O, persistence, hardware, or
156//! lifecycle behaviour, and evaluating mutates and allocates nothing.
157//!
158//! # Runtime guarantees and independence
159//!
160//! `#![no_std]` is unconditional. It is not relaxed by any feature; the crate
161//! declares none. The implementation is core-only: no allocator, no `std`, and
162//! no `unsafe`. The package has no runtime, development, or build dependency.
163//!
164//! In particular, **this crate has no dependency of any kind on `ph-curves`**:
165//! not direct, transitive, optional, feature-gated, target-specific,
166//! development, build, path, or Git. Its scalar arithmetic is a private helper
167//! specified and verified in this crate. Shared arithmetic is a separate
168//! post-v0.1 decision.
169//!
170//! Those are mechanically checked by the repository's local gate rather than
171//! merely asserted: the runtime is built with a nightly `-Z build-std=core`
172//! core-only sysroot on ARM (`thumbv7em-none-eabi`) and RISC-V
173//! (`riscv32imac-unknown-none-elf`), so an allocator reference cannot link;
174//! the manifest, lockfile, and `cargo metadata` are checked for the banned
175//! name; and the packaged artifact's own doctests and a downstream `#![no_std]`
176//! consumer are compiled from the unpacked package. Every other Rust target,
177//! Xtensa included, is unproven and unclaimed.
178//!
179//! # Examples
180//!
181//! The Cargo examples listed under Status are the firmware teaching path.
182//! The two maps below remain the packaged `ELEVATION` and `CORRECTION`
183//! fixtures. They demonstrate generic mechanics only — nonuniform axes,
184//! mixed-sign values, a boundary policy, and the rounding rule on
185//! hand-computable points — and make no claim about any device, vendor,
186//! sensor, calibration, or measurement accuracy.
187//!
188//! A mixed-sign elevation map holding its last column past the far X edge:
189//!
190//! ```
191//! use ph_surfaces::{BilinearSurface, Boundary, BoundaryPolicy, SurfaceError};
192//!
193//! static ELEVATION_X: [u16; 5] = [0, 25, 60, 100, 180];
194//! static ELEVATION_Y: [u16; 4] = [0, 40, 90, 150];
195//! static ELEVATION_VALUES: [[i32; 5]; 4] = [
196//!     [-120, -35, 40, 15, -60],
197//!     [-80, 10, 95, 60, -20],
198//!     [-15, 55, 130, 88, 5],
199//!     [-40, 20, 70, 110, 45],
200//! ];
201//! static ELEVATION: BilinearSurface<5, 4> =
202//!     BilinearSurface::new(&ELEVATION_X, &ELEVATION_Y, &ELEVATION_VALUES)
203//!         .with_policy(BoundaryPolicy::new().with_x_above(Boundary::Clamp));
204//!
205//! assert_eq!(ELEVATION.evaluate(60, 90), Ok(130)); // a declared knot
206//! assert_eq!(ELEVATION.evaluate(10, 20), Ok(-65)); // rows -86, -44; midway
207//! assert_eq!(ELEVATION.evaluate(75, 100), Ok(109)); // rows 114, 85; 114 - 29*10/60
208//! assert_eq!(ELEVATION.evaluate(140, 60), Ok(31)); // rows 20, 47; 20 + 27*20/50
209//! assert_eq!(ELEVATION.evaluate(u16::MAX, 0), Ok(-60)); // X clamps to 180
210//! assert_eq!(
211//!     ELEVATION.evaluate(500, 151),
212//!     Err(SurfaceError::YAbove { coordinate: 151, bound: 150 })
213//! );
214//! ```
215//!
216//! An asymmetric process-correction map holding its last load row above its
217//! range:
218//!
219//! ```
220//! use ph_surfaces::{BilinearSurface, Boundary, BoundaryPolicy, SurfaceError};
221//!
222//! static CORRECTION_X: [u16; 4] = [40, 55, 90, 200];
223//! static CORRECTION_Y: [u16; 5] = [0, 10, 25, 70, 120];
224//! static CORRECTION_VALUES: [[i32; 4]; 5] = [
225//!     [125, 80, -15, -140],
226//!     [90, 41, -33, -170],
227//!     [30, -7, -61, -205],
228//!     [-48, -95, -150, -260],
229//!     [-110, -142, -199, -333],
230//! ];
231//! static CORRECTION: BilinearSurface<4, 5> =
232//!     BilinearSurface::new(&CORRECTION_X, &CORRECTION_Y, &CORRECTION_VALUES)
233//!         .with_policy(BoundaryPolicy::new().with_y_above(Boundary::Clamp));
234//!
235//! assert_eq!(CORRECTION.evaluate(47, 5), Ok(86)); // rows 104, 67; 85.5 -> 86
236//! assert_eq!(CORRECTION.evaluate(145, 100), Ok(-242));
237//! assert_eq!(CORRECTION.evaluate(60, 40), Ok(-44));
238//! assert_eq!(CORRECTION.evaluate(90, u16::MAX), Ok(-199)); // Y clamps to 120
239//! assert_eq!(
240//!     CORRECTION.evaluate(39, 500),
241//!     Err(SurfaceError::XBelow { coordinate: 39, bound: 40 })
242//! );
243//! ```
244//!
245//! A surface whose two axes choose different lookup strategies. The X axis is
246//! irregular, so it keeps its knots and buys a bounded local scan with an
247//! eight-entry bucket index; the Y axis is evenly spaced, so it describes its
248//! knots by origin and step and stores none of them. Naming strategies changes
249//! stored bytes and search work and nothing else — the default all-binary
250//! surface over the same tables answers identically:
251//!
252//! ```
253//! use ph_surfaces::{
254//!     AxisLookup, BilinearSurface, BinaryAxis, BucketedAxis, UniformAxis,
255//!     bucket_index, max_local_comparisons,
256//! };
257//!
258//! static X: [u16; 17] = [
259//!     0, 100, 210, 300, 405, 500, 610, 700, 805, 900, 1_010, 1_100, 1_205,
260//!     1_300, 1_410, 1_500, 1_600,
261//! ];
262//! static X_INDEX: [u16; 8] = bucket_index(&X);
263//! static Y: [u16; 9] = [0, 200, 400, 600, 800, 1_000, 1_200, 1_400, 1_600];
264//! static VALUES: [[i32; 17]; 9] = [[0; 17]; 9];
265//!
266//! static MIXED: BilinearSurface<17, 9, BucketedAxis<17, 8>, UniformAxis<9, 0, 200>> =
267//!     BilinearSurface::from_axes(
268//!         BucketedAxis::new(&X, &X_INDEX),
269//!         UniformAxis::new(),
270//!         &VALUES,
271//!     );
272//! static DEFAULT: BilinearSurface<17, 9> = BilinearSurface::new(&X, &Y, &VALUES);
273//!
274//! assert_eq!(MIXED.evaluate(610, 400), DEFAULT.evaluate(610, 400));
275//! assert_eq!(MIXED.y_knot(8), 1_600); // described, not stored
276//! assert_eq!(max_local_comparisons(&X, &X_INDEX), 3);
277//! assert_eq!(<BinaryAxis<17>>::MAX_SEARCH_COMPARISONS, 5);
278//! ```
279//!
280//! # Resource accounting
281//!
282//! A [`BilinearSurface<NX, NY>`](BilinearSurface) references static tables
283//! whose element payload is exactly [`BilinearSurface::PAYLOAD_BYTES`]:
284//!
285//! ```text
286//! X::KNOT_BYTES + X::INDEX_BYTES + Y::KNOT_BYTES + Y::INDEX_BYTES + VALUE_BYTES
287//! ```
288//!
289//! with [`BilinearSurface::VALUE_BYTES`] equal to `4*NX*NY`. For the default
290//! binary pairing that is `2*NX + 2*NY + 4*NX*NY` bytes. That figure is exact
291//! and target-independent, and it is **only** the referenced element payload.
292//! It is not total RAM, flash, binary, or linker cost: alignment, section
293//! placement, code, and stack are outside it.
294//!
295//! Naming a strategy changes the two axis terms and nothing else. Each axis
296//! term is stated exactly by its strategy: `2*N` knot bytes and no index for
297//! [`LinearAxis`] and [`BinaryAxis`], nothing at all for [`UniformAxis`], and
298//! `2*N` knot bytes plus `2*B` index bytes for
299//! [`BucketedAxis<N, B>`](BucketedAxis). The same exclusions apply: these are
300//! referenced element bytes, not a total memory cost.
301//!
302//! The handle itself is separate and target-dependent:
303//! [`BilinearSurface::HANDLE_BYTES`] is `size_of` of the handle on the current
304//! target. It always has the value-grid reference and four one-byte boundary
305//! selections. A Uniform axis adds no reference, a Linear or Binary axis adds
306//! one knot-array reference, and a Bucketed axis adds knot-array and
307//! index-array references. The default binary/binary handle is therefore three
308//! thin references plus the policy and alignment padding. For a fixed strategy
309//! pairing it does not grow with `NX` or `NY`.
310//!
311//! Default binary `ELEVATION` 5×4: payload `10 + 8 + 80 = 98`, three
312//! interpolations and four grid reads on success, in-domain searches
313//! `2 + ceil(log2(5))` and `2 + ceil(log2(4))` comparisons:
314//!
315//! ```
316//! use ph_surfaces::{AxisLookup, BilinearSurface, BinaryAxis};
317//!
318//! assert_eq!(BilinearSurface::<5, 4>::VALUE_BYTES, 80);
319//! assert_eq!(BilinearSurface::<5, 4>::PAYLOAD_BYTES, 98);
320//! assert_eq!(BilinearSurface::<5, 4>::SUCCESS_INTERPOLATIONS, 3);
321//! assert_eq!(BilinearSurface::<5, 4>::SUCCESS_GRID_READS, 4);
322//! assert_eq!(<BinaryAxis<5>>::MAX_SEARCH_COMPARISONS, 3);
323//! assert_eq!(<BinaryAxis<4>>::MAX_SEARCH_COMPARISONS, 2);
324//! assert_eq!(
325//!     BilinearSurface::<5, 4>::HANDLE_BYTES,
326//!     core::mem::size_of::<BilinearSurface<5, 4>>()
327//! );
328//! ```
329//!
330//! Tiny Linear×Linear 3×2: six X knot bytes, four Y knot bytes, 24 value
331//! bytes, payload 34; searches at most `N - 1` knot comparisons per axis:
332//!
333//! ```
334//! use ph_surfaces::{AxisLookup, BilinearSurface, LinearAxis};
335//!
336//! type Tiny = BilinearSurface<3, 2, LinearAxis<3>, LinearAxis<2>>;
337//! assert_eq!(Tiny::VALUE_BYTES, 24);
338//! assert_eq!(Tiny::PAYLOAD_BYTES, 34);
339//! assert_eq!(<LinearAxis<3>>::MAX_SEARCH_COMPARISONS, 2);
340//! assert_eq!(<LinearAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
341//! assert_eq!(Tiny::SUCCESS_INTERPOLATIONS, 3);
342//! assert_eq!(Tiny::SUCCESS_GRID_READS, 4);
343//! ```
344//!
345//! Mixed [`BucketedAxis<17, 8>`](BucketedAxis) ×
346//! [`UniformAxis<9, 0, 200>`](UniformAxis): X knots+index `34 + 16`, Y knots
347//! 0, grid 612, payload 662. The concrete bucket index bounds X at 3 knot
348//! comparisons rather than Binary's 5; Uniform uses none. Including endpoint
349//! comparisons, that is 7 rather than 13 for Binary×Binary, while the
350//! referenced payload is 662 rather than 664 bytes:
351//!
352//! ```
353//! use ph_surfaces::{
354//!     AxisLookup, BilinearSurface, BinaryAxis, BucketedAxis, UniformAxis,
355//!     bucket_index, max_local_comparisons,
356//! };
357//!
358//! static X: [u16; 17] = [
359//!     0, 100, 210, 300, 405, 500, 610, 700, 805, 900, 1_010, 1_100, 1_205,
360//!     1_300, 1_410, 1_500, 1_600,
361//! ];
362//! static X_INDEX: [u16; 8] = bucket_index(&X);
363//! type Mixed = BilinearSurface<17, 9, BucketedAxis<17, 8>, UniformAxis<9, 0, 200>>;
364//! type AllBinary = BilinearSurface<17, 9>;
365//! assert_eq!(<BucketedAxis<17, 8>>::KNOT_BYTES, 34);
366//! assert_eq!(<BucketedAxis<17, 8>>::INDEX_BYTES, 16);
367//! assert_eq!(max_local_comparisons(&X, &X_INDEX), 3);
368//! assert_eq!(<BinaryAxis<17>>::MAX_SEARCH_COMPARISONS, 5);
369//! assert_eq!(<UniformAxis<9, 0, 200>>::KNOT_BYTES, 0);
370//! assert_eq!(<UniformAxis<9, 0, 200>>::MAX_SEARCH_COMPARISONS, 0);
371//! assert_eq!(Mixed::VALUE_BYTES, 612);
372//! assert_eq!(Mixed::PAYLOAD_BYTES, 662);
373//! assert_eq!(AllBinary::PAYLOAD_BYTES, 664);
374//! assert_eq!(Mixed::SUCCESS_INTERPOLATIONS, 3);
375//! assert_eq!(Mixed::SUCCESS_GRID_READS, 4);
376//! ```
377//!
378//! # Evaluation cost
379//!
380//! [`BilinearSurface::evaluate`] performs, in the worst case, two axis
381//! searches and [`BilinearSurface::SUCCESS_INTERPOLATIONS`] scalar
382//! interpolations. Each in-domain axis search is two endpoint comparisons plus
383//! the search work of that axis's strategy; a clamped coordinate costs one or
384//! two comparisons and no probes (the endpoint path, not a search); a rejected
385//! evaluation returns before any interpolation or
386//! [`BilinearSurface::SUCCESS_GRID_READS`] grid reads, and a rejected X also
387//! skips the Y search. Exactly four value-grid elements are read on success.
388//! The grid is never scanned.
389//!
390//! The per-strategy search work, in knot comparisons, is
391//! [`AxisLookup::MAX_SEARCH_COMPARISONS`]: exactly `ceil(log2(N))` for the
392//! default [`BinaryAxis`], at most `N - 1` for [`LinearAxis`], none at all for
393//! [`UniformAxis`] — which locates by one subtraction and one division — and,
394//! for [`BucketedAxis`], one bucket read plus a local scan bounded by
395//! [`max_local_comparisons`] for that axis's knots and index. Raising a bucket
396//! count to a multiple of itself splits buckets rather than moving their
397//! boundaries, so that figure never increases.
398//!
399//! That is a statement of operation structure, derived from the
400//! implementation and asserted by its tests. It is not a cycle count or a
401//! WCET figure: no timing has been measured, and none is claimed.
402//!
403//! # Scope
404//!
405//! This crate owns static multidimensional mapping mechanics: shape and
406//! invariant validation, axis location, explicit domain policies, deterministic
407//! integer interpolation, and truthful resource accounting.
408//!
409//! It does not own hardware access, sensor configuration, sampling, clocks,
410//! persistence, calibration discovery, fault or application policy, device
411//! lifecycle, vendor catalogs, or total measurement accuracy.
412//!
413//! # Not in v0.1
414//!
415//! Explicitly outside this version: a dependency on `ph-curves` or extraction
416//! of a shared arithmetic crate; inverse lookup or solving for either axis;
417//! other dimensions, axis widths, or output types; scattered points, irregular
418//! meshes, bicubic interpolation, extrapolation, or fitting; dynamic or
419//! runtime-loaded grids, mutation, caching, allocation, `unsafe`, or floating
420//! point; runtime metadata, units, or provenance; host generation or CLI
421//! tooling; runtime-selectable strategies or runtime-generated indexes; and a
422//! direct coordinate-to-cell LUT before a concrete consumer supplies its
423//! coordinate domain and latency bound, measurements showing Bucketed misses
424//! that bound on a named target/profile, an adequate static-data budget, and a
425//! reproducible generation and validation plan.
426
427#![no_std]
428#![forbid(unsafe_code)]
429#![deny(missing_docs)]
430#![deny(clippy::correctness)]
431#![deny(
432    clippy::std_instead_of_core,
433    clippy::std_instead_of_alloc,
434    clippy::alloc_instead_of_core
435)]
436
437mod axis;
438mod boundary;
439mod error;
440mod evaluate;
441mod interp;
442mod lookup;
443mod surface;
444
445pub use axis::{
446    AxisLookup, BinaryAxis, BucketedAxis, KnotArray, LinearAxis, UniformAxis, bucket_index,
447    max_local_comparisons,
448};
449pub use boundary::{Boundary, BoundaryPolicy};
450pub use error::SurfaceError;
451pub use surface::BilinearSurface;
452
453/// Compiles every code block in the packaged `README.md` as a doctest, so the
454/// README cannot drift from the API it documents. Present only under
455/// `cfg(doctest)`; it adds nothing to the built crate or its rustdoc.
456#[cfg(doctest)]
457mod readme_doctests {
458    #![doc = include_str!("../README.md")]
459}
460
461#[cfg(test)]
462mod tests {
463    #[test]
464    fn crate_links_on_core_only_types() {
465        let none: Option<u8> = None;
466        assert!(none.is_none());
467    }
468}