Skip to main content

firmware_cost_budget/
firmware_cost_budget.rs

1//! Exact firmware resource-budget comparisons from the public const cost API.
2//!
3//! Three declared shapes, each split into referenced static payload, handle
4//! bytes, bounded operation structure, and target-dependent measurement.
5//! Comparison counts are not cycles or WCET. Payload is not total RAM, flash,
6//! or linked binary size. `HANDLE_BYTES` is `size_of` on this host and must be
7//! re-read on the firmware target. See the usage guide §10–11 and the
8//! strategy guide (neither is packaged):
9//! <https://github.com/photon-circus/ph-surfaces/blob/v0.1.0/docs/usage-guide.md>,
10//! <https://github.com/photon-circus/ph-surfaces/blob/v0.1.0/docs/choosing-a-strategy.md>.
11//!
12//! Host `main` is an assertion harness. Declarations are `static` and
13//! `core`-compatible.
14
15use ph_surfaces::{
16    AxisLookup, BilinearSurface, BinaryAxis, BucketedAxis, LinearAxis, UniformAxis, bucket_index,
17    max_local_comparisons,
18};
19
20fn tiny_linear_linear() {
21    // 3×2 firmware table. Linear is the "tiny axis" starting point; Binary
22    // stores the same knots and, on this shape, the same comparison bound.
23    static X: [u16; 3] = [0, 10, 20];
24    static Y: [u16; 2] = [0, 100];
25    static VALUES: [[i32; 3]; 2] = [[0, 1, 2], [10, 11, 12]];
26
27    type TinyLinear = BilinearSurface<3, 2, LinearAxis<3>, LinearAxis<2>>;
28    type TinyBinary = BilinearSurface<3, 2>;
29
30    static LINEAR: TinyLinear =
31        BilinearSurface::from_axes(LinearAxis::new(&X), LinearAxis::new(&Y), &VALUES);
32    static BINARY: TinyBinary = BilinearSurface::new(&X, &Y, &VALUES);
33
34    assert_eq!(LINEAR.evaluate(10, 100), Ok(11));
35    assert_eq!(LINEAR.evaluate(10, 100), BINARY.evaluate(10, 100));
36
37    // Referenced payload: 4*3*2 values + 2*3 + 2*2 knots = 24 + 10 = 34.
38    assert_eq!(TinyLinear::VALUE_BYTES, 24);
39    assert_eq!(TinyLinear::PAYLOAD_BYTES, 34);
40    assert_eq!(TinyBinary::PAYLOAD_BYTES, 34);
41    assert_eq!(TinyLinear::HANDLE_BYTES, core::mem::size_of::<TinyLinear>());
42
43    // Work: four successful endpoint comparisons plus (3-1)+(2-1) = 3 search
44    // comparisons = 7 knot comparisons. Binary is ceil(log2(3))+ceil(log2(2))
45    // = 2+1 = 3 search comparisons as well. Choosing Linear vs Binary on this
46    // shape requires target code/timing evidence, not a universal threshold.
47    assert_eq!(<LinearAxis<3>>::MAX_SEARCH_COMPARISONS, 2);
48    assert_eq!(<LinearAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
49    assert_eq!(<BinaryAxis<3>>::MAX_SEARCH_COMPARISONS, 2);
50    assert_eq!(<BinaryAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
51    assert_eq!(TinyLinear::SUCCESS_INTERPOLATIONS, 3);
52    assert_eq!(TinyLinear::SUCCESS_GRID_READS, 4);
53}
54
55fn uniform_uniform_17x9() {
56    // Both axes are exact arithmetic progressions, so Uniform stores no knots.
57    // Location is a subtraction and a division by a compile-time STEP — zero
58    // knot comparisons, not zero cycles.
59    type UniformPair = BilinearSurface<17, 9, UniformAxis<17, 0, 100>, UniformAxis<9, 0, 200>>;
60    type AllBinary = BilinearSurface<17, 9>;
61
62    static VALUES: [[i32; 17]; 9] = [[0; 17]; 9];
63    static SURFACE: UniformPair =
64        BilinearSurface::from_axes(UniformAxis::new(), UniformAxis::new(), &VALUES);
65
66    assert_eq!(SURFACE.evaluate(100, 200), Ok(0));
67    assert_eq!(SURFACE.x_knot(16), 1_600);
68    assert_eq!(SURFACE.y_knot(8), 1_600);
69
70    assert_eq!(UniformPair::VALUE_BYTES, 612);
71    assert_eq!(UniformPair::PAYLOAD_BYTES, 612);
72    assert_eq!(AllBinary::PAYLOAD_BYTES, 664);
73    assert_eq!(UniformPair::PAYLOAD_BYTES + 52, AllBinary::PAYLOAD_BYTES);
74    assert_eq!(<UniformAxis<17, 0, 100>>::KNOT_BYTES, 0);
75    assert_eq!(<UniformAxis<17, 0, 100>>::MAX_SEARCH_COMPARISONS, 0);
76    assert_eq!(<UniformAxis<9, 0, 200>>::MAX_SEARCH_COMPARISONS, 0);
77    assert_eq!(UniformPair::SUCCESS_INTERPOLATIONS, 3);
78    assert_eq!(UniformPair::SUCCESS_GRID_READS, 4);
79    assert_eq!(
80        UniformPair::HANDLE_BYTES,
81        core::mem::size_of::<UniformPair>()
82    );
83}
84
85fn mixed_bucketed_uniform_17x9() {
86    static X: [u16; 17] = [
87        0, 100, 210, 300, 405, 500, 610, 700, 805, 900, 1_010, 1_100, 1_205, 1_300, 1_410, 1_500,
88        1_600,
89    ];
90    static X_INDEX: [u16; 8] = bucket_index(&X);
91    static VALUES: [[i32; 17]; 9] = [[0; 17]; 9];
92
93    type Mixed = BilinearSurface<17, 9, BucketedAxis<17, 8>, UniformAxis<9, 0, 200>>;
94    type AllBinary = BilinearSurface<17, 9>;
95
96    static MIXED: Mixed =
97        BilinearSurface::from_axes(BucketedAxis::new(&X, &X_INDEX), UniformAxis::new(), &VALUES);
98
99    assert_eq!(MIXED.evaluate(1_600, 1_600), Ok(0));
100
101    // Payload: 612 grid + 34 X knots + 16 X index + 0 Y = 662. Binary is 664.
102    assert_eq!(Mixed::VALUE_BYTES, 612);
103    assert_eq!(<BucketedAxis<17, 8>>::KNOT_BYTES, 34);
104    assert_eq!(<BucketedAxis<17, 8>>::INDEX_BYTES, 16);
105    assert_eq!(<UniformAxis<9, 0, 200>>::KNOT_BYTES, 0);
106    assert_eq!(Mixed::PAYLOAD_BYTES, 662);
107    assert_eq!(AllBinary::PAYLOAD_BYTES, 664);
108    assert_eq!(max_local_comparisons(&X, &X_INDEX), 3);
109
110    // Work: four endpoint comparisons, plus at most 3 X local comparisons and
111    // 0 Y comparisons = 7 knot comparisons, versus 4 + 5 + 4 = 13 for
112    // Binary/Binary. Each Bucketed search also reads one bucket and maps the
113    // coordinate arithmetically; that is not included in the comparison count
114    // and is not a cycle count.
115    assert_eq!(<BinaryAxis<17>>::MAX_SEARCH_COMPARISONS, 5);
116    assert_eq!(<BinaryAxis<9>>::MAX_SEARCH_COMPARISONS, 4);
117    assert_eq!(<UniformAxis<9, 0, 200>>::MAX_SEARCH_COMPARISONS, 0);
118    assert_eq!(Mixed::SUCCESS_INTERPOLATIONS, 3);
119    assert_eq!(Mixed::SUCCESS_GRID_READS, 4);
120    assert_eq!(Mixed::HANDLE_BYTES, core::mem::size_of::<Mixed>());
121}
122
123fn main() {
124    tiny_linear_linear();
125    uniform_uniform_17x9();
126    mixed_bucketed_uniform_17x9();
127}