ph-surfaces 0.1.0

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

#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::correctness)]
#![deny(
    clippy::std_instead_of_core,
    clippy::std_instead_of_alloc,
    clippy::alloc_instead_of_core
)]

mod axis;
mod boundary;
mod error;
mod evaluate;
mod interp;
mod lookup;
mod surface;

pub use axis::{
    AxisLookup, BinaryAxis, BucketedAxis, KnotArray, LinearAxis, UniformAxis, bucket_index,
    max_local_comparisons,
};
pub use boundary::{Boundary, BoundaryPolicy};
pub use error::SurfaceError;
pub use surface::BilinearSurface;

/// Compiles every code block in the packaged `README.md` as a doctest, so the
/// README cannot drift from the API it documents. Present only under
/// `cfg(doctest)`; it adds nothing to the built crate or its rustdoc.
#[cfg(doctest)]
mod readme_doctests {
    #![doc = include_str!("../README.md")]
}

#[cfg(test)]
mod tests {
    #[test]
    fn crate_links_on_core_only_types() {
        let none: Option<u8> = None;
        assert!(none.is_none());
    }
}