From b2f8eb84c9d6f1692b417ae915bc1afeb62bbbce Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Fri, 28 Aug 2026 17:24:37 -0700 Subject: [PATCH 1/4] Add a header-only SegmentedNumber type for piecewise-quantized physical values. Keep existing TieredInt/FixedPoint/Exponential wire formats unchanged and compile the packed rank from an independent curve layout. Co-authored-by: Cursor --- README.md | 80 +- ae-numeric/details/segmented_compiler.h | 748 ++++++++++++++++++ ae-numeric/details/segmented_curves.h | 434 ++++++++++ ae-numeric/details/segmented_format.h | 174 ++++ .../details/segmented_formula_backend.h | 322 ++++++++ ae-numeric/details/segmented_lookup_backend.h | 104 +++ ae-numeric/details/segmented_math.h | 283 +++++++ ae-numeric/integer_math.h | 15 + ae-numeric/segmented_number.h | 379 +++++++++ .../segmented_number_floating_runtime.h | 84 ++ ae-numeric/segmented_number_wire_io.h | 47 ++ tests/CMakeLists.txt | 64 +- .../segmented_duplicate_boundary.cpp | 42 + .../segmented_duplicate_runtime_value.cpp | 38 + .../segmented_exponential_non_positive.cpp | 38 + tests/compile-fail/segmented_gap.cpp | 40 + .../segmented_impossible_continuous_step.cpp | 39 + .../segmented_impossible_error.cpp | 39 + .../segmented_invalid_wire_bytes.cpp | 38 + tests/compile-fail/segmented_overlap.cpp | 40 + .../segmented_runtime_rep_too_small.cpp | 38 + .../segmented_too_many_one_byte_codes.cpp | 38 + tests/footprint/segmented_footprint.cpp | 100 +++ tests/main.cpp | 12 + tests/segmented_test_formats.h | 178 +++++ tests/test-integer-math.cpp | 14 + tests/test-segmented-number-core.cpp | 188 +++++ ...test-segmented-number-floating-runtime.cpp | 95 +++ tests/test-segmented-number-formats.cpp | 262 ++++++ .../test-segmented-number-formula-lookup.cpp | 104 +++ tests/test-segmented-number-size.cpp | 65 ++ tests/test-segmented-number-wire.cpp | 113 +++ 32 files changed, 4250 insertions(+), 5 deletions(-) create mode 100644 ae-numeric/details/segmented_compiler.h create mode 100644 ae-numeric/details/segmented_curves.h create mode 100644 ae-numeric/details/segmented_format.h create mode 100644 ae-numeric/details/segmented_formula_backend.h create mode 100644 ae-numeric/details/segmented_lookup_backend.h create mode 100644 ae-numeric/details/segmented_math.h create mode 100644 ae-numeric/segmented_number.h create mode 100644 ae-numeric/segmented_number_floating_runtime.h create mode 100644 ae-numeric/segmented_number_wire_io.h create mode 100644 tests/compile-fail/segmented_duplicate_boundary.cpp create mode 100644 tests/compile-fail/segmented_duplicate_runtime_value.cpp create mode 100644 tests/compile-fail/segmented_exponential_non_positive.cpp create mode 100644 tests/compile-fail/segmented_gap.cpp create mode 100644 tests/compile-fail/segmented_impossible_continuous_step.cpp create mode 100644 tests/compile-fail/segmented_impossible_error.cpp create mode 100644 tests/compile-fail/segmented_invalid_wire_bytes.cpp create mode 100644 tests/compile-fail/segmented_overlap.cpp create mode 100644 tests/compile-fail/segmented_runtime_rep_too_small.cpp create mode 100644 tests/compile-fail/segmented_too_many_one_byte_codes.cpp create mode 100644 tests/footprint/segmented_footprint.cpp create mode 100644 tests/segmented_test_formats.h create mode 100644 tests/test-segmented-number-core.cpp create mode 100644 tests/test-segmented-number-floating-runtime.cpp create mode 100644 tests/test-segmented-number-formats.cpp create mode 100644 tests/test-segmented-number-formula-lookup.cpp create mode 100644 tests/test-segmented-number-size.cpp create mode 100644 tests/test-segmented-number-wire.cpp diff --git a/README.md b/README.md index 77aed12..4495fb6 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ They are used across the Æthernet C++ client to represent durations, counters, 6. [Ostream IO](#ostream-io) 7. [Wire IO](#wire-io) 8. [Combined Types](#combined-types) -9. [Integration Notes](#integration-notes) -10. [Running Tests](#running-tests) +9. [SegmentedNumber](#segmentednumber) +10. [Integration Notes](#integration-notes) +11. [Running Tests](#running-tests) --- @@ -297,12 +298,87 @@ This is a linear scale: every raw step is one millisecond. Use `Exponential` ins --- +## SegmentedNumber + +`SegmentedNumber` is a header-only piecewise quantized number. The object stores only the physical runtime value. A dense packed rank is computed when encoding and is serialized through a compiled `uint8_t` or `TieredInt` wire type. + +The description splits four layers: + +1. runtime representation (`runtime::Fixed` or opt-in `runtime::Floating`); +2. mathematical curves of representable values; +3. assignment of those codes to wire lengths 1/2/4/8 bytes; +4. the serializable packed rank. + +Bounds and steps are written with `ae::Decimal` / `ae::Ratio`, not `double`. + +```cpp +#include +#include + +template +using D = ae::Decimal; + +using Spec = ae::seg::Format< + ae::seg::runtime::Fixed, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout< + ae::seg::GeometricStep< + ae::seg::Range, D<10>>, + ae::seg::Intervals<349>, + ae::seg::StepAtUpper>, + ae::seg::Place>>, + ae::seg::UniformStep< + ae::seg::Range, D<352, -1>>, + ae::seg::Step>, + ae::seg::Place>>, + ae::seg::GeometricStep< + ae::seg::Range, D<125>>, + ae::seg::Intervals<419>, + ae::seg::StepAtLower>, + ae::seg::Place>>>>; + +using Temperature = ae::seg::Compile; + +static_assert(sizeof(Temperature) == sizeof(Temperature::runtime_type)); +static_assert(Temperature::kCodeCount == 1021); +static_assert(Temperature::kMaxWireBytes == 2); +``` + +`Compile` is `SegmentedNumber`. The object does not store the wire rank. Encode with `TryEncode` / `TryFromRuntime`; out-of-range input is rejected unless `Saturating` is used. Comparisons use the runtime value, never the packed rank: rank order need not follow physical order (the temperature window uses 1-byte codes in the middle and 2-byte codes on both tails). + +Curve primitives: `UniformStep`, `UniformValues`, `ExponentialValues`, `GeometricStep`, `LinearStepRamp`. Allocation helpers: `Intervals`, `FillTier`, `MinimumIntervals`, `AutoSplit`, and `ContinuousExponential` with `WireCuts` / `OptimizeCuts`. + +Two compute backends: + +* `compute::Formula` (default) — small per-segment coefficients, no per-code table, no runtime `float`/`double` for `Fixed` runtime. Segment selection is O(S). Uniform and linear-ramp paths are O(1) (integer square root for the ramp). Exponential and geometric decode use integer exponentiation-by-squaring of a compiled ratio (O(log n) multiplies). Encode of those curves binary-searches the selected segment and checks neighboring codes. Serialization is O(1), at most 8 bytes, heap = 0; +* `compute::Lookup` — consteval decoded-raw table, O(1) decode and O(log N) encode, used as a Formula oracle. Flash/data is O(N). + +Shared physical endpoints are encoded once. The segment with the smaller wire size owns the joint; if the sizes match, the previous physical segment owns it. Unused packed ranks deserialize with `bytes_read == 0`. + +Floating runtime is opt-in and does not change the wire ABI: + +```cpp +#include + +using FloatSpec = ae::seg::Format< + ae::seg::runtime::Floating, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + typename Spec::layout_type>; +``` + +Release footprint binaries (section GC, volatile sinks) are the `footprint-*` / `segmented-footprint` targets in `tests/`. + +--- + ## Integration Notes * Header-only numeric types. * C++20. * Deterministic integer runtime paths for embedded use. * Optional floating runtime support for `Exponential` is isolated in `ae-numeric/exponential_floating_runtime.h`. +* Optional floating runtime support for `SegmentedNumber` is isolated in `ae-numeric/segmented_number_floating_runtime.h`. * Designed for low-overhead serialization on MCUs and constrained networks. --- diff --git a/ae-numeric/details/segmented_compiler.h b/ae-numeric/details/segmented_compiler.h new file mode 100644 index 0000000..20ca55a --- /dev/null +++ b/ae-numeric/details/segmented_compiler.h @@ -0,0 +1,748 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_COMPILER_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_COMPILER_H_ + +#include +#include +#include +#include +#include + +#include + +#include "ae-numeric/details/segmented_curves.h" +#include "ae-numeric/details/segmented_format.h" +#include "ae-numeric/details/segmented_math.h" +#include "ae-numeric/fixed_math.h" +#include "ae-numeric/fixed_point.h" +#include "ae-numeric/tiered_int.h" + +namespace ae::seg::segmented_compiler_internal { + +using segmented_curves_internal::CurveDraft; +using segmented_curves_internal::FlattenLayout; +using segmented_curves_internal::kMaxDrafts; +using segmented_curves_internal::StepMode; +using segmented_math_internal::AutoSplitTwoExp; +using segmented_math_internal::ExpRatio; +using segmented_math_internal::GeomSum; +using segmented_math_internal::MinRampIntervals; +using segmented_math_internal::MixHash; +using segmented_math_internal::OptimizeContinuousExp; +using segmented_math_internal::SegmentedSpecError; +using segmented_math_internal::SolveQForGeomSum; +using segmented_math_internal::ThreeTierMaxU8; +using segmented_math_internal::TwoTierMaxU8; + +inline constexpr double kEps = 1.0e-12; + +template +inline constexpr int kMaxBytesTag = -1; +template +inline constexpr int kMaxBytesTag> = static_cast(N); + +template +consteval void TakeMaxBytes(int& v) { + if constexpr (kMaxBytesTag >= 0) { + v = kMaxBytesTag; + } +} + +template +consteval int PolicyMaxBytes(wire::AutoTiered const*) { + int v = 8; + (TakeMaxBytes(v), ...); + return v; +} + +template +consteval int FormatMaxBytes() { + return PolicyMaxBytes(static_cast(nullptr)); +} + +consteval bool NearlyEqual(double a, double b) { + double const s = gcem::abs(a) > gcem::abs(b) ? gcem::abs(a) : gcem::abs(b); + double const tol = kEps * (s > 1.0 ? s : 1.0); + return gcem::abs(a - b) <= tol; +} + +consteval int RoundN(double x) { + if (x < 0.0) { + return static_cast(x - 0.5); + } + return static_cast(x + 0.5); +} + +consteval bool GeomFromUpper(CurveDraft const& d) { + return d.step_mode == StepMode::kUpperExplicit || + d.step_mode == StepMode::kUpperInherit; +} + +consteval double GeomUpperStep(CurveDraft const& d) { + if (d.step_mode == StepMode::kUpperInherit) { + return d.last_step; + } + if (d.specified_step != 0.0) { + return d.specified_step; + } + return d.last_step; +} + +consteval double DecodeMath(CurveDraft const& d, int i) { + if (d.intervals <= 0) { + return d.begin; + } + if (i <= 0) { + return d.begin; + } + if (i >= d.intervals) { + return d.end; + } + if (d.kind == CurveKind::kUniformStep || + d.kind == CurveKind::kUniformValues) { + return d.begin + (d.end - d.begin) * + static_cast(i) / + static_cast(d.intervals); + } + if (d.kind == CurveKind::kExponentialValues) { + return d.begin * gcem::pow(d.r, static_cast(i)); + } + if (d.kind == CurveKind::kGeometricStep) { + if (GeomFromUpper(d)) { + double const se = GeomUpperStep(d); + return d.end - se * GeomSum(d.q, d.intervals - i); + } + return d.begin + d.step0 * GeomSum(d.q, i); + } + return d.begin + static_cast(i) * d.step0 + + d.delta * static_cast(i) * static_cast(i - 1) / 2.0; +} + +consteval double FirstAbsStep(CurveDraft const& d) { + return gcem::abs(DecodeMath(d, 1) - DecodeMath(d, 0)); +} + +consteval double LastAbsStep(CurveDraft const& d) { + return gcem::abs(DecodeMath(d, d.intervals) - DecodeMath(d, d.intervals - 1)); +} + +consteval void AssignOwnership(CurveDraft* d, int n) { + if (n <= 0) { + SegmentedSpecError(); + return; + } + d[0].own_begin = true; + d[n - 1].own_end = true; + for (int i = 0; i < n - 1; ++i) { + if (!NearlyEqual(d[i].end, d[i + 1].begin)) { + if (d[i + 1].begin < d[i].end) { + SegmentedSpecError(); + } else { + SegmentedSpecError(); + } + } + int const bi = d[i].bytes == 0 ? 1 : d[i].bytes; + int const bj = d[i + 1].bytes == 0 ? 1 : d[i + 1].bytes; + if (bi < bj) { + d[i].own_end = true; + d[i + 1].own_begin = false; + } else if (bi > bj) { + d[i].own_end = false; + d[i + 1].own_begin = true; + } else { + d[i].own_end = true; + d[i + 1].own_begin = false; + } + } + if (n == 1) { + d[0].own_end = true; + } +} + +consteval int StoredOf(CurveDraft const& d) { + return d.intervals + (d.own_begin ? 1 : 0) + (d.own_end ? 1 : 0) - 1; +} + +consteval bool HasBytes(CurveDraft const* d, int n, int bytes) { + for (int i = 0; i < n; ++i) { + if (d[i].bytes == bytes || (d[i].is_cont_exp && bytes >= 1)) { + if (d[i].is_cont_exp) { + return bytes == 1 || bytes == 2 || bytes == 4; + } + if (d[i].bytes == bytes) { + return true; + } + } + } + return false; +} + +consteval void ComputeCoeffsKnownN(CurveDraft& d) { + if (d.intervals <= 0) { + return; + } + double const span = d.end - d.begin; + if (d.kind == CurveKind::kUniformStep || + d.kind == CurveKind::kUniformValues) { + d.step0 = span / static_cast(d.intervals); + d.last_step = d.step0; + d.delta = 0.0; + d.r = 1.0; + d.q = 1.0; + return; + } + if (d.kind == CurveKind::kExponentialValues) { + d.r = ExpRatio(d.begin, d.end, d.intervals); + d.step0 = d.begin * (d.r - 1.0); + d.last_step = d.end * (1.0 - 1.0 / d.r); + return; + } + if (d.kind == CurveKind::kGeometricStep) { + if (d.step_mode == StepMode::kLowerExplicit && d.specified_step > 0.0) { + d.step0 = d.specified_step; + d.q = SolveQForGeomSum(d.intervals, span / d.step0); + d.last_step = d.step0 * gcem::pow(d.q, d.intervals - 1); + } else if (d.step_mode == StepMode::kUpperExplicit && + d.specified_step > 0.0) { + d.last_step = d.specified_step; + d.q = SolveQForGeomSum(d.intervals, span / d.last_step); + d.step0 = d.last_step * gcem::pow(d.q, d.intervals - 1); + } else if (d.step_mode == StepMode::kLowerInherit && d.step0 > 0.0) { + d.q = SolveQForGeomSum(d.intervals, span / d.step0); + d.last_step = d.step0 * gcem::pow(d.q, d.intervals - 1); + } else if (d.step_mode == StepMode::kUpperInherit && d.last_step > 0.0) { + d.q = SolveQForGeomSum(d.intervals, span / d.last_step); + d.step0 = d.last_step * gcem::pow(d.q, d.intervals - 1); + } + return; + } + if (d.kind == CurveKind::kLinearStepRamp) { + if (d.step_mode == StepMode::kLowerExplicit) { + d.step0 = d.specified_step; + } + if (d.step_mode == StepMode::kUpperExplicit) { + d.last_step = d.specified_step; + } + if (d.step0 > 0.0 && d.last_step > 0.0) { + double const mean = 2.0 * span / static_cast(d.intervals); + (void)mean; + } + if (d.step0 > 0.0 && d.last_step <= 0.0 && d.intervals > 0) { + d.last_step = 2.0 * span / static_cast(d.intervals) - d.step0; + } + if (d.last_step > 0.0 && d.step0 <= 0.0 && d.intervals > 0 && + d.step_mode != StepMode::kLowerInherit) { + d.step0 = 2.0 * span / static_cast(d.intervals) - d.last_step; + } + if (d.intervals > 1 && d.step0 > 0.0 && d.last_step > 0.0) { + d.delta = (d.last_step - d.step0) / + static_cast(d.intervals - 1); + } + if (d.has_max_err_lower && d.step0 > 2.0 * d.max_err_lower + 1.0e-15) { + SegmentedSpecError(); + } + if (d.has_max_err_upper && d.last_step > 2.0 * d.max_err_upper + 1.0e-12) { + SegmentedSpecError(); + } + } +} + +consteval bool TryInherit(CurveDraft* d, int n) { + bool changed = false; + for (int i = 0; i < n; ++i) { + if (d[i].step_mode == StepMode::kLowerInherit && i > 0) { + if (d[i - 1].last_step > 0.0) { + d[i].step0 = d[i - 1].last_step; + changed = true; + } + } + if (d[i].step_mode == StepMode::kUpperInherit && i + 1 < n) { + double nxt = 0.0; + if (d[i + 1].step0 > 0.0) { + nxt = d[i + 1].step0; + } else if (d[i + 1].intervals > 0 && + (d[i + 1].kind == CurveKind::kUniformValues || + d[i + 1].kind == CurveKind::kUniformStep || + d[i + 1].kind == CurveKind::kExponentialValues)) { + nxt = FirstAbsStep(d[i + 1]); + } + if (nxt > 0.0) { + d[i].last_step = nxt; + changed = true; + } + } + } + return changed; +} + +struct LogicalPlan { + std::array segs{}; + int count = 0; + std::uint32_t n1 = 0; + std::uint32_t n2 = 0; + std::uint32_t n4 = 0; + std::uint32_t n8 = 0; + std::uint32_t code_count = 0; + int max_bytes = 1; + double max_abs = 0.0; + std::uint64_t schema_hash = 0; +}; + +consteval void AssignWire(CurveDraft* d, int n, LogicalPlan& plan) { + std::uint32_t next = 0; + int const order[4] = {1, 2, 4, 8}; + std::uint32_t* counts[4] = {&plan.n1, &plan.n2, &plan.n4, &plan.n8}; + for (int t = 0; t < 4; ++t) { + for (int i = 0; i < n; ++i) { + if (d[i].bytes == order[t]) { + d[i].wire_begin = next; + if (d[i].stored <= 0) { + d[i].stored = StoredOf(d[i]); + d[i].math_first = d[i].own_begin ? 0 : 1; + } else if (!d[i].own_begin && d[i].math_first == 0) { + // FillTier pre-assigns stored but leaves math_first at 0. + d[i].math_first = 1; + } + if (d[i].stored <= 0) { + SegmentedSpecError(); + } + int const last_math = d[i].math_first + d[i].stored - 1; + d[i].phys_begin = DecodeMath(d[i], d[i].math_first); + d[i].phys_end = DecodeMath(d[i], last_math); + next += static_cast(d[i].stored); + *counts[t] += static_cast(d[i].stored); + } + } + } + plan.code_count = next; + if (plan.code_count == 0) { + SegmentedSpecError(); + } +} + +consteval LogicalPlan SplitContExp(LogicalPlan in) { + LogicalPlan out{}; + out.max_bytes = in.max_bytes; + int w = 0; + for (int i = 0; i < in.count; ++i) { + CurveDraft const& s = in.segs[static_cast(i)]; + if (!s.is_cont_exp) { + out.segs[static_cast(w++)] = s; + continue; + } + int const n = s.intervals; + int const a = s.last_1; + int const b = s.last_2; + auto push_slice = [&](int math_lo, int math_hi, int bytes, int stored) { + CurveDraft c = s; + c.is_cont_exp = false; + c.bytes = bytes; + c.math_first = math_lo; + c.own_begin = true; + c.own_end = true; + c.stored = stored; + c.phys_begin = DecodeMath(s, math_lo); + c.phys_end = DecodeMath(s, math_hi); + out.segs[static_cast(w++)] = c; + }; + push_slice(0, a, 1, a + 1); + push_slice(a + 1, b, 2, b - a); + push_slice(b + 1, n, 4, n - b); + } + out.count = w; + AssignWire(out.segs.data(), out.count, out); + return out; +} + +template +consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { + LogicalPlan plan{}; + plan.max_bytes = max_bytes; + int idx = 0; + int as_id = 1; + FlattenLayout::Fill(plan.segs.data(), idx, as_id); + plan.count = idx; + if (plan.count <= 0 || plan.count > kMaxDrafts) { + SegmentedSpecError(); + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (d.bytes > max_bytes && d.bytes != 0) { + SegmentedSpecError(); + } + double const mag = + gcem::abs(d.begin) > gcem::abs(d.end) ? gcem::abs(d.begin) + : gcem::abs(d.end); + if (mag > plan.max_abs) { + plan.max_abs = mag; + } + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (d.autosplit_id != 0 && d.kind == CurveKind::kExponentialValues && + d.intervals < 0) { + int j = i; + while (j < plan.count && + plan.segs[static_cast(j)].autosplit_id == + d.autosplit_id) { + ++j; + } + if (j - i != 2) { + SegmentedSpecError(); + } + CurveDraft& a = plan.segs[static_cast(i)]; + CurveDraft& b = plan.segs[static_cast(i + 1)]; + if (!NearlyEqual(a.end, b.begin)) { + SegmentedSpecError(); + } + auto const sp = AutoSplitTwoExp(a.begin, a.end, b.end, a.total_values - 1); + a.intervals = sp.n1; + b.intervals = sp.n2; + a.r = sp.r1; + b.r = sp.r2; + i = j - 1; + } + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (d.is_cont_exp) { + bool const has4 = max_bytes >= 4; + int const last1 = (max_bytes >= 2) ? 254 : 255; + int const max_last2 = + has4 ? static_cast(TwoTierMaxU8(static_cast(last1)) - + 1U) + : last1 + 1; + auto const ce = OptimizeContinuousExp(d.begin, d.end, d.cut1, d.cut2, last1, + last1 + 2, 2500, max_last2); + d.intervals = ce.intervals; + d.last_1 = ce.last_1; + d.last_2 = ce.last_2; + d.r = ce.r; + } + if (d.kind == CurveKind::kUniformStep && d.intervals < 0 && + d.specified_step > 0.0) { + double const n = (d.end - d.begin) / d.specified_step; + d.intervals = RoundN(n); + if (d.intervals < 1) { + SegmentedSpecError(); + } + } + } + + AssignOwnership(plan.segs.data(), plan.count); + + bool has2 = false; + bool has4 = false; + bool has8 = false; + for (int i = 0; i < plan.count; ++i) { + if (plan.segs[static_cast(i)].is_cont_exp) { + has2 = true; + has4 = max_bytes >= 4; + } + if (plan.segs[static_cast(i)].bytes == 2) { + has2 = true; + } + if (plan.segs[static_cast(i)].bytes == 4) { + has4 = true; + } + if (plan.segs[static_cast(i)].bytes == 8) { + has8 = true; + } + } + (void)has8; + + int n1_known = 0; + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (d.bytes == 1 && d.intervals >= 0 && !d.fill_tier) { + n1_known += StoredOf(d); + } + if (d.is_cont_exp) { + n1_known += d.last_1 + 1; + } + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (!d.fill_tier) { + continue; + } + int cap = 0; + if (d.bytes == 1) { + cap = has2 ? 255 : 256; + cap -= n1_known; + } else if (d.bytes == 2) { + std::uint32_t const b0 = + n1_known > 0 ? static_cast(n1_known - 1) : 0; + std::uint64_t const tmax = TwoTierMaxU8(b0); + cap = has4 ? static_cast(tmax - static_cast(n1_known)) + : static_cast(tmax + 1U - + static_cast(n1_known)); + } + if (cap < 1) { + SegmentedSpecError(); + } + d.stored = cap; + d.intervals = cap - (d.own_begin ? 1 : 0) - (d.own_end ? 1 : 0) + 1; + } + + for (int pass = 0; pass < 8; ++pass) { + for (int i = 0; i < plan.count; ++i) { + ComputeCoeffsKnownN(plan.segs[static_cast(i)]); + } + TryInherit(plan.segs.data(), plan.count); + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft& d = plan.segs[static_cast(i)]; + if (d.min_intervals) { + if (d.step0 <= 0.0 || !d.has_max_err_upper) { + SegmentedSpecError(); + } + d.intervals = MinRampIntervals(d.end - d.begin, d.step0, d.max_err_upper); + } + } + + for (int pass = 0; pass < 4; ++pass) { + for (int i = 0; i < plan.count; ++i) { + ComputeCoeffsKnownN(plan.segs[static_cast(i)]); + } + } + + for (int i = 0; i < plan.count; ++i) { + CurveDraft const& d = plan.segs[static_cast(i)]; + if (d.intervals < 1 && !d.is_cont_exp) { + SegmentedSpecError(); + } + if (d.step_mode == StepMode::kLowerInherit && d.step0 <= 0.0) { + SegmentedSpecError(); + } + } + + plan.schema_hash = MixHash(0xcbf29ce484222325ULL, + static_cast(plan.count)); + for (int i = 0; i < plan.count; ++i) { + CurveDraft const& d = plan.segs[static_cast(i)]; + plan.schema_hash = MixHash(plan.schema_hash, + static_cast(d.intervals)); + plan.schema_hash = MixHash(plan.schema_hash, + static_cast(d.bytes)); + } + return plan; +} + +template +consteval LogicalPlan CompileLogical() { + constexpr int kMaxB = FormatMaxBytes(); + auto unsplit = MakeUnsplitPlan(kMaxB); + auto plan = SplitContExp(unsplit); + if (plan.n1 > 256U) { + SegmentedSpecError(); + } + if (plan.n2 > 0 && plan.n1 > 255U) { + SegmentedSpecError(); + } + if (plan.n1 == 0) { + SegmentedSpecError(); + } + plan.max_abs = unsplit.max_abs; + plan.schema_hash = MixHash(unsplit.schema_hash, plan.code_count); + return plan; +} + +struct CompiledSegment { + std::int64_t physical_begin_raw = 0; + std::int64_t physical_end_raw = 0; + std::uint32_t wire_code_begin = 0; + std::uint32_t code_count = 0; + CurveKind curve_kind = CurveKind::kUniformStep; + std::uint8_t wire_bytes = 1; + std::int32_t intervals = 0; + std::int32_t math_first = 0; + std::int64_t curve_begin_raw = 0; + std::int64_t curve_end_raw = 0; + std::int64_t step0_raw = 0; + std::int64_t last_step_raw = 0; + std::int64_t delta_raw = 0; + std::int32_t log2_r_raw = 0; + std::int32_t log2_q_raw = 0; + std::int32_t log2_begin_raw = 0; + std::int32_t ratio_raw = 0; + std::uint8_t from_upper = 0; +}; + +template +struct WireSelKind; + +template +struct WireSelKind<0, A, B, C> { + using type = std::uint8_t; +}; + +template +struct WireSelKind<1, A, B, C> { + using type = TieredInt; +}; + +template +struct WireSelKind<2, A, B, C> { + using type = TieredInt; +}; + +template +struct WireSelKind<3, A, B, C> { + using type = TieredInt; +}; + +template +struct PlanHolder { + static constexpr LogicalPlan kPlan = CompileLogical(); +}; + +template +inline constexpr int kWireKind = + (PlanHolder::kPlan.n2 == 0 && PlanHolder::kPlan.n4 == 0 && + PlanHolder::kPlan.n8 == 0) + ? 0 + : ((PlanHolder::kPlan.n4 == 0 && PlanHolder::kPlan.n8 == 0) + ? 1 + : (PlanHolder::kPlan.n8 == 0 ? 2 : 3)); + +template +using WireTypeOf = typename WireSelKind< + kWireKind, + PlanHolder::kPlan.n1 - 1U, + PlanHolder::kPlan.n1 + PlanHolder::kPlan.n2 - 1U, + PlanHolder::kPlan.n1 + PlanHolder::kPlan.n2 + + PlanHolder::kPlan.n4 - 1U>::type; + +template +inline constexpr double kMaxAbsBound = PlanHolder::kPlan.max_abs == 0.0 + ? 1.0 + : PlanHolder::kPlan.max_abs; + +template +struct LogicalTypeSel; + +template +struct LogicalTypeSel { + using type = FixedPoint>; +}; + +template +struct LogicalTypeSel { + using type = FixedPoint>; +}; + +template +using LogicalTypeOf = + typename LogicalTypeSel, + Spec>::type; + +template +using FixedRuntimeOf = LogicalTypeOf; + +template +consteval std::int64_t RawAt(CurveDraft const& d, int i) { + return static_cast(RT::FromDouble(DecodeMath(d, i)).RawValue()); +} + +template +consteval CompiledSegment CompileOne(CurveDraft const& d) { + using Log = segmented_math_internal::SegFixedMathPolicy::log_type; + CompiledSegment c{}; + c.physical_begin_raw = RawAt(d, d.math_first); + c.physical_end_raw = RawAt(d, d.math_first + d.stored - 1); + if (c.physical_end_raw < c.physical_begin_raw) { + auto const t = c.physical_begin_raw; + c.physical_begin_raw = c.physical_end_raw; + c.physical_end_raw = t; + } + c.wire_code_begin = d.wire_begin; + c.code_count = static_cast(d.stored); + c.curve_kind = d.kind; + c.wire_bytes = static_cast(d.bytes); + c.intervals = d.intervals; + c.math_first = d.math_first; + c.curve_begin_raw = RawAt(d, 0); + c.curve_end_raw = RawAt(d, d.intervals); + if (d.intervals >= 1) { + c.step0_raw = RawAt(d, 1) - RawAt(d, 0); + c.last_step_raw = + RawAt(d, d.intervals) - RawAt(d, d.intervals - 1); + } + if (d.intervals >= 2) { + c.delta_raw = (RawAt(d, 2) - RawAt(d, 1)) - c.step0_raw; + } + c.from_upper = GeomFromUpper(d) ? 1 : 0; + if (d.kind == CurveKind::kExponentialValues && d.begin > 0.0 && d.r > 0.0) { + c.log2_r_raw = static_cast( + Log::FromDouble(gcem::log(d.r) / gcem::log(2.0)).RawValue()); + c.log2_begin_raw = static_cast( + Log::FromDouble(gcem::log(d.begin) / gcem::log(2.0)).RawValue()); + c.ratio_raw = segmented_math_internal::RatioToQ30(d.r); + } + if (d.kind == CurveKind::kGeometricStep && d.q > 1.0) { + c.log2_q_raw = static_cast( + Log::FromDouble(gcem::log(d.q) / gcem::log(2.0)).RawValue()); + c.ratio_raw = static_cast( + segmented_math_internal::SegPow::FromDouble(d.q).RawValue()); + } + return c; +} + +template +consteval std::array MakeCompiledSegments() { + constexpr LogicalPlan kPlan = PlanHolder::kPlan; + std::array out{}; + for (int i = 0; i < kPlan.count; ++i) { + CompiledSegment const c = + CompileOne(kPlan.segs[static_cast(i)]); + std::int64_t span = c.physical_end_raw - c.physical_begin_raw; + if (span < 0) { + span = -span; + } + if (c.code_count > static_cast(span) + 1U) { + SegmentedSpecError(); + } + out[static_cast(i)] = c; + } + for (int i = 1; i < kPlan.count; ++i) { + CompiledSegment const key = out[static_cast(i)]; + int j = i; + while (j > 0 && + (out[static_cast(j - 1)].physical_begin_raw > + key.physical_begin_raw || + (out[static_cast(j - 1)].physical_begin_raw == + key.physical_begin_raw && + out[static_cast(j - 1)].wire_code_begin > + key.wire_code_begin))) { + out[static_cast(j)] = out[static_cast(j - 1)]; + --j; + } + out[static_cast(j)] = key; + } + return out; +} + +} // namespace ae::seg::segmented_compiler_internal + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_COMPILER_H_ diff --git a/ae-numeric/details/segmented_curves.h b/ae-numeric/details/segmented_curves.h new file mode 100644 index 0000000..f46726b --- /dev/null +++ b/ae-numeric/details/segmented_curves.h @@ -0,0 +1,434 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_CURVES_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_CURVES_H_ + +#include +#include +#include + +#include "ae-numeric/details/segmented_format.h" +#include "ae-numeric/details/segmented_math.h" + +namespace ae::seg::segmented_curves_internal { + +using segmented_math_internal::SegmentedSpecError; +using segmented_math_internal::ValueToDouble; + +inline constexpr int kMaxDrafts = 16; + +enum class StepMode : std::uint8_t { + kNone = 0, + kLowerExplicit = 1, + kUpperExplicit = 2, + kLowerInherit = 3, + kUpperInherit = 4, +}; + +struct CurveDraft { + CurveKind kind = CurveKind::kUniformStep; + double begin = 0.0; + double end = 0.0; + int bytes = 1; + int intervals = -1; + StepMode step_mode = StepMode::kNone; + double specified_step = 0.0; + bool fill_tier = false; + bool min_intervals = false; + double max_err_upper = 0.0; + bool has_max_err_upper = false; + double max_err_lower = 0.0; + bool has_max_err_lower = false; + int autosplit_id = 0; + int total_values = 0; + bool is_cont_exp = false; + double cut1 = 0.0; + double cut2 = 0.0; + int last_1 = -1; + int last_2 = -1; + double r = 1.0; + double q = 1.0; + double step0 = 0.0; + double last_step = 0.0; + double delta = 0.0; + bool own_begin = false; + bool own_end = false; + int stored = 0; + int math_first = 0; + std::uint32_t wire_begin = 0; + double phys_begin = 0.0; + double phys_end = 0.0; +}; + +template +inline constexpr bool kIsRangeV = false; +template +inline constexpr bool kIsRangeV> = true; + +template +inline constexpr bool kIsPlaceV = false; +template +inline constexpr bool kIsPlaceV> = true; + +template +inline constexpr int kBytesOf = -1; +template +inline constexpr int kBytesOf> = static_cast(N); + +template +inline constexpr int kIntervalsOf = -1; +template +inline constexpr int kIntervalsOf> = static_cast(N); + +template +inline constexpr int kTotalValuesOf = -1; +template +inline constexpr int kTotalValuesOf> = static_cast(N); + +template +inline constexpr bool kIsStepV = false; +template +inline constexpr bool kIsStepV> = true; + +template +inline constexpr bool kIsStepAtLowerV = false; +template +inline constexpr bool kIsStepAtLowerV> = true; + +template +inline constexpr bool kIsStepAtUpperV = false; +template +inline constexpr bool kIsStepAtUpperV> = true; + +template +inline constexpr bool kIsMaxErrLowerV = false; +template +inline constexpr bool kIsMaxErrLowerV> = true; + +template +inline constexpr bool kIsMaxErrUpperV = false; +template +inline constexpr bool kIsMaxErrUpperV> = true; + +template +inline constexpr bool kIsApproxCutV = false; +template +inline constexpr bool kIsApproxCutV> = true; + +template +inline constexpr bool kIsRestV = false; +template +inline constexpr bool kIsRestV> = true; + +template +inline constexpr bool kIsWireCutsV = false; +template +inline constexpr bool kIsWireCutsV> = true; + +template +inline constexpr bool kIsMathCurveV = false; +template +inline constexpr bool kIsMathCurveV> = true; +template +inline constexpr bool kIsMathCurveV> = true; +template +inline constexpr bool kIsMathCurveV> = true; +template +inline constexpr bool kIsMathCurveV> = true; +template +inline constexpr bool kIsMathCurveV> = true; + +struct OptAcc { + double begin = 0.0; + double end = 0.0; + bool has_range = false; + int bytes = -1; + int intervals = -1; + int total_values = -1; + StepMode step_mode = StepMode::kNone; + double specified_step = 0.0; + bool fill_tier = false; + bool min_intervals = false; + double max_err_upper = 0.0; + bool has_max_err_upper = false; + double max_err_lower = 0.0; + bool has_max_err_lower = false; + double cut1 = 0.0; + double cut2 = 0.0; + int ncuts = 0; +}; + +template +consteval void ApplyOne(OptAcc& a, Range) { + a.begin = ValueToDouble(); + a.end = ValueToDouble(); + a.has_range = true; +} + +template +consteval void ApplyOne(OptAcc& a, Place) { + a.bytes = kBytesOf; +} + +template +consteval void ApplyOne(OptAcc& a, Intervals) { + a.intervals = static_cast(N); +} + +template +consteval void ApplyOne(OptAcc& a, TotalValues) { + a.total_values = static_cast(N); +} + +consteval void ApplyOne(OptAcc& a, FillTier) { a.fill_tier = true; } + +consteval void ApplyOne(OptAcc& a, MinimumIntervals) { a.min_intervals = true; } + +template +consteval void ApplyOne(OptAcc& a, Step) { + a.specified_step = ValueToDouble(); + a.step_mode = StepMode::kLowerExplicit; +} + +template +consteval void ApplyOne(OptAcc& a, StepAtLower) { + if constexpr (std::is_same_v) { + a.step_mode = StepMode::kLowerInherit; + } else { + a.step_mode = StepMode::kLowerExplicit; + a.specified_step = ValueToDouble(); + } +} + +template +consteval void ApplyOne(OptAcc& a, StepAtUpper) { + if constexpr (std::is_same_v) { + a.step_mode = StepMode::kUpperInherit; + } else { + a.step_mode = StepMode::kUpperExplicit; + a.specified_step = ValueToDouble(); + } +} + +template +consteval void ApplyOne(OptAcc& a, MaxAbsErrorAtLower) { + a.max_err_lower = ValueToDouble(); + a.has_max_err_lower = true; +} + +template +consteval void ApplyOne(OptAcc& a, MaxAbsErrorAtUpper) { + a.max_err_upper = ValueToDouble(); + a.has_max_err_upper = true; +} + +template +consteval void ApplyOne(OptAcc& a, ApproximateCut) { + if (a.ncuts == 0) { + a.cut1 = ValueToDouble(); + } else { + a.cut2 = ValueToDouble(); + } + ++a.ncuts; +} + +template +consteval void ApplyOne(OptAcc& /*a*/, Rest) {} + +template +consteval void ApplyOne(OptAcc& a, WireCuts) { + (ApplyOne(a, C{}), ...); +} + +consteval void ApplyOne(OptAcc& /*a*/, ExactEndpoints) {} +consteval void ApplyOne(OptAcc& /*a*/, OptimizeCuts) {} +template +consteval void ApplyOne(OptAcc& /*a*/, EndpointPolicy

) {} +template +consteval void ApplyOne(OptAcc& /*a*/, Allocate

) {} +template +consteval void ApplyOne(OptAcc& /*a*/, T) {} + +template +consteval OptAcc ParseOpts() { + OptAcc a{}; + (ApplyOne(a, Opts{}), ...); + return a; +} + +consteval CurveDraft DraftFromAcc(CurveKind kind, OptAcc const& a, int bytes_fallback) { + CurveDraft d{}; + d.kind = kind; + if (!a.has_range) { + SegmentedSpecError(); + } + d.begin = a.begin; + d.end = a.end; + if (d.end < d.begin) { + SegmentedSpecError(); + } + d.bytes = a.bytes >= 0 ? a.bytes : bytes_fallback; + if (d.bytes != 1 && d.bytes != 2 && d.bytes != 4 && d.bytes != 8) { + SegmentedSpecError(); + } + d.intervals = a.intervals; + d.step_mode = a.step_mode; + d.specified_step = a.specified_step; + d.fill_tier = a.fill_tier; + d.min_intervals = a.min_intervals; + d.max_err_upper = a.max_err_upper; + d.has_max_err_upper = a.has_max_err_upper; + d.max_err_lower = a.max_err_lower; + d.has_max_err_lower = a.has_max_err_lower; + d.cut1 = a.cut1; + d.cut2 = a.cut2; + return d; +} + +template +struct DraftsOf; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + out[i++] = DraftFromAcc(CurveKind::kUniformStep, ParseOpts(), bytes_fb); + } +}; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + out[i++] = + DraftFromAcc(CurveKind::kUniformValues, ParseOpts(), bytes_fb); + } +}; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + CurveDraft d = + DraftFromAcc(CurveKind::kExponentialValues, ParseOpts(), bytes_fb); + if (d.begin <= 0.0 || d.end <= 0.0) { + SegmentedSpecError(); + } + out[i++] = d; + } +}; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + out[i++] = + DraftFromAcc(CurveKind::kGeometricStep, ParseOpts(), bytes_fb); + } +}; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + out[i++] = + DraftFromAcc(CurveKind::kLinearStepRamp, ParseOpts(), bytes_fb); + } +}; + +template +struct DraftsOf> { + static constexpr int kCount = 1; + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + CurveDraft d = DraftFromAcc(CurveKind::kExponentialValues, + ParseOpts(), bytes_fb); + if (d.begin <= 0.0 || d.end <= 0.0) { + SegmentedSpecError(); + } + d.is_cont_exp = true; + d.bytes = 0; + out[i++] = d; + } +}; + +template +consteval int CurveDraftCount() { + if constexpr (kIsMathCurveV) { + return DraftsOf::kCount; + } else { + return 0; + } +} + +template +struct CountAutoCurves { + static constexpr int value = (CurveDraftCount() + ... + 0); +}; + +template +consteval void FillIfCurve(CurveDraft* out, int& i, int as_id, int bytes) { + if constexpr (kIsMathCurveV) { + DraftsOf::Fill(out, i, as_id, bytes); + } +} + +template +struct DraftsOf> { + static constexpr int kCount = CountAutoCurves::value; + static consteval void Fill(CurveDraft* out, int& i, int as_id, int /*bytes_fb*/) { + OptAcc const pack = ParseOpts(); + int const bytes = pack.bytes; + int const total = pack.total_values; + if (bytes < 0 || total < 2 || kCount < 2) { + SegmentedSpecError(); + } + int const start = i; + (FillIfCurve(out, i, as_id, bytes), ...); + for (int k = start; k < i; ++k) { + out[k].autosplit_id = as_id; + out[k].total_values = total; + out[k].bytes = bytes; + } + } +}; + +template +struct FlattenLayout; + +template <> +struct FlattenLayout<> { + static constexpr int kCount = 0; + static consteval void Fill(CurveDraft*, int&, int&) {} +}; + +template +struct FlattenLayout { + static constexpr int kCount = + DraftsOf::kCount + FlattenLayout::kCount; + static consteval void Fill(CurveDraft* out, int& i, int& next_as) { + DraftsOf::Fill(out, i, next_as, 1); + ++next_as; + FlattenLayout::Fill(out, i, next_as); + } +}; + +template +struct FlattenLayout> : FlattenLayout {}; + +} // namespace ae::seg::segmented_curves_internal + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_CURVES_H_ diff --git a/ae-numeric/details/segmented_format.h b/ae-numeric/details/segmented_format.h new file mode 100644 index 0000000..08e42a3 --- /dev/null +++ b/ae-numeric/details/segmented_format.h @@ -0,0 +1,174 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_FORMAT_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_FORMAT_H_ + +#include +#include +#include + +#include "ae-numeric/decimal.h" + +namespace ae::seg { + +template +struct RuntimePolicyTag { + using rep = T; +}; + +namespace runtime { +template +struct Fixed { + using rep = Rep; + static constexpr bool kIsFloating = false; +}; + +template +struct Floating { + using rep = T; + static constexpr bool kIsFloating = true; +}; +} // namespace runtime + +template +inline constexpr bool kIsFloatingRuntimePolicy = false; +template +inline constexpr bool kIsFloatingRuntimePolicy> = true; + +namespace wire { +template +struct MaxBytes { + static constexpr std::size_t value = N; +}; + +template +struct AutoTiered { + using cell_type = Cell; +}; +} // namespace wire + +namespace compute { +struct Formula {}; +struct Lookup {}; +} // namespace compute + +template +struct Range {}; + +template +struct Step {}; + +template +struct Intervals { + static constexpr std::size_t value = N; +}; + +template +struct TotalValues { + static constexpr std::size_t value = N; +}; + +template +struct Bytes { + static constexpr std::size_t value = N; +}; + +template +struct Place {}; + +struct InheritStep {}; +struct FillTier {}; +struct MinimumIntervals {}; +struct ExactEndpoints {}; +struct OptimizeCuts {}; +struct ContinuousAbsoluteStep {}; +struct MinimaxRelativeError {}; + +template +struct EndpointPolicy {}; + +template +struct Allocate {}; + +template +struct StepAtLower {}; + +template +struct StepAtUpper {}; + +template +struct MaxAbsErrorAtLower {}; + +template +struct MaxAbsErrorAtUpper {}; + +template +struct Objective {}; + +template +struct ApproximateCut {}; + +template +struct Rest {}; + +template +struct WireCuts {}; + +template +struct Layout {}; + +template +struct UniformStep {}; + +template +struct UniformValues {}; + +template +struct ExponentialValues {}; + +template +struct GeometricStep {}; + +template +struct LinearStepRamp {}; + +template +struct AutoSplit {}; + +template +struct ContinuousExponential {}; + +template +struct Format { + using runtime_policy = RuntimePolicy; + using wire_policy = WirePolicy; + using compute_policy = ComputePolicy; + using layout_type = LayoutT; +}; + +enum class CurveKind : std::uint8_t { + kUniformStep = 1, + kUniformValues = 2, + kExponentialValues = 3, + kGeometricStep = 4, + kLinearStepRamp = 5, +}; + +} // namespace ae::seg + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_FORMAT_H_ diff --git a/ae-numeric/details/segmented_formula_backend.h b/ae-numeric/details/segmented_formula_backend.h new file mode 100644 index 0000000..f1eefc9 --- /dev/null +++ b/ae-numeric/details/segmented_formula_backend.h @@ -0,0 +1,322 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_FORMULA_BACKEND_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_FORMULA_BACKEND_H_ + +#include +#include + +#include "ae-numeric/details/segmented_compiler.h" +#include "ae-numeric/details/segmented_math.h" +#include "ae-numeric/fixed_point.h" +#include "ae-numeric/integer_math.h" + +namespace ae::seg::segmented_formula_internal { + +using segmented_compiler_internal::CompiledSegment; +using SegPow = segmented_math_internal::SegPow; + +constexpr std::int64_t ClampI64(std::int64_t v, std::int64_t lo, + std::int64_t hi) { + if (v < lo) { + return lo; + } + if (v > hi) { + return hi; + } + return v; +} + +template +constexpr T FromI64Raw(std::int64_t raw) { + auto const clamped = ClampI64(raw, static_cast(T::kRawMin), + static_cast(T::kRawMax)); + return T::FromRaw( + fixed_point_internal::RepFromRawValue( + static_cast(clamped))); +} + +constexpr SegPow PowFromRatioRaw(std::int32_t ratio_raw) { + return FromI64Raw(static_cast(ratio_raw)); +} + +// Exponentiation by squaring in SegPow. e == 0 => 1. O(log e) muls, no heap. +constexpr SegPow PowUint(SegPow base, unsigned e) { + SegPow result = SegPow::FromRuntimeInteger(1); + SegPow b = base; + while (e > 0U) { + if ((e & 1U) != 0U) { + result = MulTo(result, b); + } + e >>= 1U; + if (e > 0U) { + b = MulTo(b, b); + } + } + return result; +} + +constexpr std::int64_t LerpRaw(std::int64_t a, std::int64_t b, int i, int n) { + if (n <= 0 || i <= 0) { + return a; + } + if (i >= n) { + return b; + } + std::int64_t const diff = b - a; + bool const neg = diff < 0; + std::uint64_t out = 0; + if (!integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(diff), + static_cast(i), + static_cast(n), out)) { + return i >= n / 2 ? b : a; + } + auto const mag = static_cast(out); + return neg ? a - mag : a + mag; +} + +constexpr std::uint64_t RatioPowQ30(std::uint64_t base, unsigned e) { + std::uint64_t const one = + std::uint64_t{1} << segmented_math_internal::kSegRatioQ; + std::uint64_t result = one; + std::uint64_t b = base; + while (e > 0U) { + if ((e & 1U) != 0U) { + std::uint64_t out = 0; + if (!integer_math::MulDivU64Nearest(result, b, one, out)) { + return std::numeric_limits::max(); + } + result = out; + } + e >>= 1U; + if (e > 0U) { + std::uint64_t out = 0; + if (!integer_math::MulDivU64Nearest(b, b, one, out)) { + return std::numeric_limits::max(); + } + b = out; + } + } + return result; +} + +template +constexpr std::int64_t ExpValueRaw(CompiledSegment const& s, int math_i) { + if (math_i <= 0) { + return s.curve_begin_raw; + } + if (math_i >= s.intervals) { + return s.curve_end_raw; + } + if (s.ratio_raw <= 0 || s.curve_begin_raw <= 0) { + return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + } + std::uint64_t const p = + RatioPowQ30(static_cast(s.ratio_raw), + static_cast(math_i)); + std::uint64_t const one = + std::uint64_t{1} << segmented_math_internal::kSegRatioQ; + std::uint64_t out = 0; + if (!integer_math::MulDivU64Nearest( + integer_math::AbsI64ToU64(s.curve_begin_raw), p, one, out)) { + return s.curve_end_raw; + } + if (out > static_cast(std::numeric_limits::max())) { + return s.curve_end_raw; + } + return static_cast(out); +} + +constexpr std::int64_t LinearValueRaw(CompiledSegment const& s, int math_i) { + if (math_i <= 0) { + return s.curve_begin_raw; + } + if (math_i >= s.intervals) { + return s.curve_end_raw; + } + std::int64_t const n = math_i; + std::int64_t raw = s.curve_begin_raw; + raw += n * s.step0_raw; + raw += s.delta_raw * n * (n - 1) / 2; + return raw; +} + +constexpr std::int64_t GeomValueRaw(CompiledSegment const& s, int math_i) { + if (math_i <= 0) { + return s.curve_begin_raw; + } + if (math_i >= s.intervals) { + return s.curve_end_raw; + } + std::int64_t const span = s.curve_end_raw - s.curve_begin_raw; + if (s.ratio_raw <= 0 || span == 0) { + return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + } + int const n = s.from_upper != 0 ? (s.intervals - math_i) : math_i; + SegPow const q = PowFromRatioRaw(s.ratio_raw); + SegPow const qn = PowUint(q, static_cast(n)); + SegPow const qN = PowUint(q, static_cast(s.intervals)); + std::int64_t const one = + static_cast(SegPow::FromRuntimeInteger(1).RawValue()); + std::int64_t const num = static_cast(qn.RawValue()) - one; + std::int64_t const den = static_cast(qN.RawValue()) - one; + if (den == 0 || num < 0) { + return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + } + bool const neg = span < 0; + std::uint64_t out = 0; + if (!integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(span), + integer_math::AbsI64ToU64(num), + integer_math::AbsI64ToU64(den), out)) { + return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + } + auto const mag = static_cast(out); + std::int64_t const offset = neg ? -mag : mag; + if (s.from_upper != 0) { + return s.curve_end_raw - offset; + } + return s.curve_begin_raw + offset; +} + +template +constexpr std::int64_t DecodeMathRaw(CompiledSegment const& s, int math_i) { + if (s.curve_kind == CurveKind::kExponentialValues) { + return ExpValueRaw(s, math_i); + } + if (s.curve_kind == CurveKind::kGeometricStep) { + return GeomValueRaw(s, math_i); + } + if (s.curve_kind == CurveKind::kLinearStepRamp) { + return LinearValueRaw(s, math_i); + } + return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); +} + +template +constexpr std::int64_t DecodeRankRaw(CompiledSegment const* segs, int nseg, + std::uint32_t rank) { + for (int i = 0; i < nseg; ++i) { + CompiledSegment const& s = segs[i]; + if (rank >= s.wire_code_begin && + rank < s.wire_code_begin + s.code_count) { + int const local = static_cast(rank - s.wire_code_begin); + int const math_i = s.math_first + local; + return DecodeMathRaw(s, math_i); + } + } + return segs[0].curve_begin_raw; +} + +constexpr int LinearApproxIndex(CompiledSegment const& s, std::int64_t raw) { + std::int64_t const span = s.curve_end_raw - s.curve_begin_raw; + if (span == 0 || s.intervals <= 0) { + return 0; + } + std::uint64_t out = 0; + std::int64_t const pos = raw - s.curve_begin_raw; + integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(pos), + static_cast(s.intervals), + integer_math::AbsI64ToU64(span), out); + return static_cast(out); +} + +template +constexpr int ClosestMathIndex(CompiledSegment const& s, std::int64_t raw) { + int const first = s.math_first; + int const last = s.math_first + static_cast(s.code_count) - 1; + if (last <= first) { + return first; + } + int lo = first; + int hi = last; + while (lo < hi) { + int const mid = lo + (hi - lo + 1) / 2; + if (DecodeMathRaw(s, mid) <= raw) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; +} + +template +constexpr int ApproxIndex(CompiledSegment const& s, std::int64_t raw) { + if (s.intervals <= 1) { + return 0; + } + if (s.curve_kind == CurveKind::kExponentialValues || + s.curve_kind == CurveKind::kGeometricStep) { + return ClosestMathIndex(s, raw); + } + if (s.curve_kind == CurveKind::kLinearStepRamp && s.delta_raw != 0) { + std::int64_t const a = s.delta_raw; + std::int64_t const b = 2 * s.step0_raw - s.delta_raw; + std::int64_t const cc = 2 * (s.curve_begin_raw - raw); + std::int64_t disc = b * b - 4 * a * cc; + if (disc < 0) { + disc = 0; + } + std::uint64_t const root = + integer_math::SqrtU64(static_cast(disc)); + std::int64_t const den = 2 * a; + if (den == 0) { + return 0; + } + std::int64_t const num = -b + static_cast(root); + return static_cast(num / den); + } + return LinearApproxIndex(s, raw); +} + +template +constexpr std::uint32_t EncodeRaw(CompiledSegment const* segs, int nseg, + std::uint32_t code_count, std::int64_t raw) { + std::uint32_t best = 0; + std::uint64_t best_d = std::numeric_limits::max(); + for (int si = 0; si < nseg; ++si) { + CompiledSegment const& s = segs[si]; + int approx = ApproxIndex(s, raw); + int const last = s.math_first + static_cast(s.code_count) - 1; + if (approx < s.math_first) { + approx = s.math_first; + } + if (approx > last) { + approx = last; + } + int const lo_j = (approx < s.math_first + 4) ? s.math_first : (approx - 4); + int const hi_j = (approx + 4 > last) ? last : (approx + 4); + for (int j = lo_j; j <= hi_j; ++j) { + std::int64_t const dec = DecodeMathRaw(s, j); + std::uint64_t const d = integer_math::AbsI64ToU64(dec - raw); + std::uint32_t const rank = + s.wire_code_begin + static_cast(j - s.math_first); + if (d < best_d || (d == best_d && rank < best)) { + best_d = d; + best = rank; + } + } + } + if (best >= code_count) { + return code_count - 1U; + } + return best; +} + +} // namespace ae::seg::segmented_formula_internal + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_FORMULA_BACKEND_H_ diff --git a/ae-numeric/details/segmented_lookup_backend.h b/ae-numeric/details/segmented_lookup_backend.h new file mode 100644 index 0000000..2edaea5 --- /dev/null +++ b/ae-numeric/details/segmented_lookup_backend.h @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ + +#include +#include +#include +#include + +#include "ae-numeric/details/segmented_compiler.h" +#include "ae-numeric/details/segmented_formula_backend.h" +#include "ae-numeric/integer_math.h" + +namespace ae::seg::segmented_lookup_internal { + +template +struct LookupTables { + std::array decoded{}; + std::array order{}; +}; + +template +consteval LookupTables MakeLookupTables() { + using segmented_compiler_internal::MakeCompiledSegments; + using segmented_compiler_internal::PlanHolder; + LookupTables t{}; + constexpr auto kSegs = MakeCompiledSegments(); + constexpr int kNs = PlanHolder::kPlan.count; + for (std::size_t i = 0; i < N; ++i) { + t.decoded[i] = segmented_formula_internal::DecodeRankRaw( + kSegs.data(), kNs, static_cast(i)); + t.order[i] = static_cast(i); + } + for (std::size_t i = 1; i < N; ++i) { + std::uint32_t const key = t.order[i]; + std::int64_t const keyv = t.decoded[key]; + std::size_t j = i; + while (j > 0 && t.decoded[t.order[j - 1]] > keyv) { + t.order[j] = t.order[j - 1]; + --j; + } + t.order[j] = key; + } + return t; +} + +template +constexpr std::uint32_t LookupEncode(LookupTables const& t, + std::int64_t raw) { + if (N == 0) { + return 0; + } + std::size_t lo = 0; + std::size_t hi = N; + while (lo < hi) { + std::size_t const mid = lo + (hi - lo) / 2U; + if (t.decoded[t.order[mid]] < raw) { + lo = mid + 1U; + } else { + hi = mid; + } + } + std::uint32_t best = t.order[lo < N ? lo : N - 1U]; + std::uint64_t best_d = integer_math::AbsI64ToU64(t.decoded[best] - raw); + auto consider = [&](std::size_t idx) { + if (idx >= N) { + return; + } + std::uint32_t const rank = t.order[idx]; + std::uint64_t const d = + integer_math::AbsI64ToU64(t.decoded[rank] - raw); + if (d < best_d || (d == best_d && rank < best)) { + best_d = d; + best = rank; + } + }; + if (lo > 0) { + consider(lo - 1U); + } + consider(lo); + if (lo + 1U < N) { + consider(lo + 1U); + } + return best; +} + +} // namespace ae::seg::segmented_lookup_internal + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ diff --git a/ae-numeric/details/segmented_math.h b/ae-numeric/details/segmented_math.h new file mode 100644 index 0000000..7f18a1c --- /dev/null +++ b/ae-numeric/details/segmented_math.h @@ -0,0 +1,283 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ +#define AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ + +#include +#include +#include +#include + +#include + +#include "ae-numeric/decimal.h" +#include "ae-numeric/details/segmented_format.h" +#include "ae-numeric/fixed_point.h" +#include "ae-numeric/integer_math.h" + +namespace ae::seg::segmented_math_internal { + +struct SegFixedMathPolicy { + using log_type = FixedPoint; + using mant_type = FixedPoint; + using mul_intermediate_type = std::int64_t; + static constexpr int kLogIterations = 16; + static constexpr int kExp2FractionBits = 16; +}; + +inline void SegmentedSpecError() {} + +// High-resolution ratio (r, q ≈ 1). Geometric decode uses SegPow +// exponentiation-by-squaring (max 256 covers q^N up to ~23). Exponential +// decode uses a Q30 integer ratio (kSegRatioQ) instead. +using SegPow = FixedPoint; + +inline constexpr int kSegRatioQ = 30; + +consteval std::int32_t RatioToQ30(double r) { + double const scaled = r * static_cast(std::uint64_t{1} << kSegRatioQ); + if (scaled < 1.0 || + scaled > static_cast(std::numeric_limits::max())) { + SegmentedSpecError(); + return 0; + } + return static_cast(scaled + 0.5); +} + +template +consteval double ValueToDouble() { + if constexpr (kIsDecimalV) { + double const mag = static_cast( + T::kMantissa < 0 ? -T::kMantissa : T::kMantissa); + double const scaled = + T::kExponent10 >= 0 + ? mag * static_cast(Pow10u(static_cast( + T::kExponent10))) + : mag / static_cast(Pow10u(static_cast( + -T::kExponent10))); + return T::kMantissa < 0 ? -scaled : scaled; + } else if constexpr (kIsRatioV) { + return static_cast(T::kNum) / static_cast(T::kDen); + } else { + SegmentedSpecError(); + return 0.0; + } +} + +consteval double GeomSum(double q, int n) { + if (n <= 0) { + return 0.0; + } + if (gcem::abs(q - 1.0) < 1.0e-18) { + return static_cast(n); + } + return (gcem::pow(q, n) - 1.0) / (q - 1.0); +} + +// Solve (q^n - 1)/(q-1) = S for q > 1. +consteval double SolveQForGeomSum(int n, double sum) { + if (n <= 0 || sum <= 0.0) { + SegmentedSpecError(); + return 1.0; + } + double lo = 1.0 + 1.0e-18; + double hi = 2.0; + for (int i = 0; i < 40 && GeomSum(hi, n) < sum; ++i) { + hi *= 2.0; + } + for (int i = 0; i < 80; ++i) { + double const mid = 0.5 * (lo + hi); + if (GeomSum(mid, n) < sum) { + lo = mid; + } else { + hi = mid; + } + } + return 0.5 * (lo + hi); +} + +consteval double ExpRatio(double begin, double end, int intervals) { + if (begin <= 0.0 || end <= 0.0 || intervals <= 0) { + SegmentedSpecError(); + return 1.0; + } + return gcem::pow(end / begin, 1.0 / static_cast(intervals)); +} + +struct AutoSplitResult { + int n1 = 0; + int n2 = 0; + double r1 = 1.0; + double r2 = 1.0; +}; + +consteval AutoSplitResult AutoSplitTwoExp(double begin, double mid, double end, + int total_intervals) { + AutoSplitResult best{}; + bool have = false; + double best_jump = 0.0; + double best_err = 0.0; + for (int n1 = 1; n1 < total_intervals; ++n1) { + int const n2 = total_intervals - n1; + double const r1 = ExpRatio(begin, mid, n1); + double const r2 = ExpRatio(mid, end, n2); + double const step_before = mid * (1.0 - 1.0 / r1); + double const step_after = mid * (r2 - 1.0); + double const smaller = + step_before < step_after ? step_before : step_after; + if (smaller <= 0.0) { + continue; + } + double const jump = gcem::abs(step_after - step_before) / smaller; + double const err1 = gcem::sqrt(r1) - 1.0; + double const err2 = gcem::sqrt(r2) - 1.0; + double const max_err = err1 > err2 ? err1 : err2; + bool const better = !have || jump < best_jump - 1.0e-18 || + (gcem::abs(jump - best_jump) <= 1.0e-18 && + (max_err < best_err - 1.0e-18 || + (gcem::abs(max_err - best_err) <= 1.0e-18 && + n1 < best.n1))); + if (better) { + have = true; + best_jump = jump; + best_err = max_err; + best.n1 = n1; + best.n2 = n2; + best.r1 = r1; + best.r2 = r2; + } + } + if (!have) { + SegmentedSpecError(); + } + return best; +} + +struct ContExpResult { + int intervals = 0; + int last_1 = 0; + int last_2 = 0; + double r = 1.0; +}; + +consteval double LogErr(double got, double want) { + return gcem::abs(gcem::log(got / want)); +} + +consteval int RoundNearestNonneg(double x) { + if (x < 0.0) { + return 0; + } + return static_cast(x + 0.5); +} + +// Fill the 1-byte tier (last code = last_1_max, typically 254), then choose +// total intervals so nearest-code region boundaries sit as close as possible +// to the requested physical cuts. +consteval ContExpResult OptimizeContinuousExp(double vmin, double vmax, + double cut1, double cut2, + int last_1_max, int min_n, + int max_n, int max_last_2) { + ContExpResult best{}; + bool have = false; + double best_cut = 0.0; + double best_rel = 0.0; + int const i1 = last_1_max; + for (int n = min_n; n <= max_n; ++n) { + if (n <= i1 + 1) { + continue; + } + double const r = ExpRatio(vmin, vmax, n); + double const span_log = gcem::log(vmax / vmin); + double const i2f = static_cast(n) * gcem::log(cut2 / vmin) / + span_log - + 0.5; + int const i2 = RoundNearestNonneg(i2f); + if (i2 <= i1 || i2 >= n || i2 > max_last_2) { + continue; + } + double const b1 = vmin * gcem::pow(r, static_cast(i1) + 0.5); + double const b2 = vmin * gcem::pow(r, static_cast(i2) + 0.5); + double const cut = + LogErr(b1, cut1) > LogErr(b2, cut2) ? LogErr(b1, cut1) : LogErr(b2, cut2); + double const rel = gcem::sqrt(r) - 1.0; + bool const better = + !have || cut < best_cut - 1.0e-18 || + (gcem::abs(cut - best_cut) <= 1.0e-18 && + (rel < best_rel - 1.0e-18 || + (gcem::abs(rel - best_rel) <= 1.0e-18 && n < best.intervals))); + if (better) { + have = true; + best_cut = cut; + best_rel = rel; + best.intervals = n; + best.last_1 = i1; + best.last_2 = i2; + best.r = r; + } + } + if (!have) { + SegmentedSpecError(); + } + return best; +} + +consteval std::uint64_t TwoTierMaxU8(std::uint32_t b0) { + return (255ULL - b0 - 1ULL) * 256ULL + b0 + 1ULL + 255ULL; +} + +consteval std::uint64_t ThreeTierMaxU8(std::uint32_t b0, std::uint32_t b1) { + std::uint64_t const two = TwoTierMaxU8(b0); + constexpr std::uint64_t kWord2 = 65536ULL; + return (two - b1 - 1ULL) * kWord2 + b1 + 1ULL + (kWord2 - 1ULL); +} + +consteval int CeilPositive(double x) { + int const i = static_cast(x); + if (static_cast(i) < x) { + return i + 1; + } + return i; +} + +consteval int MinRampIntervals(double span, double step0, double max_err) { + if (span <= 0.0 || step0 <= 0.0 || max_err <= 0.0) { + SegmentedSpecError(); + return 1; + } + double const last = 2.0 * max_err; + double const den = step0 + last; + if (den <= 0.0) { + SegmentedSpecError(); + return 1; + } + int n = CeilPositive(2.0 * span / den); + if (n < 1) { + n = 1; + } + return n; +} + +consteval std::uint64_t MixHash(std::uint64_t h, std::uint64_t v) { + h ^= v; + h *= 1099511628211ULL; + return h; +} + +} // namespace ae::seg::segmented_math_internal + +#endif // AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ diff --git a/ae-numeric/integer_math.h b/ae-numeric/integer_math.h index 049cb1b..7a8d03f 100644 --- a/ae-numeric/integer_math.h +++ b/ae-numeric/integer_math.h @@ -271,6 +271,21 @@ AE_INTEGER_MATH_CONSTEXPR bool RoundDivPow2U64(std::uint64_t a, std::uint64_t b, return RoundDivU64(a, scaled_den, out); } +// Floor square root. Newton iteration; no overflow of x*x in the check because +// `x > n / x` is used instead of `x * x > n`. +AE_INTEGER_MATH_CONSTEXPR std::uint64_t SqrtU64(std::uint64_t n) noexcept { + if (n < 2U) { + return n; + } + std::uint64_t x0 = n >> 1U; + std::uint64_t x1 = (x0 + n / x0) >> 1U; + while (x1 < x0) { + x0 = x1; + x1 = (x0 + n / x0) >> 1U; + } + return x0; +} + } // namespace ae::integer_math #undef AE_INTEGER_MATH_CONSTEXPR diff --git a/ae-numeric/segmented_number.h b/ae-numeric/segmented_number.h new file mode 100644 index 0000000..236dd4e --- /dev/null +++ b/ae-numeric/segmented_number.h @@ -0,0 +1,379 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_SEGMENTED_NUMBER_H_ +#define AE_NUMERIC_SEGMENTED_NUMBER_H_ + +#include +#include +#include +#include +#include + +#include "ae-numeric/details/segmented_compiler.h" +#include "ae-numeric/details/segmented_curves.h" +#include "ae-numeric/details/segmented_format.h" +#include "ae-numeric/details/segmented_formula_backend.h" +#include "ae-numeric/details/segmented_lookup_backend.h" +#include "ae-numeric/fixed_point.h" +#include "ae-numeric/numeric_traits.h" +#include "ae-numeric/runtime_numeric_traits.h" +#include "ae-numeric/wire_io.h" + +namespace ae::seg { + +template +inline constexpr bool kFloatingRuntimeEnabled = false; + +namespace segmented_number_internal { + +template +struct RuntimeRawMap { + static constexpr std::int64_t ToRaw(RT const& v) { + static_assert(numeric_traits::kIsFixedPoint, + "include ae-numeric/segmented_number_floating_runtime.h " + "to use floating runtime"); + return static_cast(v.RawValue()); + } + + static constexpr RT FromRaw(std::int64_t raw) { + static_assert(numeric_traits::kIsFixedPoint, + "include ae-numeric/segmented_number_floating_runtime.h " + "to use floating runtime"); + auto const clamped = RT::ClampRaw(static_cast( + raw < static_cast(RT::kRawMin) + ? RT::kRawMin + : (raw > static_cast(RT::kRawMax) ? RT::kRawMax + : raw))); + return RT::FromRaw( + fixed_point_internal::RepFromRawValue(clamped)); + } +}; + +template +struct Codec { + static constexpr std::uint32_t Encode( + segmented_compiler_internal::CompiledSegment const* segs, int nseg, + std::uint32_t code_count, std::int64_t raw) { + return segmented_formula_internal::EncodeRaw(segs, nseg, code_count, + raw); + } + + static constexpr std::int64_t Decode( + segmented_compiler_internal::CompiledSegment const* segs, int nseg, + std::uint32_t rank) { + return segmented_formula_internal::DecodeRankRaw(segs, nseg, rank); + } +}; + +template +struct Codec { + static constexpr auto kTables = + segmented_lookup_internal::MakeLookupTables(); + + static constexpr std::uint32_t Encode( + segmented_compiler_internal::CompiledSegment const*, int, std::uint32_t, + std::int64_t raw) { + return segmented_lookup_internal::LookupEncode(kTables, + raw); + } + + static constexpr std::int64_t Decode( + segmented_compiler_internal::CompiledSegment const*, int, + std::uint32_t rank) { + if (rank >= N) { + return kTables.decoded[0]; + } + return kTables.decoded[rank]; + } +}; + +template +using RuntimeType = std::conditional_t< + kIsFloatingRuntimePolicy, + typename Spec::runtime_policy::rep, + segmented_compiler_internal::LogicalTypeOf>; + +template +inline constexpr bool kUseLookup = + std::is_same_v; + +template +constexpr Wire RankToWire(std::uint32_t rank) { + if constexpr (std::is_same_v) { + return static_cast(rank); + } else { + return Wire{rank}; + } +} + +template +constexpr std::uint32_t WireToRank(Wire const& w) { + if constexpr (std::is_same_v) { + return static_cast(w); + } else { + return static_cast(static_cast(w)); + } +} + +} // namespace segmented_number_internal + +template +class SegmentedNumber { + public: + using spec_type = Spec; + using runtime_type = segmented_number_internal::RuntimeType; + using logical_type = segmented_compiler_internal::LogicalTypeOf; + using wire_type = segmented_compiler_internal::WireTypeOf; + using runtime_raw_type = std::int64_t; + + static constexpr auto kPlan = + segmented_compiler_internal::PlanHolder::kPlan; + static constexpr std::size_t kSegmentCount = + static_cast(kPlan.count); + static constexpr std::size_t kCodeCount = + static_cast(kPlan.code_count); + static constexpr std::size_t kMaxWireBytes = + kPlan.n8 > 0 ? 8U : (kPlan.n4 > 0 ? 4U : (kPlan.n2 > 0 ? 2U : 1U)); + static constexpr std::uint64_t kSchemaHash = kPlan.schema_hash; + static constexpr std::uint32_t kOneByteCount = kPlan.n1; + static constexpr std::uint32_t kTwoByteCount = kPlan.n2; + static constexpr std::uint32_t kFourByteCount = kPlan.n4; + static constexpr std::size_t kFormulaCoefficientBytes = + sizeof(segmented_compiler_internal::CompiledSegment) * kSegmentCount; + + static_assert(!kIsFloatingRuntimePolicy || + kFloatingRuntimeEnabled, + "include ae-numeric/segmented_number_floating_runtime.h " + "to use floating runtime"); + static_assert(kCodeCount >= 1U, "format without values"); + + static constexpr auto kSegments = + segmented_compiler_internal::MakeCompiledSegments(); + + static constexpr std::size_t kLookupTableBytes = + segmented_number_internal::kUseLookup + ? kCodeCount * (sizeof(std::int64_t) + sizeof(std::uint32_t)) + : 0; + + constexpr SegmentedNumber() : value_(Decode(wire_type{})) {} + + runtime_type Value() const { return value_; } + + static constexpr runtime_type Decode(wire_type wire) { + std::uint32_t rank = segmented_number_internal::WireToRank(wire); + if (rank >= kCodeCount) { + rank = 0; + } + std::int64_t const raw = segmented_number_internal::Codec< + Spec, logical_type, kCodeCount, + segmented_number_internal::kUseLookup>::Decode( + kSegments.data(), kPlan.count, rank); + return segmented_number_internal::RuntimeRawMap::FromRaw(raw); + } + + static std::optional TryEncode(runtime_type value) { + std::int64_t const raw = + segmented_number_internal::RuntimeRawMap::ToRaw(value); + if (raw < kRawMin || raw > kRawMax) { + return std::nullopt; + } + return segmented_number_internal::RankToWire(EncodeRaw(raw)); + } + + static std::optional TryFromRuntime(runtime_type value) { + auto const w = TryEncode(value); + if (!w.has_value()) { + return std::nullopt; + } + return FromWire(*w); + } + + static SegmentedNumber Saturating(runtime_type value) { + std::int64_t raw = + segmented_number_internal::RuntimeRawMap::ToRaw(value); + if (raw < kRawMin) { + raw = kRawMin; + } + if (raw > kRawMax) { + raw = kRawMax; + } + return FromWire(segmented_number_internal::RankToWire( + EncodeRaw(raw))); + } + + static SegmentedNumber FromRuntimeUnchecked(runtime_type value) { + return FromWire(segmented_number_internal::RankToWire( + EncodeRaw(segmented_number_internal::RuntimeRawMap< + logical_type, runtime_type>::ToRaw(value)))); + } + + static SegmentedNumber FromWire(wire_type wire) { + SegmentedNumber n; + n.value_ = Decode(wire); + return n; + } + + constexpr bool operator==(SegmentedNumber const& o) const { + return value_ == o.value_; + } + constexpr bool operator!=(SegmentedNumber const& o) const { + return value_ != o.value_; + } + constexpr bool operator<(SegmentedNumber const& o) const { + return value_ < o.value_; + } + constexpr bool operator>(SegmentedNumber const& o) const { + return value_ > o.value_; + } + constexpr bool operator<=(SegmentedNumber const& o) const { + return value_ <= o.value_; + } + constexpr bool operator>=(SegmentedNumber const& o) const { + return value_ >= o.value_; + } + + static std::size_t Serialize(SegmentedNumber const& value, std::uint8_t* out) { + auto const enc = TryEncode(value.Value()); + if (!enc.has_value()) { + return 0; + } + return wire_traits::Serialize(*enc, out); + } + + static DeserializeResult Deserialize(std::uint8_t const* in, + std::size_t len) { + if (in == nullptr || len == 0) { + return {SegmentedNumber{}, 0}; + } + if constexpr (!std::is_same_v) { + if (wire_type::WireBytesNeeded(in, len) == 0) { + return {SegmentedNumber{}, 0}; + } + } else if (len < 1) { + return {SegmentedNumber{}, 0}; + } + auto const wr = wire_traits::Deserialize(in, len); + if (wr.bytes_read == 0 || wr.bytes_read > len) { + return {SegmentedNumber{}, 0}; + } + std::uint32_t const rank = + segmented_number_internal::WireToRank(wr.value); + if (rank >= kCodeCount) { + return {SegmentedNumber{}, 0}; + } + return {FromWire(wr.value), wr.bytes_read}; + } + + static constexpr std::size_t MaxWireBytes() { return kMaxWireBytes; } + + static constexpr auto const& Logical() { return kPlan; } + + private: + static constexpr std::int64_t kRawMin = std::invoke([]() { + std::int64_t m = segmented_formula_internal::DecodeRankRaw( + kSegments.data(), kPlan.count, 0); + for (std::uint32_t r = 1; r < static_cast(kCodeCount); ++r) { + std::int64_t const v = + segmented_formula_internal::DecodeRankRaw( + kSegments.data(), kPlan.count, r); + if (v < m) { + m = v; + } + } + return m; + }); + static constexpr std::int64_t kRawMax = std::invoke([]() { + std::int64_t m = segmented_formula_internal::DecodeRankRaw( + kSegments.data(), kPlan.count, 0); + for (std::uint32_t r = 1; r < static_cast(kCodeCount); ++r) { + std::int64_t const v = + segmented_formula_internal::DecodeRankRaw( + kSegments.data(), kPlan.count, r); + if (v > m) { + m = v; + } + } + return m; + }); + + static constexpr std::uint32_t EncodeRaw(std::int64_t raw) { + return segmented_number_internal::Codec< + Spec, logical_type, kCodeCount, + segmented_number_internal::kUseLookup>::Encode( + kSegments.data(), kPlan.count, static_cast(kCodeCount), + raw); + } + + runtime_type value_{}; +}; + +template +using Compile = SegmentedNumber; + +} // namespace ae::seg + +namespace ae::seg { +namespace segmented_number_size_internal { +template +constexpr bool SizeMatches() { + return sizeof(SegmentedNumber) == + sizeof(typename SegmentedNumber::runtime_type); +} +} // namespace segmented_number_size_internal +} // namespace ae::seg + +namespace ae { + +using seg::SegmentedNumber; + +template +struct numeric_traits> { + using value_type = seg::SegmentedNumber; + using rep_type = typename value_type::wire_type; + using rep_value_type = typename numeric_traits::rep_value_type; + + static constexpr bool kIsIntegerLike = false; + static constexpr bool kIsFixedPoint = false; + static constexpr bool kIsExponential = false; + static constexpr bool kIsSegmented = true; + static constexpr bool kIsSigned = + numeric_traits::kIsSigned; +}; + +template +struct runtime_numeric_traits> { + using value_type = seg::SegmentedNumber; + using RT = typename value_type::runtime_type; + + static constexpr bool kIsSupported = true; + static constexpr bool kIsSigned = runtime_numeric_traits::kIsSigned; + + static constexpr value_type FromInteger(std::int64_t value) { + return value_type::Saturating(runtime_numeric_traits::FromInteger(value)); + } + + static consteval value_type FromDouble(double value) { + return value_type::Saturating(runtime_numeric_traits::FromDouble(value)); + } +}; + +} // namespace ae + +#endif // AE_NUMERIC_SEGMENTED_NUMBER_H_ diff --git a/ae-numeric/segmented_number_floating_runtime.h b/ae-numeric/segmented_number_floating_runtime.h new file mode 100644 index 0000000..d70f812 --- /dev/null +++ b/ae-numeric/segmented_number_floating_runtime.h @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_SEGMENTED_NUMBER_FLOATING_RUNTIME_H_ +#define AE_NUMERIC_SEGMENTED_NUMBER_FLOATING_RUNTIME_H_ + +#include "ae-numeric/exponential.h" +#include "ae-numeric/exponential_floating_runtime.h" +#include "ae-numeric/segmented_number.h" + +namespace ae::seg { + +template <> +inline constexpr bool kFloatingRuntimeEnabled = true; +template <> +inline constexpr bool kFloatingRuntimeEnabled = true; + +namespace segmented_number_internal { + +template +constexpr std::int64_t FloatingToLogicalRaw(FloatingT value) { + if (value == FloatingT{0}) { + return 0; + } + bool const neg = value < FloatingT{0}; + FloatingT const mag = neg ? -value : value; + Logical const work = + exponential_internal::FloatingToWork(mag); + std::int64_t const raw = static_cast(work.RawValue()); + return neg ? -raw : raw; +} + +template +constexpr FloatingT LogicalRawToFloating(std::int64_t raw) { + bool const neg = raw < 0; + std::int64_t ar = neg ? -raw : raw; + if (ar > static_cast(Logical::kRawMax)) { + ar = static_cast(Logical::kRawMax); + } + Logical const work = Logical::FromRaw( + fixed_point_internal::RepFromRawValue( + static_cast(ar))); + FloatingT const mag = + exponential_internal::WorkToFloating(work); + return neg ? -mag : mag; +} + +template +struct RuntimeRawMap { + static constexpr std::int64_t ToRaw(float v) { + return FloatingToLogicalRaw(v); + } + static constexpr float FromRaw(std::int64_t raw) { + return LogicalRawToFloating(raw); + } +}; + +template +struct RuntimeRawMap { + static constexpr std::int64_t ToRaw(double v) { + return FloatingToLogicalRaw(v); + } + static constexpr double FromRaw(std::int64_t raw) { + return LogicalRawToFloating(raw); + } +}; + +} // namespace segmented_number_internal +} // namespace ae::seg + +#endif // AE_NUMERIC_SEGMENTED_NUMBER_FLOATING_RUNTIME_H_ diff --git a/ae-numeric/segmented_number_wire_io.h b/ae-numeric/segmented_number_wire_io.h new file mode 100644 index 0000000..ebb3c8f --- /dev/null +++ b/ae-numeric/segmented_number_wire_io.h @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_SEGMENTED_NUMBER_WIRE_IO_H_ +#define AE_NUMERIC_SEGMENTED_NUMBER_WIRE_IO_H_ + +#include + +#include "ae-numeric/segmented_number.h" +#include "ae-numeric/wire_io.h" + +namespace ae { + +template +struct wire_traits> { + using T = seg::SegmentedNumber; + using WireTraits = wire_traits; + + static constexpr std::size_t kMaxWireBytes = T::kMaxWireBytes; + + static std::size_t Serialize(T const& value, std::uint8_t* out) { + assert(out != nullptr); + return T::Serialize(value, out); + } + + static DeserializeResult Deserialize(std::uint8_t const* in, + std::size_t len) { + return T::Deserialize(in, len); + } +}; + +} // namespace ae + +#endif // AE_NUMERIC_SEGMENTED_NUMBER_WIRE_IO_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 20f78cd..32cf8e0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,6 +50,12 @@ target_sources(${PROJECT_NAME} PRIVATE test-composed-types.cpp test-composed-exponential.cpp test-composed-exponential-tiered.cpp + test-segmented-number-core.cpp + test-segmented-number-formats.cpp + test-segmented-number-wire.cpp + test-segmented-number-formula-lookup.cpp + test-segmented-number-floating-runtime.cpp + test-segmented-number-size.cpp test-fixed-math.cpp test-integer-math.cpp test-tiered-int-aether-compat.cpp @@ -61,8 +67,8 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ae-numeric unity) # enable warnings and werror target_compile_options(${PROJECT_NAME} PRIVATE $<$: -Wall -Wextra -Werror> - $<$: -Wall -Wextra -Werror> - $<$:/W4 /WX /MP /constexpr:steps16777216 /wd4530 /wd4702 /wd4127> + $<$: -Wall -Wextra -Werror -fconstexpr-ops-limit=268435456 -fconstexpr-loop-limit=1048576> + $<$:/W4 /WX /MP /constexpr:steps100000000 /wd4530 /wd4702 /wd4127> ) @@ -115,7 +121,17 @@ foreach(fail_case exp_float_runtime wide_mul_intermediate tiered_add_overflow - tiered_add_underflow) + tiered_add_underflow + segmented_overlap + segmented_gap + segmented_duplicate_boundary + segmented_exponential_non_positive + segmented_too_many_one_byte_codes + segmented_impossible_error + segmented_runtime_rep_too_small + segmented_invalid_wire_bytes + segmented_duplicate_runtime_value + segmented_impossible_continuous_step) add_executable(fail-${fail_case} EXCLUDE_FROM_ALL compile-fail/${fail_case}.cpp) target_link_libraries(fail-${fail_case} PRIVATE ae-numeric) @@ -124,3 +140,45 @@ foreach(fail_case --target fail-${fail_case} --config $) set_tests_properties(fail-${fail_case} PROPERTIES WILL_FAIL TRUE) endforeach() + +function(ae_numeric_add_footprint tgt) + add_executable(${tgt} EXCLUDE_FROM_ALL footprint/segmented_footprint.cpp) + target_link_libraries(${tgt} PRIVATE ae-numeric) + target_compile_definitions(${tgt} PRIVATE ${ARGN}) + target_compile_options(${tgt} PRIVATE + $<$: -Wall -Wextra -Werror> + $<$: -Wall -Wextra -Werror -fconstexpr-ops-limit=268435456 -fconstexpr-loop-limit=1048576 -ffunction-sections -fdata-sections> + $<$:/W4 /WX /MP /constexpr:steps100000000 /Gy /wd4530 /wd4702 /wd4127> + ) + target_link_options(${tgt} PRIVATE + $<$:-Wl,--gc-sections> + $<$:-Wl,--gc-sections> + $<$:/OPT:REF> + ) +endfunction() + +ae_numeric_add_footprint(footprint-rssi AE_SEG_HAS_RSSI=1) +ae_numeric_add_footprint(footprint-temperature AE_SEG_HAS_TEMP=1) +ae_numeric_add_footprint(footprint-humidity AE_SEG_HAS_HUM=1) +ae_numeric_add_footprint(footprint-co2 AE_SEG_HAS_CO2=1) +ae_numeric_add_footprint(footprint-rx-window AE_SEG_HAS_RX=1) +ae_numeric_add_footprint(footprint-battery AE_SEG_HAS_BAT=1) +ae_numeric_add_footprint(footprint-connect-duration AE_SEG_HAS_CONN=1) +ae_numeric_add_footprint(footprint-all-formula AE_SEG_FOOTPRINT_ALL=1) +ae_numeric_add_footprint(footprint-rssi-lookup AE_SEG_HAS_RSSI_LOOKUP=1) +ae_numeric_add_footprint(footprint-temperature-lookup AE_SEG_HAS_TEMP_LOOKUP=1) +ae_numeric_add_footprint(footprint-all-lookup AE_SEG_FOOTPRINT_ALL_LOOKUP=1) + +add_custom_target(segmented-footprint DEPENDS + footprint-rssi + footprint-temperature + footprint-humidity + footprint-co2 + footprint-rx-window + footprint-battery + footprint-connect-duration + footprint-all-formula + footprint-rssi-lookup + footprint-temperature-lookup + footprint-all-lookup +) diff --git a/tests/compile-fail/segmented_duplicate_boundary.cpp b/tests/compile-fail/segmented_duplicate_boundary.cpp new file mode 100644 index 0000000..59f886e --- /dev/null +++ b/tests/compile-fail/segmented_duplicate_boundary.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout< + ae::seg::UniformStep, D<10>>, ae::seg::Step>, + ae::seg::Place>>, + ae::seg::UniformStep, D<20>>, ae::seg::Step>, + ae::seg::Place>>, + ae::seg::UniformStep, D<15>>, ae::seg::Step>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_duplicate_runtime_value.cpp b/tests/compile-fail/segmented_duplicate_runtime_value.cpp new file mode 100644 index 0000000..cb5a9e5 --- /dev/null +++ b/tests/compile-fail/segmented_duplicate_runtime_value.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<1>>, ae::seg::Intervals<200>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_exponential_non_positive.cpp b/tests/compile-fail/segmented_exponential_non_positive.cpp new file mode 100644 index 0000000..f0622a7 --- /dev/null +++ b/tests/compile-fail/segmented_exponential_non_positive.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<10>>, ae::seg::Intervals<8>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_gap.cpp b/tests/compile-fail/segmented_gap.cpp new file mode 100644 index 0000000..07f545c --- /dev/null +++ b/tests/compile-fail/segmented_gap.cpp @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout< + ae::seg::UniformStep, D<10>>, ae::seg::Step>, + ae::seg::Place>>, + ae::seg::UniformStep, D<30>>, ae::seg::Step>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_impossible_continuous_step.cpp b/tests/compile-fail/segmented_impossible_continuous_step.cpp new file mode 100644 index 0000000..7c0ef5c --- /dev/null +++ b/tests/compile-fail/segmented_impossible_continuous_step.cpp @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<10>>, ae::seg::Intervals<8>, + ae::seg::StepAtLower, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_impossible_error.cpp b/tests/compile-fail/segmented_impossible_error.cpp new file mode 100644 index 0000000..7424a2e --- /dev/null +++ b/tests/compile-fail/segmented_impossible_error.cpp @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<100>>, ae::seg::Intervals<10>, + ae::seg::StepAtLower>, ae::seg::MaxAbsErrorAtLower>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_invalid_wire_bytes.cpp b/tests/compile-fail/segmented_invalid_wire_bytes.cpp new file mode 100644 index 0000000..aa7738d --- /dev/null +++ b/tests/compile-fail/segmented_invalid_wire_bytes.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<10>>, ae::seg::Step>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_overlap.cpp b/tests/compile-fail/segmented_overlap.cpp new file mode 100644 index 0000000..1ce550b --- /dev/null +++ b/tests/compile-fail/segmented_overlap.cpp @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout< + ae::seg::UniformStep, D<10>>, ae::seg::Step>, + ae::seg::Place>>, + ae::seg::UniformStep, D<20>>, ae::seg::Step>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_runtime_rep_too_small.cpp b/tests/compile-fail/segmented_runtime_rep_too_small.cpp new file mode 100644 index 0000000..4aa3e8a --- /dev/null +++ b/tests/compile-fail/segmented_runtime_rep_too_small.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<10>>, ae::seg::Intervals<250>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/compile-fail/segmented_too_many_one_byte_codes.cpp b/tests/compile-fail/segmented_too_many_one_byte_codes.cpp new file mode 100644 index 0000000..663dda7 --- /dev/null +++ b/tests/compile-fail/segmented_too_many_one_byte_codes.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +template +using D = ae::Decimal; + +using Bad = ae::seg::Compile, + ae::seg::wire::AutoTiered>, + ae::seg::compute::Formula, + ae::seg::Layout, D<300>>, ae::seg::Step>, + ae::seg::Place>>>>>; + +Bad value{}; + +int main() { + (void)value; + return 0; +} diff --git a/tests/footprint/segmented_footprint.cpp b/tests/footprint/segmented_footprint.cpp new file mode 100644 index 0000000..e8fbe57 --- /dev/null +++ b/tests/footprint/segmented_footprint.cpp @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "../segmented_test_formats.h" + +namespace { + +#if defined(AE_SEG_FOOTPRINT_ALL) +#define AE_SEG_HAS_RSSI 1 +#define AE_SEG_HAS_TEMP 1 +#define AE_SEG_HAS_HUM 1 +#define AE_SEG_HAS_CO2 1 +#define AE_SEG_HAS_RX 1 +#define AE_SEG_HAS_BAT 1 +#define AE_SEG_HAS_CONN 1 +#endif + +template +void SinkOne(char const* name) { + volatile std::size_t sink = 0; + sink += sizeof(Num); + sink += sizeof(typename Num::runtime_type); + sink += sizeof(typename Num::wire_type); + sink += Num::kCodeCount; + sink += Num::kMaxWireBytes; + sink += Num::kSegmentCount; + sink += Num::kFormulaCoefficientBytes; + sink += Num::kLookupTableBytes; + auto const n = Num::Saturating(Num::runtime_type::FromInteger(1)); + std::uint8_t buf[8] = {}; + sink += Num::Serialize(n, buf); + auto const back = Num::Deserialize(buf, sizeof(buf)); + sink += back.bytes_read; + std::printf( + "%s sizeof(runtime)=%zu sizeof(number)=%zu sizeof(wire)=%zu " + "codes=%zu max_bytes=%zu segs=%zu formula_bytes=%zu lookup_bytes=%zu " + "sink=%zu\n", + name, sizeof(typename Num::runtime_type), sizeof(Num), + sizeof(typename Num::wire_type), Num::kCodeCount, Num::kMaxWireBytes, + Num::kSegmentCount, Num::kFormulaCoefficientBytes, Num::kLookupTableBytes, + static_cast(sink)); +} + +} // namespace + +int main() { +#if defined(AE_SEG_HAS_RSSI) + SinkOne("rssi"); +#endif +#if defined(AE_SEG_HAS_TEMP) + SinkOne("temperature"); +#endif +#if defined(AE_SEG_HAS_HUM) + SinkOne("humidity"); +#endif +#if defined(AE_SEG_HAS_CO2) + SinkOne("co2"); +#endif +#if defined(AE_SEG_HAS_RX) + SinkOne("rx-window"); +#endif +#if defined(AE_SEG_HAS_BAT) + SinkOne("battery"); +#endif +#if defined(AE_SEG_HAS_CONN) + SinkOne("connect-duration"); +#endif +#if defined(AE_SEG_HAS_RSSI_LOOKUP) || defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) + SinkOne("rssi-lookup"); +#endif +#if defined(AE_SEG_HAS_TEMP_LOOKUP) || defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) + SinkOne("temperature-lookup"); +#endif +#if defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) + SinkOne("humidity-lookup"); + SinkOne("co2-lookup"); + SinkOne("battery-lookup"); + SinkOne("connect-lookup"); +#endif + return 0; +} diff --git a/tests/main.cpp b/tests/main.cpp index d0379ac..46d0a3b 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -33,6 +33,12 @@ extern int test_packed_ring(); extern int test_composed_types(); extern int test_composed_exponential(); extern int test_composed_exponential_tiered(); +extern int test_segmented_number_core(); +extern int test_segmented_number_formats(); +extern int test_segmented_number_wire(); +extern int test_segmented_number_formula_lookup(); +extern int test_segmented_number_floating_runtime(); +extern int test_segmented_number_size(); extern int test_fixed_math(); extern int test_integer_math(); extern int test_tiered_int_aether_compat(); @@ -54,6 +60,12 @@ int main() { res += test_composed_types(); res += test_composed_exponential(); res += test_composed_exponential_tiered(); + res += test_segmented_number_core(); + res += test_segmented_number_formats(); + res += test_segmented_number_wire(); + res += test_segmented_number_formula_lookup(); + res += test_segmented_number_floating_runtime(); + res += test_segmented_number_size(); res += test_fixed_math(); res += test_integer_math(); res += test_tiered_int_aether_compat(); diff --git a/tests/segmented_test_formats.h b/tests/segmented_test_formats.h new file mode 100644 index 0000000..da6bc40 --- /dev/null +++ b/tests/segmented_test_formats.h @@ -0,0 +1,178 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_TESTS_SEGMENTED_TEST_FORMATS_H_ +#define AE_NUMERIC_TESTS_SEGMENTED_TEST_FORMATS_H_ + +#include + +#include +#include + +namespace ae::test_segmented_formats { + +template +using D = Decimal; + +using RssiSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<0>>, seg::Step>, + seg::Place>>>>; +using Rssi = seg::Compile; + +using TemperatureSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::GeometricStep, D<10>>, seg::Intervals<349>, + seg::StepAtUpper>, + seg::Place>>, + seg::UniformStep, D<352, -1>>, seg::Step>, + seg::Place>>, + seg::GeometricStep, D<125>>, seg::Intervals<419>, + seg::StepAtLower>, + seg::Place>>>>; +using Temperature = seg::Compile; + +using HumiditySpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::LinearStepRamp, D<20>>, seg::Intervals<45>, + seg::StepAtUpper, + seg::Place>>, + seg::UniformValues, D<80>>, seg::Intervals<168>, + seg::Place>>, + seg::LinearStepRamp, D<100>>, seg::Intervals<42>, + seg::StepAtLower, + seg::Place>>>>; +using Humidity = seg::Compile; + +using Co2Spec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<32000>>, seg::ExactEndpoints, + seg::WireCuts, seg::Bytes<1>>, + seg::ApproximateCut, seg::Bytes<2>>, + seg::Rest>>, + seg::OptimizeCuts>>>; +using Co2 = seg::Compile; + +using RxWindowSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::AutoSplit, seg::Place>, + seg::Objective, + seg::ExponentialValues, D<1>>>, + seg::ExponentialValues, D<60>>>>, + seg::GeometricStep, D<3600>>, seg::FillTier, + seg::StepAtLower, + seg::Place>>, + seg::LinearStepRamp, D<86400>>, + seg::MinimumIntervals, + seg::StepAtLower, + seg::MaxAbsErrorAtUpper>, + seg::Place>>>>; +using RxWindow = seg::Compile; + +using BatteryVoltageSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::GeometricStep, D<275, -2>>, + seg::Intervals<130>, seg::StepAtUpper>, + seg::Place>>, + seg::UniformStep, D<300, -2>>, + seg::Step>, + seg::Place>>>>; +using Battery = seg::Compile; + +using ConnectDurationSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, seg::Place>, + seg::Objective, + seg::ExponentialValues, D<2>>>, + seg::ExponentialValues, D<60>>>>>>; +using ConnectDuration = seg::Compile; + +using TemperatureLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, + typename TemperatureSpec::layout_type>; +using TemperatureLookup = seg::Compile; + +using RssiLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename RssiSpec::layout_type>; +using RssiLookup = seg::Compile; + +using HumidityLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename HumiditySpec::layout_type>; +using HumidityLookup = seg::Compile; + +using Co2LookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename Co2Spec::layout_type>; +using Co2Lookup = seg::Compile; + +using BatteryLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename BatteryVoltageSpec::layout_type>; +using BatteryLookup = seg::Compile; + +using ConnectLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename ConnectDurationSpec::layout_type>; +using ConnectLookup = seg::Compile; + +using RxWindowLookupSpec = seg::Format< + seg::runtime::Fixed, + seg::wire::AutoTiered>, + seg::compute::Lookup, typename RxWindowSpec::layout_type>; +using RxWindowLookup = seg::Compile; + +template +typename Num::wire_type WireFromRank(std::uint32_t rank) { + if constexpr (std::is_same_v) { + return static_cast(rank); + } else { + return typename Num::wire_type{rank}; + } +} + +} // namespace ae::test_segmented_formats + +#endif // AE_NUMERIC_TESTS_SEGMENTED_TEST_FORMATS_H_ diff --git a/tests/test-integer-math.cpp b/tests/test-integer-math.cpp index f19ceec..3a61a19 100644 --- a/tests/test-integer-math.cpp +++ b/tests/test-integer-math.cpp @@ -41,6 +41,7 @@ struct RuntimeFns { &RoundMulPow2DivU64; bool (*div_pow2)(std::uint64_t, std::uint64_t, unsigned, std::uint64_t&) = &RoundDivPow2U64; + std::uint64_t (*sqrt_u64)(std::uint64_t) = &SqrtU64; }; RuntimeFns const& Fns() { @@ -460,6 +461,18 @@ void test_RawFromRatioExhaustiveSmall() { } } +void test_SqrtU64() { + auto const& f = Fns(); + TEST_ASSERT_EQUAL_UINT(0U, f.sqrt_u64(0)); + TEST_ASSERT_EQUAL_UINT(1U, f.sqrt_u64(1)); + TEST_ASSERT_EQUAL_UINT(1U, f.sqrt_u64(3)); + TEST_ASSERT_EQUAL_UINT(2U, f.sqrt_u64(4)); + TEST_ASSERT_EQUAL_UINT(10U, f.sqrt_u64(100)); + TEST_ASSERT_EQUAL_UINT(255U, f.sqrt_u64(65535)); + TEST_ASSERT_EQUAL_UINT(65536U, f.sqrt_u64(65536ULL * 65536ULL)); + TEST_ASSERT_EQUAL_UINT(4294967295ULL, f.sqrt_u64(~0ULL)); +} + } // namespace ae::test_integer_math int test_integer_math() { @@ -484,5 +497,6 @@ int test_integer_math() { RUN_TEST(ae::test_integer_math::test_RawFromRatioGcdAndRounding); RUN_TEST(ae::test_integer_math::test_RawFromRatioOverflowButFitsAndBounds); RUN_TEST(ae::test_integer_math::test_RawFromRatioExhaustiveSmall); + RUN_TEST(ae::test_integer_math::test_SqrtU64); return UNITY_END(); } diff --git a/tests/test-segmented-number-core.cpp b/tests/test-segmented-number-core.cpp new file mode 100644 index 0000000..834de04 --- /dev/null +++ b/tests/test-segmented-number-core.cpp @@ -0,0 +1,188 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_core { + +using test_segmented_formats::Rssi; +using test_segmented_formats::Temperature; + +template +double AsDouble(RT const& v) { + return std::ldexp(static_cast(v.RawValue()), RT::kScaleExp); +} + +template +std::size_t WireSizeOf(typename Num::runtime_type const& v) { + std::uint8_t buf[8] = {}; + auto const n = Num::TryFromRuntime(v); + TEST_ASSERT(n.has_value()); + return Num::Serialize(*n, buf); +} + +template +void CheckRankRoundTrip() { + for (std::uint32_t rank = 0; rank < static_cast(Num::kCodeCount); + ++rank) { + typename Num::wire_type const w = + test_segmented_formats::WireFromRank(rank); + auto const decoded = Num::Decode(w); + auto const enc = Num::TryEncode(decoded); + TEST_ASSERT(enc.has_value()); + TEST_ASSERT_EQUAL_UINT(rank, static_cast(*enc)); + } +} + +static_assert(sizeof(Rssi) == sizeof(Rssi::runtime_type)); +static_assert(Rssi::kCodeCount == 128); +static_assert(std::is_same_v); +static_assert(Rssi::kMaxWireBytes == 1); +static_assert(Rssi::kLookupTableBytes == 0); +static_assert(sizeof(Temperature) == sizeof(Temperature::runtime_type)); +static_assert(Temperature::kCodeCount == 1021); +static_assert(Temperature::kOneByteCount == 253); +static_assert(Temperature::kTwoByteCount == 768); +static_assert(Temperature::kMaxWireBytes == 2); +static_assert(std::is_same_v>); +static_assert(numeric_traits::kIsSegmented); + +void test_RssiLayout() { + TEST_ASSERT_EQUAL_UINT(128U, Rssi::kCodeCount); + TEST_ASSERT_EQUAL_UINT(1U, Rssi::kMaxWireBytes); + TEST_ASSERT_EQUAL_UINT(128U, Rssi::kOneByteCount); + TEST_ASSERT_EQUAL_UINT(0U, Rssi::kTwoByteCount); +} + +void test_RssiEndpoints() { + auto const lo = Rssi::TryFromRuntime(Rssi::runtime_type::FromInteger(-127)); + auto const hi = Rssi::TryFromRuntime(Rssi::runtime_type::FromInteger(0)); + TEST_ASSERT(lo.has_value()); + TEST_ASSERT(hi.has_value()); + TEST_ASSERT_EQUAL(-127, lo->Value().RawValue()); + TEST_ASSERT_EQUAL(0, hi->Value().RawValue()); +} + +void test_RssiRoundTrip() { CheckRankRoundTrip(); } + +void test_RssiUnusedRank() { + std::uint8_t buf[1] = {200}; + auto const r = Rssi::Deserialize(buf, 1); + TEST_ASSERT_EQUAL_UINT(0U, r.bytes_read); + std::uint8_t empty[1] = {0}; + auto const t = Rssi::Deserialize(empty, 0); + TEST_ASSERT_EQUAL_UINT(0U, t.bytes_read); +} + +void test_RssiOutOfRange() { + auto const too_hi = + Rssi::TryFromRuntime(Rssi::runtime_type::FromInteger(1)); + TEST_ASSERT_FALSE(too_hi.has_value()); + auto const sat = Rssi::Saturating(Rssi::runtime_type::FromInteger(1)); + TEST_ASSERT_EQUAL(0, sat.Value().RawValue()); +} + +void test_RssiFractional() { + auto const n = + Rssi::TryFromRuntime(Rssi::runtime_type::FromRatio(-1275, 10)); + TEST_ASSERT(n.has_value()); + TEST_ASSERT_DOUBLE_WITHIN(0.5, -127.5, AsDouble(n->Value())); +} + +void test_TemperatureGolden() { + TEST_ASSERT_EQUAL_UINT(1021U, Temperature::kCodeCount); + TEST_ASSERT_EQUAL_UINT(253U, Temperature::kOneByteCount); + TEST_ASSERT_EQUAL_UINT(768U, Temperature::kTwoByteCount); + TEST_ASSERT_EQUAL_UINT(2U, Temperature::kMaxWireBytes); + auto const& p = Temperature::Logical(); + TEST_ASSERT_EQUAL(3, p.count); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0019571403660685, p.segs[0].q); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0032825748092233, p.segs[2].q); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.1974705407, p.segs[0].step0); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.3934835786, p.segs[2].last_step); +} + +void test_TemperatureEndpointsAndWire() { + using RT = Temperature::runtime_type; + TEST_ASSERT_EQUAL_UINT(2U, WireSizeOf(RT::FromInteger(-40))); + TEST_ASSERT_EQUAL_UINT(1U, WireSizeOf(RT::FromInteger(10))); + TEST_ASSERT_EQUAL_UINT(1U, WireSizeOf(RT::FromRatio(352, 10))); + TEST_ASSERT_EQUAL_UINT(1U, WireSizeOf(RT::FromInteger(20))); + TEST_ASSERT_EQUAL_UINT(2U, WireSizeOf(RT::FromInteger(125))); + TEST_ASSERT_EQUAL_UINT(2U, WireSizeOf(RT::FromRatio(99, 10))); + TEST_ASSERT_EQUAL_UINT(2U, WireSizeOf(RT::FromRatio(353, 10))); + + auto const a = Temperature::TryFromRuntime(RT::FromInteger(-40)); + auto const b = Temperature::TryFromRuntime(RT::FromInteger(10)); + auto const c = Temperature::TryFromRuntime(RT::FromRatio(352, 10)); + auto const d = Temperature::TryFromRuntime(RT::FromInteger(125)); + TEST_ASSERT(a && b && c && d); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-3, -40.0, AsDouble(a->Value())); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-3, 10.0, AsDouble(b->Value())); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-3, 35.2, AsDouble(c->Value())); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-3, 125.0, AsDouble(d->Value())); +} + +void test_TemperatureQuantization() { + using RT = Temperature::runtime_type; + auto max_err = [](double lo, double hi, int steps) { + double m = 0.0; + for (int i = 0; i <= steps; ++i) { + double const x = lo + (hi - lo) * static_cast(i) / + static_cast(steps); + auto const v = RT::FromRatio(static_cast(std::llround(x * 100.0)), + 100); + auto const n = Temperature::TryFromRuntime(v); + TEST_ASSERT(n.has_value()); + m = std::max(m, std::fabs(AsDouble(n->Value()) - x)); + } + return m; + }; + TEST_ASSERT(max_err(9.9, 10.1, 40) <= 0.06); + TEST_ASSERT(max_err(35.1, 35.3, 40) <= 0.06); + TEST_ASSERT(max_err(-40.0, -39.7, 40) <= 0.11); + TEST_ASSERT(max_err(124.6, 125.0, 40) <= 0.22); +} + +void test_TemperatureRoundTrip() { CheckRankRoundTrip(); } + +} // namespace ae::test_segmented_number_core + +int test_segmented_number_core() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_core::test_RssiLayout); + RUN_TEST(ae::test_segmented_number_core::test_RssiEndpoints); + RUN_TEST(ae::test_segmented_number_core::test_RssiRoundTrip); + RUN_TEST(ae::test_segmented_number_core::test_RssiUnusedRank); + RUN_TEST(ae::test_segmented_number_core::test_RssiOutOfRange); + RUN_TEST(ae::test_segmented_number_core::test_RssiFractional); + RUN_TEST(ae::test_segmented_number_core::test_TemperatureGolden); + RUN_TEST(ae::test_segmented_number_core::test_TemperatureEndpointsAndWire); + RUN_TEST(ae::test_segmented_number_core::test_TemperatureQuantization); + RUN_TEST(ae::test_segmented_number_core::test_TemperatureRoundTrip); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-floating-runtime.cpp b/tests/test-segmented-number-floating-runtime.cpp new file mode 100644 index 0000000..78fc7e8 --- /dev/null +++ b/tests/test-segmented-number-floating-runtime.cpp @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_floating_runtime { + +using test_segmented_formats::RssiSpec; +using test_segmented_formats::TemperatureSpec; + +using RssiFloatSpec = seg::Format< + seg::runtime::Floating, + seg::wire::AutoTiered>, + seg::compute::Formula, typename RssiSpec::layout_type>; +using RssiFloat = seg::Compile; + +using TemperatureFloatSpec = seg::Format< + seg::runtime::Floating, + seg::wire::AutoTiered>, + seg::compute::Formula, typename TemperatureSpec::layout_type>; +using TemperatureFloat = seg::Compile; + +static_assert(sizeof(RssiFloat) == sizeof(double)); +static_assert(RssiFloat::kCodeCount == test_segmented_formats::Rssi::kCodeCount); +static_assert(std::is_same_v); +static_assert(TemperatureFloat::kCodeCount == + test_segmented_formats::Temperature::kCodeCount); +static_assert(std::is_same_v); + +void test_RssiFloatingWireMatchesFixed() { + using Fixed = test_segmented_formats::Rssi; + for (int v = -127; v <= 0; ++v) { + auto const f = Fixed::TryFromRuntime(Fixed::runtime_type::FromInteger(v)); + auto const d = RssiFloat::TryFromRuntime(static_cast(v)); + TEST_ASSERT(f.has_value()); + TEST_ASSERT(d.has_value()); + std::uint8_t a[2] = {}; + std::uint8_t b[2] = {}; + TEST_ASSERT_EQUAL_UINT(Fixed::Serialize(*f, a), RssiFloat::Serialize(*d, b)); + TEST_ASSERT_EQUAL_HEX8(a[0], b[0]); + } +} + +void test_TemperatureFloatingEndpoints() { + using Fixed = test_segmented_formats::Temperature; + double const pts[] = {-40.0, 10.0, 35.2, 125.0}; + for (double x : pts) { + auto const d = TemperatureFloat::TryFromRuntime(x); + TEST_ASSERT(d.has_value()); + std::uint8_t buf[4] = {}; + std::size_t const n = TemperatureFloat::Serialize(*d, buf); + TEST_ASSERT(n > 0); + auto const back = TemperatureFloat::Deserialize(buf, n); + TEST_ASSERT_EQUAL_UINT(n, back.bytes_read); + auto const enc = TemperatureFloat::TryEncode(d->Value()); + auto const fenc = Fixed::TryEncode(Fixed::Decode(*enc)); + TEST_ASSERT(enc.has_value()); + TEST_ASSERT(fenc.has_value()); + TEST_ASSERT_EQUAL_UINT(static_cast(*enc), + static_cast(*fenc)); + } +} + +} // namespace ae::test_segmented_number_floating_runtime + +int test_segmented_number_floating_runtime() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_floating_runtime:: + test_RssiFloatingWireMatchesFixed); + RUN_TEST(ae::test_segmented_number_floating_runtime:: + test_TemperatureFloatingEndpoints); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-formats.cpp b/tests/test-segmented-number-formats.cpp new file mode 100644 index 0000000..de48eb3 --- /dev/null +++ b/tests/test-segmented-number-formats.cpp @@ -0,0 +1,262 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_formats { + +using test_segmented_formats::Battery; +using test_segmented_formats::Co2; +using test_segmented_formats::ConnectDuration; +using test_segmented_formats::Humidity; +using test_segmented_formats::Rssi; +using test_segmented_formats::RxWindow; +using test_segmented_formats::Temperature; + +template +double AsDouble(RT const& v) { + return std::ldexp(static_cast(v.RawValue()), RT::kScaleExp); +} + +template +void CheckRankRoundTrip() { + for (std::uint32_t rank = 0; rank < static_cast(Num::kCodeCount); + ++rank) { + typename Num::wire_type const w = + test_segmented_formats::WireFromRank(rank); + auto const decoded = Num::Decode(w); + auto const enc = Num::TryEncode(decoded); + TEST_ASSERT(enc.has_value()); + TEST_ASSERT_EQUAL_UINT(rank, static_cast(*enc)); + } +} + +template +void CheckUniqueRaws() { + std::int64_t raws[4096]; + static_assert(Num::kCodeCount <= 4096); + for (std::uint32_t i = 0; i < static_cast(Num::kCodeCount); + ++i) { + raws[i] = static_cast( + Num::Decode(test_segmented_formats::WireFromRank(i)).RawValue()); + } + std::uint32_t dup = 0; + for (std::uint32_t i = 0; i < static_cast(Num::kCodeCount); + ++i) { + for (std::uint32_t j = i + 1; j < static_cast(Num::kCodeCount); + ++j) { + if (raws[i] == raws[j]) { + ++dup; + } + } + } + TEST_ASSERT_EQUAL_UINT(0U, dup); +} + +template +void CheckSerializeRoundTrip() { + for (std::uint32_t rank = 0; rank < static_cast(Num::kCodeCount); + ++rank) { + auto const n = + Num::FromWire(test_segmented_formats::WireFromRank(rank)); + std::uint8_t buf[8] = {}; + std::size_t const wrote = Num::Serialize(n, buf); + TEST_ASSERT(wrote > 0); + TEST_ASSERT(wrote <= Num::kMaxWireBytes); + auto const back = Num::Deserialize(buf, wrote); + TEST_ASSERT_EQUAL_UINT(wrote, back.bytes_read); + TEST_ASSERT(n == back.value); + } +} + +void test_HumidityGolden() { + TEST_ASSERT_EQUAL_UINT(256U, Humidity::kCodeCount); + TEST_ASSERT_EQUAL_UINT(1U, Humidity::kMaxWireBytes); + auto const& p = Humidity::Logical(); + TEST_ASSERT_EQUAL(45, p.segs[0].intervals); + TEST_ASSERT_EQUAL(168, p.segs[1].intervals); + TEST_ASSERT_EQUAL(42, p.segs[2].intervals); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-8, 60.0 / 168.0, p.segs[1].step0); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.5317460317, p.segs[0].step0); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.5952380952, p.segs[2].last_step); +} + +void test_HumidityErrors() { + using RT = Humidity::runtime_type; + auto max_err = [](double lo, double hi, int steps) { + double m = 0.0; + for (int i = 0; i <= steps; ++i) { + double const x = lo + (hi - lo) * static_cast(i) / + static_cast(steps); + auto const v = RT::FromRatio(static_cast(std::llround(x * 100.0)), + 100); + auto const n = Humidity::TryFromRuntime(v); + TEST_ASSERT(n.has_value()); + m = std::max(m, std::fabs(AsDouble(n->Value()) - x)); + } + return m; + }; + TEST_ASSERT(max_err(0.0, 0.6, 40) <= 0.29); + TEST_ASSERT(max_err(19.8, 20.2, 40) <= 0.20); + TEST_ASSERT(max_err(49.8, 50.2, 40) <= 0.20); + TEST_ASSERT(max_err(79.8, 80.2, 40) <= 0.20); + TEST_ASSERT(max_err(99.4, 100.0, 40) <= 0.32); +} + +void test_Co2Golden() { + TEST_ASSERT_EQUAL_UINT(822U, Co2::kCodeCount); + TEST_ASSERT_EQUAL_UINT(255U, Co2::kOneByteCount); + TEST_ASSERT_EQUAL_UINT(223U, Co2::kTwoByteCount); + TEST_ASSERT_EQUAL_UINT(4U, Co2::kMaxWireBytes); + auto const& p = Co2::Logical(); + TEST_ASSERT_EQUAL(821, p.segs[0].intervals); + TEST_ASSERT_EQUAL(254, p.segs[0].last_1); + TEST_ASSERT_EQUAL(477, p.segs[0].last_2); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.005414508222845, p.segs[0].r); +} + +void test_Co2DecodedCuts() { + auto at = [](std::uint32_t rank) { + return AsDouble(Co2::Decode(Co2::wire_type{rank})); + }; + TEST_ASSERT_DOUBLE_WITHIN(0.5, 380.0, at(0)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 1497.7907686, at(254)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 1505.9005690, at(255)); + TEST_ASSERT_DOUBLE_WITHIN(4.0, 4993.6617217, at(477)); + TEST_ASSERT_DOUBLE_WITHIN(4.0, 5020.6999442, at(478)); + TEST_ASSERT_DOUBLE_WITHIN(1.0, 32000.0, at(821)); +} + +void test_RxWindowGolden() { + auto const& p = RxWindow::Logical(); + TEST_ASSERT_EQUAL(132, p.segs[0].intervals); + TEST_ASSERT_EQUAL(122, p.segs[1].intervals); + TEST_ASSERT_EQUAL(255, p.segs[2].intervals); + TEST_ASSERT_EQUAL(2536, p.segs[3].intervals); + TEST_ASSERT_EQUAL_UINT(255U, RxWindow::kOneByteCount); + TEST_ASSERT_EQUAL_UINT(255U, RxWindow::kTwoByteCount); + TEST_ASSERT_EQUAL_UINT(3046U, RxWindow::kCodeCount); + TEST_ASSERT_EQUAL_UINT(4U, RxWindow::kMaxWireBytes); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0355033664891309, p.segs[0].r); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0341296978352505, p.segs[1].r); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.012401106168161, p.segs[2].q); +} + +void test_BatteryGolden() { + TEST_ASSERT_EQUAL_UINT(256U, Battery::kCodeCount); + auto const& p = Battery::Logical(); + TEST_ASSERT_EQUAL(130, p.segs[0].intervals); + TEST_ASSERT_EQUAL(125, p.segs[1].intervals); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0116049124714404, p.segs[0].q); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-7, 0.0088601265, p.segs[0].step0); +} + +void test_ConnectDurationGolden() { + TEST_ASSERT_EQUAL_UINT(256U, ConnectDuration::kCodeCount); + auto const& p = ConnectDuration::Logical(); + TEST_ASSERT_EQUAL(102, p.segs[0].intervals); + TEST_ASSERT_EQUAL(153, p.segs[1].intervals); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0228310927967654, p.segs[0].r); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0224789769119687, p.segs[1].r); +} + +void test_RoundTripAllFormats() { + CheckRankRoundTrip(); + CheckRankRoundTrip(); + CheckRankRoundTrip(); + CheckRankRoundTrip(); + CheckRankRoundTrip(); +} + +void test_UniqueAndSerializeSmall() { + CheckUniqueRaws(); + CheckUniqueRaws(); + CheckUniqueRaws(); + CheckUniqueRaws(); + CheckSerializeRoundTrip(); + CheckSerializeRoundTrip(); + CheckSerializeRoundTrip(); +} + +void test_Co2UniqueAndSerialize() { + CheckUniqueRaws(); + CheckSerializeRoundTrip(); +} + +template +double MaxAbsErrorRatio(std::int64_t den, std::int64_t n0, std::int64_t n1, + std::int64_t step) { + double m = 0.0; + for (std::int64_t n = n0; n <= n1; n += step) { + auto const v = Num::runtime_type::FromRatio(n, den); + auto const got = Num::TryFromRuntime(v); + if (!got.has_value()) { + continue; + } + double const x = static_cast(n) / static_cast(den); + m = std::max(m, std::fabs(AsDouble(got->Value()) - x)); + } + return m; +} + +void test_DenseSampling() { + TEST_ASSERT(MaxAbsErrorRatio(100, -4000, 12500, 1) <= 0.22); + TEST_ASSERT(MaxAbsErrorRatio(100, 0, 10000, 1) <= 0.32); + TEST_ASSERT(MaxAbsErrorRatio(10, -1270, 0, 1) <= 0.51); + TEST_ASSERT(MaxAbsErrorRatio(10000, 21500, 30000, 1) <= 0.005); + TEST_ASSERT(MaxAbsErrorRatio(1000, 200, 60000, 1) <= 0.70); + double co2_rel = 0.0; + for (int ppm = 380; ppm <= 32000; ++ppm) { + auto const v = Co2::runtime_type::FromInteger(ppm); + auto const n = Co2::TryFromRuntime(v); + TEST_ASSERT(n.has_value()); + double const got = AsDouble(n->Value()); + double const rel = std::fabs(got - static_cast(ppm)) / + static_cast(ppm); + co2_rel = std::max(co2_rel, rel); + } + TEST_ASSERT(co2_rel <= 0.0035); + TEST_ASSERT(MaxAbsErrorRatio(1, 1, 86400, 1) <= 23.0); + TEST_ASSERT(MaxAbsErrorRatio(1, 86000, 86400, 1) <= 12.0); +} + +} // namespace ae::test_segmented_number_formats + +int test_segmented_number_formats() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_formats::test_HumidityGolden); + RUN_TEST(ae::test_segmented_number_formats::test_HumidityErrors); + RUN_TEST(ae::test_segmented_number_formats::test_Co2Golden); + RUN_TEST(ae::test_segmented_number_formats::test_Co2DecodedCuts); + RUN_TEST(ae::test_segmented_number_formats::test_RxWindowGolden); + RUN_TEST(ae::test_segmented_number_formats::test_BatteryGolden); + RUN_TEST(ae::test_segmented_number_formats::test_ConnectDurationGolden); + RUN_TEST(ae::test_segmented_number_formats::test_RoundTripAllFormats); + RUN_TEST(ae::test_segmented_number_formats::test_UniqueAndSerializeSmall); + RUN_TEST(ae::test_segmented_number_formats::test_Co2UniqueAndSerialize); + RUN_TEST(ae::test_segmented_number_formats::test_DenseSampling); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-formula-lookup.cpp b/tests/test-segmented-number-formula-lookup.cpp new file mode 100644 index 0000000..57a211a --- /dev/null +++ b/tests/test-segmented-number-formula-lookup.cpp @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_formula_lookup { + +using test_segmented_formats::Battery; +using test_segmented_formats::BatteryLookup; +using test_segmented_formats::Co2; +using test_segmented_formats::Co2Lookup; +using test_segmented_formats::ConnectDuration; +using test_segmented_formats::ConnectLookup; +using test_segmented_formats::Humidity; +using test_segmented_formats::HumidityLookup; +using test_segmented_formats::Rssi; +using test_segmented_formats::RssiLookup; +using test_segmented_formats::RxWindow; +using test_segmented_formats::RxWindowLookup; +using test_segmented_formats::Temperature; +using test_segmented_formats::TemperatureLookup; + +static_assert(Rssi::kLookupTableBytes == 0); +static_assert(RssiLookup::kLookupTableBytes > 0); +static_assert(Temperature::kLookupTableBytes == 0); +static_assert(TemperatureLookup::kLookupTableBytes > 0); + +template +void CompareAllRanks() { + TEST_ASSERT_EQUAL_UINT(Formula::kCodeCount, Lookup::kCodeCount); + TEST_ASSERT_EQUAL_UINT(Formula::kOneByteCount, Lookup::kOneByteCount); + TEST_ASSERT_EQUAL_UINT(Formula::kMaxWireBytes, Lookup::kMaxWireBytes); + for (std::uint32_t rank = 0; + rank < static_cast(Formula::kCodeCount); ++rank) { + auto const wf = test_segmented_formats::WireFromRank(rank); + auto const wl = test_segmented_formats::WireFromRank(rank); + auto const df = Formula::Decode(wf); + auto const dl = Lookup::Decode(wl); + TEST_ASSERT_EQUAL(df.RawValue(), dl.RawValue()); + auto const ef = Formula::TryEncode(df); + auto const el = Lookup::TryEncode(dl); + TEST_ASSERT(ef.has_value()); + TEST_ASSERT(el.has_value()); + TEST_ASSERT_EQUAL_UINT(static_cast(*ef), + static_cast(*el)); + } +} + +void test_RssiFormulaLookup() { CompareAllRanks(); } + +void test_HumidityFormulaLookup() { + CompareAllRanks(); +} + +void test_BatteryFormulaLookup() { CompareAllRanks(); } + +void test_ConnectFormulaLookup() { + CompareAllRanks(); +} + +void test_TemperatureFormulaLookup() { + CompareAllRanks(); +} + +void test_Co2FormulaLookup() { CompareAllRanks(); } + +void test_RxWindowFormulaLookup() { + CompareAllRanks(); +} + +} // namespace ae::test_segmented_number_formula_lookup + +int test_segmented_number_formula_lookup() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_formula_lookup::test_RssiFormulaLookup); + RUN_TEST( + ae::test_segmented_number_formula_lookup::test_HumidityFormulaLookup); + RUN_TEST(ae::test_segmented_number_formula_lookup::test_BatteryFormulaLookup); + RUN_TEST( + ae::test_segmented_number_formula_lookup::test_ConnectFormulaLookup); + RUN_TEST( + ae::test_segmented_number_formula_lookup::test_TemperatureFormulaLookup); + RUN_TEST(ae::test_segmented_number_formula_lookup::test_Co2FormulaLookup); + RUN_TEST( + ae::test_segmented_number_formula_lookup::test_RxWindowFormulaLookup); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-size.cpp b/tests/test-segmented-number-size.cpp new file mode 100644 index 0000000..dde0e5b --- /dev/null +++ b/tests/test-segmented-number-size.cpp @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_size { + +using test_segmented_formats::Battery; +using test_segmented_formats::Co2; +using test_segmented_formats::ConnectDuration; +using test_segmented_formats::Humidity; +using test_segmented_formats::Rssi; +using test_segmented_formats::RssiLookup; +using test_segmented_formats::RxWindow; +using test_segmented_formats::Temperature; + +static_assert(sizeof(Rssi) == sizeof(Rssi::runtime_type)); +static_assert(sizeof(Temperature) == sizeof(Temperature::runtime_type)); +static_assert(sizeof(Humidity) == sizeof(Humidity::runtime_type)); +static_assert(sizeof(Co2) == sizeof(Co2::runtime_type)); +static_assert(sizeof(RxWindow) == sizeof(RxWindow::runtime_type)); +static_assert(sizeof(Battery) == sizeof(Battery::runtime_type)); +static_assert(sizeof(ConnectDuration) == sizeof(ConnectDuration::runtime_type)); +static_assert(MaxWireBytes() == 1); +static_assert(MaxWireBytes() == 2); +static_assert(MaxWireBytes() == 4); +static_assert(Rssi::kLookupTableBytes == 0); +static_assert(RssiLookup::kLookupTableBytes == + Rssi::kCodeCount * (sizeof(std::int64_t) + sizeof(std::uint32_t))); + +void test_FootprintConstants() { + TEST_ASSERT_EQUAL_UINT(sizeof(Rssi::runtime_type), sizeof(Rssi)); + TEST_ASSERT_EQUAL_UINT(1U, sizeof(Rssi::wire_type)); + TEST_ASSERT(Temperature::kFormulaCoefficientBytes > 0); + TEST_ASSERT_EQUAL_UINT(0U, Temperature::kLookupTableBytes); + TEST_ASSERT_EQUAL_UINT(3U, Temperature::kSegmentCount); + TEST_ASSERT_EQUAL_UINT(1U, Rssi::kSegmentCount); +} + +} // namespace ae::test_segmented_number_size + +int test_segmented_number_size() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_size::test_FootprintConstants); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-wire.cpp b/tests/test-segmented-number-wire.cpp new file mode 100644 index 0000000..76c8dcc --- /dev/null +++ b/tests/test-segmented-number-wire.cpp @@ -0,0 +1,113 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_wire { + +using test_segmented_formats::Co2; +using test_segmented_formats::Rssi; +using test_segmented_formats::RxWindow; +using test_segmented_formats::Temperature; + +template +std::size_t SerializedSize(std::uint32_t rank) { + auto const n = Num::FromWire(test_segmented_formats::WireFromRank(rank)); + std::uint8_t buf[8] = {}; + return Num::Serialize(n, buf); +} + +void test_RssiWireTraits() { + TEST_ASSERT_EQUAL_UINT(1U, MaxWireBytes()); + std::uint8_t buf[1] = {}; + auto const n = Rssi::TryFromRuntime(Rssi::runtime_type::FromInteger(-10)); + TEST_ASSERT(n.has_value()); + std::size_t const wrote = wire_traits::Serialize(*n, buf); + TEST_ASSERT_EQUAL_UINT(1U, wrote); + auto const back = wire_traits::Deserialize(buf, wrote); + TEST_ASSERT_EQUAL_UINT(1U, back.bytes_read); + TEST_ASSERT(*n == back.value); +} + +void test_TemperatureTierSizes() { + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(0)); + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(252)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(253)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(1020)); +} + +void test_Co2TierSizes() { + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(0)); + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(254)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(255)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(477)); + TEST_ASSERT_EQUAL_UINT(4U, SerializedSize(478)); + TEST_ASSERT_EQUAL_UINT(4U, SerializedSize(821)); +} + +void test_RxWindowTierSizes() { + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(0)); + TEST_ASSERT_EQUAL_UINT(1U, SerializedSize(254)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(255)); + TEST_ASSERT_EQUAL_UINT(2U, SerializedSize(509)); + TEST_ASSERT_EQUAL_UINT(4U, SerializedSize(510)); + TEST_ASSERT_EQUAL_UINT(4U, SerializedSize(3045)); +} + +void test_TruncatedInput() { + std::uint8_t buf[8] = {}; + std::size_t const tbytes = + Temperature::Serialize(Temperature::FromWire(Temperature::wire_type{253}), + buf); + TEST_ASSERT_EQUAL_UINT(2U, tbytes); + auto const t = Temperature::Deserialize(buf, 1); + TEST_ASSERT_EQUAL_UINT(0U, t.bytes_read); + + std::size_t const cbytes = + Co2::Serialize(Co2::FromWire(Co2::wire_type{478}), buf); + TEST_ASSERT_EQUAL_UINT(4U, cbytes); + auto const c = Co2::Deserialize(buf, 2); + TEST_ASSERT_EQUAL_UINT(0U, c.bytes_read); +} + +void test_UnusedCo2Rank() { + Co2::wire_type const w{2000}; + std::uint8_t buf[8] = {}; + std::size_t const n = wire_traits::Serialize(w, buf); + TEST_ASSERT(n > 0); + auto const r = Co2::Deserialize(buf, n); + TEST_ASSERT_EQUAL_UINT(0U, r.bytes_read); +} + +} // namespace ae::test_segmented_number_wire + +int test_segmented_number_wire() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_wire::test_RssiWireTraits); + RUN_TEST(ae::test_segmented_number_wire::test_TemperatureTierSizes); + RUN_TEST(ae::test_segmented_number_wire::test_Co2TierSizes); + RUN_TEST(ae::test_segmented_number_wire::test_RxWindowTierSizes); + RUN_TEST(ae::test_segmented_number_wire::test_TruncatedInput); + RUN_TEST(ae::test_segmented_number_wire::test_UnusedCo2Rank); + return UNITY_END(); +} From 03e803305014aac6d1d1be12c2b4fc5c0c4c70ae Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Sat, 29 Aug 2026 11:49:48 -0700 Subject: [PATCH 2/4] Add compact cyclic counter Introduce CyclicCounter for truncated modular wire counters with nearest unambiguous full-value restore and TryAdvance. Co-authored-by: Cursor --- README.md | 24 +- ae-numeric/cyclic_counter.h | 239 +++++++++++++++ tests/CMakeLists.txt | 23 ++ tests/footprint/ae_fp_common.h | 34 +++ tests/footprint/minimal_cyclic_u16_u32.cpp | 41 +++ tests/footprint/minimal_cyclic_u8_u16.cpp | 43 +++ tests/footprint/minimal_cyclic_u8_u32.cpp | 43 +++ tests/main.cpp | 2 + tests/test-cyclic-counter.cpp | 327 +++++++++++++++++++++ 9 files changed, 773 insertions(+), 3 deletions(-) create mode 100644 ae-numeric/cyclic_counter.h create mode 100644 tests/footprint/ae_fp_common.h create mode 100644 tests/footprint/minimal_cyclic_u16_u32.cpp create mode 100644 tests/footprint/minimal_cyclic_u8_u16.cpp create mode 100644 tests/footprint/minimal_cyclic_u8_u32.cpp create mode 100644 tests/test-cyclic-counter.cpp diff --git a/README.md b/README.md index 4495fb6..1e94e33 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ They are used across the Æthernet C++ client to represent durations, counters, 7. [Wire IO](#wire-io) 8. [Combined Types](#combined-types) 9. [SegmentedNumber](#segmentednumber) -10. [Integration Notes](#integration-notes) -11. [Running Tests](#running-tests) +10. [CyclicCounter](#cycliccounter) +11. [Integration Notes](#integration-notes) +12. [Running Tests](#running-tests) --- @@ -34,7 +35,8 @@ The core types are: * `TieredInt` — compact integer serialization with compile-time tier boundaries; * `FixedPoint` — binary-scaled fixed point over an integral or packed integral representation; -* `Exponential` — logarithmic code mapping for values that span several orders of magnitude. +* `Exponential` — logarithmic code mapping for values that span several orders of magnitude; +* `CyclicCounter` — full local counter with truncated modular wire bits. --- @@ -372,6 +374,22 @@ Release footprint binaries (section GC, volatile sinks) are the `footprint-*` / --- +## CyclicCounter + +`CyclicCounter` keeps a full unsigned counter locally and sends only its low bits on the wire: + +```text +wire = value mod (max(WireType) + 1) +``` + +Example with `CyclicCounter`: value `1001` → wire `233`. Dropped messages do not matter: if the receiver held `1001` and next sees wire `237`, it restores `1005`. Wire wrap is the same rule: `1023`/`255` then wire `0` restores `1024`. + +Restoration is relative to the current full value and unambiguous only for absolute distances strictly less than half the wire space (`127` for `uint8_t`, `32767` for `uint16_t`). Distance exactly half (`128` / `32768`) is ambiguous — `TryRestore` returns empty; `Restore` is a contract failure. `TryAdvance` updates the local base only when the restored value is strictly newer. + +`sizeof(CyclicCounter) == sizeof(ValueType)`. Wire IO serializes `WireValue()` only; reconstructing a full counter requires a live base (`TryRestore` / `TryAdvance` / `TryDeserializeAndAdvance`). + +--- + ## Integration Notes * Header-only numeric types. diff --git a/ae-numeric/cyclic_counter.h b/ae-numeric/cyclic_counter.h new file mode 100644 index 0000000..fbb4a71 --- /dev/null +++ b/ae-numeric/cyclic_counter.h @@ -0,0 +1,239 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_CYCLIC_COUNTER_H_ +#define AE_NUMERIC_CYCLIC_COUNTER_H_ + +#include +#include +#include +#include +#include +#include + +namespace ae { + +// Compact cyclic counter: full ValueType locally, truncated WireType on the +// wire. Restoration is relative to the current full value (nearest unambiguous +// neighbor). Half wire-range is ambiguous and rejected by TryRestore. +// +// No epoch / previous / decoder state is stored — sizeof equals sizeof(ValueType). +template + requires(std::is_integral_v && std::is_unsigned_v && + !std::is_same_v && std::is_integral_v && + std::is_unsigned_v && !std::is_same_v && + (sizeof(ValueType) > sizeof(WireType))) +class CyclicCounter { + public: + using wire_type = WireType; + using value_type = ValueType; + + static constexpr value_type kWireMask = + static_cast(std::numeric_limits::max()); + // Wire modular space: 2^(8*sizeof(WireType)). Fits in ValueType because + // sizeof(ValueType) > sizeof(WireType). + static constexpr value_type kWireSpace = kWireMask + value_type{1}; + static constexpr value_type kHalfRange = kWireSpace / value_type{2}; + + static_assert(kWireSpace > kWireMask, "ValueType must hold wire space"); + static_assert(kHalfRange * value_type{2} == kWireSpace, + "wire space must be even"); + + constexpr CyclicCounter() noexcept = default; + + constexpr explicit CyclicCounter(value_type value) noexcept : value_(value) {} + + constexpr value_type Value() const noexcept { return value_; } + + constexpr wire_type WireValue() const noexcept { + return static_cast(value_ & kWireMask); + } + + // Nearest full value whose low bits equal `wire`, relative to value_. + // Returns nullopt on half-range ambiguity or ValueType overflow/underflow. + constexpr std::optional TryRestore(wire_type wire) const noexcept { + wire_type const current = WireValue(); + // Modular forward distance in wire space (unsigned wrap). + wire_type const forward = static_cast(wire - current); + + if (forward == wire_type{0}) { + return value_; + } + + value_type const forward_v = static_cast(forward); + if (forward_v == kHalfRange) { + return std::nullopt; + } + + if (forward_v < kHalfRange) { + if (value_ > (std::numeric_limits::max() - forward_v)) { + return std::nullopt; + } + return value_ + forward_v; + } + + value_type const backward = kWireSpace - forward_v; + if (value_ < backward) { + return std::nullopt; + } + return value_ - backward; + } + + // Contract: TryRestore must succeed. Debug builds assert on failure. + constexpr value_type Restore(wire_type wire) const noexcept { + auto const restored = TryRestore(wire); + assert(restored.has_value()); + return *restored; + } + + constexpr void Set(value_type value) noexcept { value_ = value; } + + // Restore relative to value_. On success: if restored > value_, update + // value_; otherwise leave value_ unchanged. Always returns restored when + // unambiguous (including older values). + constexpr std::optional TryAdvance(wire_type wire) noexcept { + auto const restored = TryRestore(wire); + if (!restored.has_value()) { + return std::nullopt; + } + if (*restored > value_) { + value_ = *restored; + } + return restored; + } + + // Read a little-endian WireType from the buffer and TryAdvance. + constexpr std::optional TryDeserializeAndAdvance( + std::uint8_t const* in, std::size_t len) noexcept { + if (in == nullptr || len < sizeof(wire_type)) { + return std::nullopt; + } + wire_type bits = 0; + for (std::size_t i = 0; i < sizeof(wire_type); ++i) { + bits |= static_cast(static_cast(in[i]) + << (8 * i)); + } + return TryAdvance(bits); + } + + constexpr CyclicCounter& operator++() noexcept { + assert(value_ < std::numeric_limits::max()); + ++value_; + return *this; + } + + constexpr CyclicCounter operator++(int) noexcept { + CyclicCounter tmp = *this; + ++(*this); + return tmp; + } + + friend constexpr bool operator==(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return a.value_ == b.value_; + } + friend constexpr bool operator!=(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return !(a == b); + } + friend constexpr bool operator<(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return a.value_ < b.value_; + } + friend constexpr bool operator<=(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return a.value_ <= b.value_; + } + friend constexpr bool operator>(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return b < a; + } + friend constexpr bool operator>=(CyclicCounter const& a, + CyclicCounter const& b) noexcept { + return b <= a; + } + + private: + value_type value_{}; +}; + +enum class WireOrder : std::uint8_t { + Same = 0, + Newer = 1, + Older = 2, + Ambiguous = 3, +}; + +// Compare two wire samples relative to modular half-range (no full counter). +template + requires(std::is_integral_v && std::is_unsigned_v && + !std::is_same_v) +constexpr WireOrder CompareWire(WireType a, WireType b) noexcept { + if (a == b) { + return WireOrder::Same; + } + using Widen = + std::conditional_t>; + static_assert(sizeof(Widen) > sizeof(WireType)); + Widen const space = + static_cast(std::numeric_limits::max()) + Widen{1}; + Widen const half = space / Widen{2}; + WireType const forward = static_cast(b - a); + Widen const f = static_cast(forward); + if (f == half) { + return WireOrder::Ambiguous; + } + if (f < half) { + return WireOrder::Newer; + } + return WireOrder::Older; +} + +} // namespace ae + +#include "ae-numeric/wire_io.h" + +namespace ae { + +// Wire IO carries only the truncated WireType projection. Stateless +// Deserialize builds CyclicCounter(wire) (high bits zero) — not a restored +// full counter. Reconstruct with an existing base via TryRestore / TryAdvance +// or TryDeserializeAndAdvance. +template +struct wire_traits> { + using T = CyclicCounter; + using WireTraits = wire_traits; + + static constexpr std::size_t kMaxWireBytes = WireTraits::kMaxWireBytes; + + static std::size_t Serialize(T const& value, std::uint8_t* out) noexcept { + assert(out != nullptr); + return WireTraits::Serialize(value.WireValue(), out); + } + + static DeserializeResult Deserialize(std::uint8_t const* in, + std::size_t len) noexcept { + auto const wire_result = WireTraits::Deserialize(in, len); + return {T{static_cast(wire_result.value)}, + wire_result.bytes_read}; + } +}; + +} // namespace ae + +#endif // AE_NUMERIC_CYCLIC_COUNTER_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 32cf8e0..cec473e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,6 +47,7 @@ target_sources(${PROJECT_NAME} PRIVATE test-ostream-io.cpp test-wire-io.cpp test-packed-ring.cpp + test-cyclic-counter.cpp test-composed-types.cpp test-composed-exponential.cpp test-composed-exponential-tiered.cpp @@ -182,3 +183,25 @@ add_custom_target(segmented-footprint DEPENDS footprint-temperature-lookup footprint-all-lookup ) + +# Minimal object-only CyclicCounter footprint probes (no CRT link). +function(ae_numeric_add_cyclic_fp_obj tgt src) + add_library(${tgt} OBJECT EXCLUDE_FROM_ALL ${src}) + target_link_libraries(${tgt} PRIVATE ae-numeric) + target_include_directories(${tgt} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/footprint) + target_compile_options(${tgt} PRIVATE + $<$: -Wall -Wextra -Werror -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti> + $<$: -Wall -Wextra -Werror -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti> + $<$:/W4 /WX /Gy /GR- /EHs-c- /wd4530> + ) +endfunction() + +ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u8-u16 footprint/minimal_cyclic_u8_u16.cpp) +ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u8-u32 footprint/minimal_cyclic_u8_u32.cpp) +ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u16-u32 footprint/minimal_cyclic_u16_u32.cpp) + +add_custom_target(cyclic-counter-footprint-obj DEPENDS + fp-obj-cyclic-u8-u16 + fp-obj-cyclic-u8-u32 + fp-obj-cyclic-u16-u32 +) diff --git a/tests/footprint/ae_fp_common.h b/tests/footprint/ae_fp_common.h new file mode 100644 index 0000000..329915d --- /dev/null +++ b/tests/footprint/ae_fp_common.h @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_TESTS_FOOTPRINT_AE_FP_COMMON_H_ +#define AE_NUMERIC_TESTS_FOOTPRINT_AE_FP_COMMON_H_ + +#include + +#if defined(_MSC_VER) +#define AE_FP_NOINLINE __declspec(noinline) +#else +#define AE_FP_NOINLINE __attribute__((noinline)) +#endif + +#if defined(_MSC_VER) +#define AE_FP_USED +#else +#define AE_FP_USED __attribute__((used)) +#endif + +#endif // AE_NUMERIC_TESTS_FOOTPRINT_AE_FP_COMMON_H_ diff --git a/tests/footprint/minimal_cyclic_u16_u32.cpp b/tests/footprint/minimal_cyclic_u16_u32.cpp new file mode 100644 index 0000000..3c0abb8 --- /dev/null +++ b/tests/footprint/minimal_cyclic_u16_u32.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +using Counter = ae::CyclicCounter; + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestRestore( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{b}; + auto const r = c.TryRestore(static_cast(w)); + return r.has_value() ? *r : 0xFFFFFFFFu; +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestAdvance( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{b}; + auto const r = c.TryAdvance(static_cast(w)); + return r.has_value() ? *r : 0xFFFFFFFFu; +} diff --git a/tests/footprint/minimal_cyclic_u8_u16.cpp b/tests/footprint/minimal_cyclic_u8_u16.cpp new file mode 100644 index 0000000..624df3e --- /dev/null +++ b/tests/footprint/minimal_cyclic_u8_u16.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +using Counter = ae::CyclicCounter; + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestRestore( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{static_cast(b)}; + auto const r = c.TryRestore(static_cast(w)); + return r.has_value() ? static_cast(*r) : 0xFFFFFFFFu; +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestAdvance( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{static_cast(b)}; + auto const r = c.TryAdvance(static_cast(w)); + volatile std::uint32_t kept = c.Value(); + return r.has_value() ? (static_cast(*r) ^ (kept << 16)) + : 0xFFFFFFFFu; +} diff --git a/tests/footprint/minimal_cyclic_u8_u32.cpp b/tests/footprint/minimal_cyclic_u8_u32.cpp new file mode 100644 index 0000000..a18571e --- /dev/null +++ b/tests/footprint/minimal_cyclic_u8_u32.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +using Counter = ae::CyclicCounter; + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestRestore( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{b}; + auto const r = c.TryRestore(static_cast(w)); + return r.has_value() ? *r : 0xFFFFFFFFu; +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestAdvance( + std::uint32_t base, std::uint32_t wire) { + volatile std::uint32_t b = base; + volatile std::uint32_t w = wire; + Counter c{b}; + auto const r = c.TryAdvance(static_cast(w)); + volatile std::uint32_t kept = c.Value(); + (void)kept; + return r.has_value() ? *r : 0xFFFFFFFFu; +} diff --git a/tests/main.cpp b/tests/main.cpp index 46d0a3b..6c32ba1 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -30,6 +30,7 @@ extern int test_text_io(); extern int test_ostream_io(); extern int test_wire_io(); extern int test_packed_ring(); +extern int test_cyclic_counter(); extern int test_composed_types(); extern int test_composed_exponential(); extern int test_composed_exponential_tiered(); @@ -57,6 +58,7 @@ int main() { res += test_ostream_io(); res += test_wire_io(); res += test_packed_ring(); + res += test_cyclic_counter(); res += test_composed_types(); res += test_composed_exponential(); res += test_composed_exponential_tiered(); diff --git a/tests/test-cyclic-counter.cpp b/tests/test-cyclic-counter.cpp new file mode 100644 index 0000000..6dd17c1 --- /dev/null +++ b/tests/test-cyclic-counter.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include +#include + +namespace ae::test_cyclic_counter { + +using U8_16 = CyclicCounter; +using U8_32 = CyclicCounter; +using U16_32 = CyclicCounter; + +void test_Size() { + static_assert(sizeof(U8_16) == sizeof(std::uint16_t)); + static_assert(sizeof(U8_32) == sizeof(std::uint32_t)); + static_assert(sizeof(U16_32) == sizeof(std::uint32_t)); + static_assert(alignof(U8_32) == alignof(std::uint32_t)); + TEST_ASSERT_EQUAL(sizeof(std::uint32_t), sizeof(U8_32)); +} + +void test_DefaultAndBasic() { + U8_32 c; + TEST_ASSERT_EQUAL_UINT32(0u, c.Value()); + TEST_ASSERT_EQUAL_UINT8(0u, c.WireValue()); + + c.Set(1000u); + TEST_ASSERT_EQUAL_UINT32(1000u, c.Value()); + TEST_ASSERT_EQUAL_UINT8(232u, c.WireValue()); + + c.Set(1001u); + TEST_ASSERT_EQUAL_UINT8(233u, c.WireValue()); + c.Set(1005u); + TEST_ASSERT_EQUAL_UINT8(237u, c.WireValue()); + c.Set(1023u); + TEST_ASSERT_EQUAL_UINT8(255u, c.WireValue()); + c.Set(1024u); + TEST_ASSERT_EQUAL_UINT8(0u, c.WireValue()); + + U8_32 base{1000u}; + auto const r = base.TryRestore(233u); + TEST_ASSERT_TRUE(r.has_value()); + TEST_ASSERT_EQUAL_UINT32(1001u, *r); + TEST_ASSERT_EQUAL_UINT32(1000u, base.Value()); +} + +void test_LostValues() { + U8_32 base{1001u}; + auto const r = base.TryRestore(237u); // wire for 1005 + TEST_ASSERT_TRUE(r.has_value()); + TEST_ASSERT_EQUAL_UINT32(1005u, *r); + TEST_ASSERT_EQUAL_UINT32(1001u, base.Value()); +} + +void test_ForwardWrap() { + U8_32 base{1023u}; + TEST_ASSERT_EQUAL_UINT8(255u, base.WireValue()); + auto r0 = base.TryRestore(0u); + TEST_ASSERT_TRUE(r0.has_value()); + TEST_ASSERT_EQUAL_UINT32(1024u, *r0); + + base.Set(1024u); + auto r1 = base.TryRestore(1u); + TEST_ASSERT_TRUE(r1.has_value()); + TEST_ASSERT_EQUAL_UINT32(1025u, *r1); +} + +void test_Backward() { + U8_32 base{1008u}; + TEST_ASSERT_EQUAL_UINT8(240u, base.WireValue()); + auto const r = base.TryRestore(235u); + TEST_ASSERT_TRUE(r.has_value()); + TEST_ASSERT_EQUAL_UINT32(1003u, *r); + TEST_ASSERT_TRUE(*r < base.Value()); +} + +void test_Advance() { + U8_32 c{1005u}; + auto a = c.TryAdvance(238u); // 1006 + TEST_ASSERT_TRUE(a.has_value()); + TEST_ASSERT_EQUAL_UINT32(1006u, *a); + TEST_ASSERT_EQUAL_UINT32(1006u, c.Value()); + + auto b = c.TryAdvance(235u); // 1003 + TEST_ASSERT_TRUE(b.has_value()); + TEST_ASSERT_EQUAL_UINT32(1003u, *b); + TEST_ASSERT_EQUAL_UINT32(1006u, c.Value()); +} + +void test_HalfRangeAmbiguousU8() { + U8_32 c{0u}; + TEST_ASSERT_FALSE(c.TryRestore(128u).has_value()); + + c.Set(128u); + TEST_ASSERT_FALSE(c.TryRestore(0u).has_value()); + + c.Set(1000u); // wire 232 + std::uint8_t const amb = + static_cast(c.WireValue() + 128u); + TEST_ASSERT_FALSE(c.TryRestore(amb).has_value()); +} + +void test_MaxLegalDistancesU8() { + U8_32 c{1000u}; + auto fwd = c.TryRestore(static_cast(232u + 127u)); + TEST_ASSERT_TRUE(fwd.has_value()); + TEST_ASSERT_EQUAL_UINT32(1000u + 127u, *fwd); + + auto back = c.TryRestore(static_cast(232u - 127u)); + TEST_ASSERT_TRUE(back.has_value()); + TEST_ASSERT_EQUAL_UINT32(1000u - 127u, *back); +} + +void test_MaxLegalDistancesU16() { + U16_32 c{100000u}; + auto fwd = c.TryRestore(static_cast( + static_cast(c.WireValue()) + 32767u)); + TEST_ASSERT_TRUE(fwd.has_value()); + TEST_ASSERT_EQUAL_UINT32(100000u + 32767u, *fwd); + + auto back = c.TryRestore(static_cast( + static_cast(c.WireValue()) - 32767u)); + TEST_ASSERT_TRUE(back.has_value()); + TEST_ASSERT_EQUAL_UINT32(100000u - 32767u, *back); + + TEST_ASSERT_FALSE(c.TryRestore(static_cast( + c.WireValue() + 32768u)) + .has_value()); +} + +void test_ValueTypeBoundaries() { + U8_16 near0{0u}; + TEST_ASSERT_FALSE(near0.TryRestore(255u).has_value()); + TEST_ASSERT_TRUE(near0.TryRestore(0u).has_value()); + TEST_ASSERT_TRUE(near0.TryRestore(1u).has_value()); + TEST_ASSERT_EQUAL_UINT16(1u, *near0.TryRestore(1u)); + + U8_16 near_max{std::numeric_limits::max()}; + TEST_ASSERT_FALSE(near_max.TryRestore(static_cast( + near_max.WireValue() + 1u)) + .has_value()); + auto back = near_max.TryRestore(static_cast( + near_max.WireValue() - 1u)); + TEST_ASSERT_TRUE(back.has_value()); + TEST_ASSERT_EQUAL_UINT16( + static_cast(std::numeric_limits::max() - 1u), + *back); + + U8_32 u32max{std::numeric_limits::max()}; + TEST_ASSERT_FALSE(u32max.TryRestore(static_cast( + u32max.WireValue() + 1u)) + .has_value()); +} + +void test_MultipleEpochs() { + U8_32 c{65534u}; + TEST_ASSERT_EQUAL_UINT8(254u, c.WireValue()); + auto a = c.TryAdvance(255u); + TEST_ASSERT_EQUAL_UINT32(65535u, *a); + TEST_ASSERT_EQUAL_UINT32(65535u, c.Value()); + auto b = c.TryAdvance(0u); + TEST_ASSERT_EQUAL_UINT32(65536u, *b); + TEST_ASSERT_EQUAL_UINT32(65536u, c.Value()); + auto d = c.TryAdvance(1u); + TEST_ASSERT_EQUAL_UINT32(65537u, *d); + TEST_ASSERT_EQUAL_UINT32(65537u, c.Value()); +} + +void test_OldServer() { + U8_32 c{65540u}; + auto r = c.TryAdvance(static_cast(65535u & 0xFFu)); + TEST_ASSERT_TRUE(r.has_value()); + TEST_ASSERT_EQUAL_UINT32(65535u, *r); + TEST_ASSERT_EQUAL_UINT32(65540u, c.Value()); +} + +void test_ExhaustiveU8() { + std::uint32_t const bases[] = { + 0u, 1u, 127u, 128u, 255u, 256u, 1000u, 1023u, 1024u, + 65534u, 65535u, 65536u, 0xFFFFFEu, 0xFFFFFFu, 0x1000000u, + std::numeric_limits::max() - 200u, + std::numeric_limits::max() - 1u, + std::numeric_limits::max()}; + + for (std::uint32_t base : bases) { + U8_32 c{base}; + std::uint8_t const cur = c.WireValue(); + for (int w = 0; w < 256; ++w) { + auto const wire = static_cast(w); + auto const restored = c.TryRestore(wire); + std::uint8_t const forward = + static_cast(wire - cur); + if (forward == 128u) { + TEST_ASSERT_FALSE(restored.has_value()); + continue; + } + if (forward < 128u) { + if (base > std::numeric_limits::max() - forward) { + TEST_ASSERT_FALSE(restored.has_value()); + } else { + TEST_ASSERT_TRUE(restored.has_value()); + TEST_ASSERT_EQUAL_UINT32(base + forward, *restored); + TEST_ASSERT_EQUAL_UINT8( + wire, static_cast(*restored & 0xFFu)); + } + } else { + std::uint32_t const backward = + 256u - static_cast(forward); + if (base < backward) { + TEST_ASSERT_FALSE(restored.has_value()); + } else { + TEST_ASSERT_TRUE(restored.has_value()); + TEST_ASSERT_EQUAL_UINT32(base - backward, *restored); + TEST_ASSERT_EQUAL_UINT8( + wire, static_cast(*restored & 0xFFu)); + } + } + } + + // Identity when in range: Restore(WireValue(value)) == value for + // neighbors within ±127 of base (same high bits). + for (int d = -127; d <= 127; ++d) { + if (d < 0 && base < static_cast(-d)) { + continue; + } + if (d > 0 && + base > std::numeric_limits::max() - + static_cast(d)) { + continue; + } + std::uint32_t const target = + d >= 0 ? base + static_cast(d) + : base - static_cast(-d); + U8_32 probe{target}; + auto const back = c.TryRestore(probe.WireValue()); + TEST_ASSERT_TRUE(back.has_value()); + TEST_ASSERT_EQUAL_UINT32(target, *back); + } + } +} + +void test_WireTraitsProjection() { + U8_32 c{1001u}; + std::uint8_t buf[4] = {}; + std::size_t const n = wire_traits::Serialize(c, buf); + TEST_ASSERT_EQUAL(1u, n); + TEST_ASSERT_EQUAL_UINT8(233u, buf[0]); + + auto const proj = wire_traits::Deserialize(buf, n); + TEST_ASSERT_EQUAL(1u, proj.bytes_read); + TEST_ASSERT_EQUAL_UINT32(233u, proj.value.Value()); + + U8_32 live{1001u}; + auto adv = live.TryDeserializeAndAdvance(buf, n); + TEST_ASSERT_TRUE(adv.has_value()); + TEST_ASSERT_EQUAL_UINT32(1001u, *adv); + TEST_ASSERT_EQUAL_UINT32(1001u, live.Value()); + + buf[0] = 237u; + adv = live.TryDeserializeAndAdvance(buf, 1); + TEST_ASSERT_EQUAL_UINT32(1005u, *adv); + TEST_ASSERT_EQUAL_UINT32(1005u, live.Value()); +} + +void test_CompareWire() { + TEST_ASSERT_TRUE(CompareWire(std::uint8_t{10}, std::uint8_t{10}) == + WireOrder::Same); + TEST_ASSERT_TRUE(CompareWire(std::uint8_t{10}, std::uint8_t{20}) == + WireOrder::Newer); + TEST_ASSERT_TRUE(CompareWire(std::uint8_t{20}, std::uint8_t{10}) == + WireOrder::Older); + TEST_ASSERT_TRUE(CompareWire(std::uint8_t{0}, std::uint8_t{128}) == + WireOrder::Ambiguous); +} + +void test_IncrementAndCompare() { + U8_32 a{10u}; + U8_32 b = a; + ++a; + TEST_ASSERT_EQUAL_UINT32(11u, a.Value()); + TEST_ASSERT_TRUE(a > b); + TEST_ASSERT_TRUE(b < a); + auto post = a++; + TEST_ASSERT_EQUAL_UINT32(11u, post.Value()); + TEST_ASSERT_EQUAL_UINT32(12u, a.Value()); +} + +} // namespace ae::test_cyclic_counter + +int test_cyclic_counter() { + UNITY_BEGIN(); + RUN_TEST(ae::test_cyclic_counter::test_Size); + RUN_TEST(ae::test_cyclic_counter::test_DefaultAndBasic); + RUN_TEST(ae::test_cyclic_counter::test_LostValues); + RUN_TEST(ae::test_cyclic_counter::test_ForwardWrap); + RUN_TEST(ae::test_cyclic_counter::test_Backward); + RUN_TEST(ae::test_cyclic_counter::test_Advance); + RUN_TEST(ae::test_cyclic_counter::test_HalfRangeAmbiguousU8); + RUN_TEST(ae::test_cyclic_counter::test_MaxLegalDistancesU8); + RUN_TEST(ae::test_cyclic_counter::test_MaxLegalDistancesU16); + RUN_TEST(ae::test_cyclic_counter::test_ValueTypeBoundaries); + RUN_TEST(ae::test_cyclic_counter::test_MultipleEpochs); + RUN_TEST(ae::test_cyclic_counter::test_OldServer); + RUN_TEST(ae::test_cyclic_counter::test_ExhaustiveU8); + RUN_TEST(ae::test_cyclic_counter::test_WireTraitsProjection); + RUN_TEST(ae::test_cyclic_counter::test_CompareWire); + RUN_TEST(ae::test_cyclic_counter::test_IncrementAndCompare); + return UNITY_END(); +} From b70c822f1f25e7359d153b1ddc72a0d301f60218 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Sat, 29 Aug 2026 12:12:33 -0700 Subject: [PATCH 3/4] Require context to deserialize cyclic counters Drop wire_traits for CyclicCounter so truncated wire values cannot be mistaken for a full counter without a live restore base. Co-authored-by: Cursor --- README.md | 16 +- ae-numeric/cyclic_counter.h | 157 ++++++++++++------ tests/CMakeLists.txt | 3 +- .../cyclic_counter_stateless_deserialize.cpp | 33 ++++ tests/test-cyclic-counter.cpp | 112 +++++++++++-- 5 files changed, 250 insertions(+), 71 deletions(-) create mode 100644 tests/compile-fail/cyclic_counter_stateless_deserialize.cpp diff --git a/README.md b/README.md index 1e94e33..9eb5877 100644 --- a/README.md +++ b/README.md @@ -382,11 +382,21 @@ Release footprint binaries (section GC, volatile sinks) are the `footprint-*` / wire = value mod (max(WireType) + 1) ``` -Example with `CyclicCounter`: value `1001` → wire `233`. Dropped messages do not matter: if the receiver held `1001` and next sees wire `237`, it restores `1005`. Wire wrap is the same rule: `1023`/`255` then wire `0` restores `1024`. +Example with `CyclicCounter`: -Restoration is relative to the current full value and unambiguous only for absolute distances strictly less than half the wire space (`127` for `uint8_t`, `32767` for `uint16_t`). Distance exactly half (`128` / `32768`) is ambiguous — `TryRestore` returns empty; `Restore` is a contract failure. `TryAdvance` updates the local base only when the restored value is strictly newer. +```cpp +Counter counter{1001}; +std::uint8_t wire = counter.WireValue(); // 233 +// ... later, after gaps, peer sends wire 237 ... +auto restored = counter.TryRestore(237); // 1005; counter still 1001 +counter.TryAdvance(237); // counter becomes 1005 +``` + +Wire wrap uses the same rule: `1023`/`255` then wire `0` restores `1024`. + +**CyclicCounter cannot be deserialized statelessly** because the truncated wire value does not contain the epoch/high bits. There is no `wire_traits` and `Deserialize(…)` does not compile. Deserialize the `WireType` first (or use `TryDeserializeAndRestore` / `TryDeserializeAndAdvance` on an existing counter) and restore relative to a live full value. -`sizeof(CyclicCounter) == sizeof(ValueType)`. Wire IO serializes `WireValue()` only; reconstructing a full counter requires a live base (`TryRestore` / `TryAdvance` / `TryDeserializeAndAdvance`). +Restoration is unambiguous only for absolute distances strictly less than half the wire space (`127` for `uint8_t`, `32767` for `uint16_t`). Distance exactly half (`128` / `32768`) is ambiguous. `TryAdvance` updates the local base only when the restored value is strictly newer. `sizeof(CyclicCounter) == sizeof(ValueType)`. --- diff --git a/ae-numeric/cyclic_counter.h b/ae-numeric/cyclic_counter.h index fbb4a71..67aaa88 100644 --- a/ae-numeric/cyclic_counter.h +++ b/ae-numeric/cyclic_counter.h @@ -26,10 +26,42 @@ namespace ae { +// Outcome of restoring a truncated wire sample relative to a full counter. +enum class CyclicRestoreStatus : std::uint8_t { + Ok = 0, + Ambiguous = 1, + OutOfRange = 2, +}; + +// Outcome of reading bytes then restoring/advancing relative to a live counter. +enum class CyclicDecodeStatus : std::uint8_t { + Ok = 0, + TruncatedInput = 1, + Ambiguous = 2, + OutOfRange = 3, +}; + +template +struct CyclicDecodeResult { + CyclicDecodeStatus status = CyclicDecodeStatus::TruncatedInput; + ValueType value{}; + std::size_t bytes_read = 0; + + constexpr bool ok() const noexcept { + return status == CyclicDecodeStatus::Ok; + } + constexpr explicit operator bool() const noexcept { return ok(); } +}; + // Compact cyclic counter: full ValueType locally, truncated WireType on the // wire. Restoration is relative to the current full value (nearest unambiguous // neighbor). Half wire-range is ambiguous and rejected by TryRestore. // +// There is no wire_traits specialization: a truncated wire value cannot form a +// full CyclicCounter without a live base. Serialize WireValue() via +// wire_traits; restore with TryRestore / TryAdvance / +// TryDeserializeAndRestore / TryDeserializeAndAdvance. +// // No epoch / previous / decoder state is stored — sizeof equals sizeof(ValueType). template requires(std::is_integral_v && std::is_unsigned_v && @@ -40,6 +72,7 @@ class CyclicCounter { public: using wire_type = WireType; using value_type = ValueType; + using decode_result = CyclicDecodeResult; static constexpr value_type kWireMask = static_cast(std::numeric_limits::max()); @@ -63,33 +96,45 @@ class CyclicCounter { } // Nearest full value whose low bits equal `wire`, relative to value_. - // Returns nullopt on half-range ambiguity or ValueType overflow/underflow. - constexpr std::optional TryRestore(wire_type wire) const noexcept { + // Distinguishes half-range ambiguity from ValueType overflow/underflow. + constexpr CyclicRestoreStatus TryRestoreStatus( + wire_type wire, value_type& out) const noexcept { wire_type const current = WireValue(); - // Modular forward distance in wire space (unsigned wrap). wire_type const forward = static_cast(wire - current); if (forward == wire_type{0}) { - return value_; + out = value_; + return CyclicRestoreStatus::Ok; } value_type const forward_v = static_cast(forward); if (forward_v == kHalfRange) { - return std::nullopt; + return CyclicRestoreStatus::Ambiguous; } if (forward_v < kHalfRange) { if (value_ > (std::numeric_limits::max() - forward_v)) { - return std::nullopt; + return CyclicRestoreStatus::OutOfRange; } - return value_ + forward_v; + out = value_ + forward_v; + return CyclicRestoreStatus::Ok; } value_type const backward = kWireSpace - forward_v; if (value_ < backward) { + return CyclicRestoreStatus::OutOfRange; + } + out = value_ - backward; + return CyclicRestoreStatus::Ok; + } + + // Returns nullopt on Ambiguous or OutOfRange. + constexpr std::optional TryRestore(wire_type wire) const noexcept { + value_type out{}; + if (TryRestoreStatus(wire, out) != CyclicRestoreStatus::Ok) { return std::nullopt; } - return value_ - backward; + return out; } // Contract: TryRestore must succeed. Debug builds assert on failure. @@ -105,28 +150,52 @@ class CyclicCounter { // value_; otherwise leave value_ unchanged. Always returns restored when // unambiguous (including older values). constexpr std::optional TryAdvance(wire_type wire) noexcept { - auto const restored = TryRestore(wire); - if (!restored.has_value()) { + value_type restored{}; + if (TryRestoreStatus(wire, restored) != CyclicRestoreStatus::Ok) { return std::nullopt; } - if (*restored > value_) { - value_ = *restored; + if (restored > value_) { + value_ = restored; } return restored; } - // Read a little-endian WireType from the buffer and TryAdvance. - constexpr std::optional TryDeserializeAndAdvance( - std::uint8_t const* in, std::size_t len) noexcept { - if (in == nullptr || len < sizeof(wire_type)) { - return std::nullopt; + // bytes -> WireType -> TryRestore. Does not modify value_. + constexpr decode_result TryDeserializeAndRestore(std::uint8_t const* in, + std::size_t len) const + noexcept { + decode_result result{}; + wire_type wire{}; + if (!ReadWireLittleEndian(in, len, wire)) { + result.status = CyclicDecodeStatus::TruncatedInput; + result.bytes_read = 0; + return result; } - wire_type bits = 0; - for (std::size_t i = 0; i < sizeof(wire_type); ++i) { - bits |= static_cast(static_cast(in[i]) - << (8 * i)); + result.bytes_read = sizeof(wire_type); + + value_type restored{}; + CyclicRestoreStatus const st = TryRestoreStatus(wire, restored); + if (st == CyclicRestoreStatus::Ambiguous) { + result.status = CyclicDecodeStatus::Ambiguous; + return result; } - return TryAdvance(bits); + if (st == CyclicRestoreStatus::OutOfRange) { + result.status = CyclicDecodeStatus::OutOfRange; + return result; + } + result.status = CyclicDecodeStatus::Ok; + result.value = restored; + return result; + } + + // bytes -> WireType -> TryAdvance. May raise value_ when restored is newer. + constexpr decode_result TryDeserializeAndAdvance(std::uint8_t const* in, + std::size_t len) noexcept { + decode_result result = TryDeserializeAndRestore(in, len); + if (result.ok() && result.value > value_) { + value_ = result.value; + } + return result; } constexpr CyclicCounter& operator++() noexcept { @@ -167,6 +236,20 @@ class CyclicCounter { } private: + static constexpr bool ReadWireLittleEndian(std::uint8_t const* in, + std::size_t len, + wire_type& out) noexcept { + if (in == nullptr || len < sizeof(wire_type)) { + return false; + } + wire_type bits = 0; + for (std::size_t i = 0; i < sizeof(wire_type); ++i) { + bits |= static_cast(static_cast(in[i]) << (8 * i)); + } + out = bits; + return true; + } + value_type value_{}; }; @@ -206,34 +289,4 @@ constexpr WireOrder CompareWire(WireType a, WireType b) noexcept { } // namespace ae -#include "ae-numeric/wire_io.h" - -namespace ae { - -// Wire IO carries only the truncated WireType projection. Stateless -// Deserialize builds CyclicCounter(wire) (high bits zero) — not a restored -// full counter. Reconstruct with an existing base via TryRestore / TryAdvance -// or TryDeserializeAndAdvance. -template -struct wire_traits> { - using T = CyclicCounter; - using WireTraits = wire_traits; - - static constexpr std::size_t kMaxWireBytes = WireTraits::kMaxWireBytes; - - static std::size_t Serialize(T const& value, std::uint8_t* out) noexcept { - assert(out != nullptr); - return WireTraits::Serialize(value.WireValue(), out); - } - - static DeserializeResult Deserialize(std::uint8_t const* in, - std::size_t len) noexcept { - auto const wire_result = WireTraits::Deserialize(in, len); - return {T{static_cast(wire_result.value)}, - wire_result.bytes_read}; - } -}; - -} // namespace ae - #endif // AE_NUMERIC_CYCLIC_COUNTER_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cec473e..b3a0e8d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -132,7 +132,8 @@ foreach(fail_case segmented_runtime_rep_too_small segmented_invalid_wire_bytes segmented_duplicate_runtime_value - segmented_impossible_continuous_step) + segmented_impossible_continuous_step + cyclic_counter_stateless_deserialize) add_executable(fail-${fail_case} EXCLUDE_FROM_ALL compile-fail/${fail_case}.cpp) target_link_libraries(fail-${fail_case} PRIVATE ae-numeric) diff --git a/tests/compile-fail/cyclic_counter_stateless_deserialize.cpp b/tests/compile-fail/cyclic_counter_stateless_deserialize.cpp new file mode 100644 index 0000000..2d3a4d5 --- /dev/null +++ b/tests/compile-fail/cyclic_counter_stateless_deserialize.cpp @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Must fail to compile: CyclicCounter has no stateless wire Deserialize. +// A truncated wire value cannot reconstruct epoch/high bits without a live +// full counter base. + +#include + +#include +#include + +using Counter = ae::CyclicCounter; + +int main() { + std::uint8_t bytes[1] = {237u}; + auto const restored = ae::Deserialize(bytes, sizeof(bytes)); + (void)restored; + return 0; +} diff --git a/tests/test-cyclic-counter.cpp b/tests/test-cyclic-counter.cpp index 6dd17c1..02c3df4 100644 --- a/tests/test-cyclic-counter.cpp +++ b/tests/test-cyclic-counter.cpp @@ -257,27 +257,105 @@ void test_ExhaustiveU8() { } } -void test_WireTraitsProjection() { +void test_WireProjectionAndContextualDecode() { + static_assert(!WireSerializable, + "CyclicCounter must not be WireSerializable"); + static_assert(!WireSerializable); + static_assert(!WireSerializable); + U8_32 c{1001u}; std::uint8_t buf[4] = {}; - std::size_t const n = wire_traits::Serialize(c, buf); + std::size_t const n = + wire_traits::Serialize(c.WireValue(), buf); TEST_ASSERT_EQUAL(1u, n); TEST_ASSERT_EQUAL_UINT8(233u, buf[0]); - auto const proj = wire_traits::Deserialize(buf, n); - TEST_ASSERT_EQUAL(1u, proj.bytes_read); - TEST_ASSERT_EQUAL_UINT32(233u, proj.value.Value()); - - U8_32 live{1001u}; - auto adv = live.TryDeserializeAndAdvance(buf, n); - TEST_ASSERT_TRUE(adv.has_value()); - TEST_ASSERT_EQUAL_UINT32(1001u, *adv); - TEST_ASSERT_EQUAL_UINT32(1001u, live.Value()); + // Same wire again: restore without advancing. + auto const same = c.TryDeserializeAndRestore(buf, n); + TEST_ASSERT_TRUE(same.ok()); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::Ok), + static_cast(same.status)); + TEST_ASSERT_EQUAL_UINT32(1001u, same.value); + TEST_ASSERT_EQUAL(1u, same.bytes_read); + TEST_ASSERT_EQUAL_UINT32(1001u, c.Value()); buf[0] = 237u; - adv = live.TryDeserializeAndAdvance(buf, 1); - TEST_ASSERT_EQUAL_UINT32(1005u, *adv); - TEST_ASSERT_EQUAL_UINT32(1005u, live.Value()); + auto const restored = c.TryDeserializeAndRestore(buf, 1); + TEST_ASSERT_TRUE(restored.ok()); + TEST_ASSERT_EQUAL_UINT32(1005u, restored.value); + TEST_ASSERT_EQUAL_UINT32(1001u, c.Value()); + + auto const advanced = c.TryDeserializeAndAdvance(buf, 1); + TEST_ASSERT_TRUE(advanced.ok()); + TEST_ASSERT_EQUAL_UINT32(1005u, advanced.value); + TEST_ASSERT_EQUAL_UINT32(1005u, c.Value()); +} + +void test_ContextualRestoreAdvanceOldWrapAmbiguous() { + U8_32 counter{1001u}; + std::uint8_t wire237 = 237u; + auto r = counter.TryDeserializeAndRestore(&wire237, 1); + TEST_ASSERT_TRUE(r.ok()); + TEST_ASSERT_EQUAL_UINT32(1005u, r.value); + TEST_ASSERT_EQUAL_UINT32(1001u, counter.Value()); + + counter.Set(1001u); + auto a = counter.TryDeserializeAndAdvance(&wire237, 1); + TEST_ASSERT_EQUAL_UINT32(1005u, a.value); + TEST_ASSERT_EQUAL_UINT32(1005u, counter.Value()); + + counter.Set(1008u); + std::uint8_t old_wire = 235u; // 1003 + auto old = counter.TryDeserializeAndRestore(&old_wire, 1); + TEST_ASSERT_EQUAL_UINT32(1003u, old.value); + TEST_ASSERT_EQUAL_UINT32(1008u, counter.Value()); + auto old_adv = counter.TryDeserializeAndAdvance(&old_wire, 1); + TEST_ASSERT_EQUAL_UINT32(1003u, old_adv.value); + TEST_ASSERT_EQUAL_UINT32(1008u, counter.Value()); + + counter.Set(1023u); + std::uint8_t wrap = 0u; + auto w = counter.TryDeserializeAndRestore(&wrap, 1); + TEST_ASSERT_EQUAL_UINT32(1024u, w.value); + + counter.Set(0u); + std::uint8_t amb = 128u; + auto bad = counter.TryDeserializeAndRestore(&amb, 1); + TEST_ASSERT_FALSE(bad.ok()); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::Ambiguous), + static_cast(bad.status)); + TEST_ASSERT_EQUAL(1u, bad.bytes_read); + TEST_ASSERT_EQUAL_UINT32(0u, counter.Value()); +} + +void test_TruncatedInputU16() { + U16_32 c{1000u}; + std::uint8_t one_byte[1] = {0x01}; + auto r = c.TryDeserializeAndRestore(one_byte, 1); + TEST_ASSERT_FALSE(r.ok()); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::TruncatedInput), + static_cast(r.status)); + TEST_ASSERT_EQUAL(0u, r.bytes_read); + + auto a = c.TryDeserializeAndAdvance(one_byte, 1); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::TruncatedInput), + static_cast(a.status)); + TEST_ASSERT_EQUAL_UINT32(1000u, c.Value()); +} + +void test_DecodeOutOfRangeNearBounds() { + U8_16 near0{0u}; + std::uint8_t back = 255u; + auto u = near0.TryDeserializeAndRestore(&back, 1); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::OutOfRange), + static_cast(u.status)); + TEST_ASSERT_EQUAL(1u, u.bytes_read); + + U8_16 near_max{std::numeric_limits::max()}; + std::uint8_t fwd = static_cast(near_max.WireValue() + 1u); + auto o = near_max.TryDeserializeAndRestore(&fwd, 1); + TEST_ASSERT_EQUAL_INT(static_cast(CyclicDecodeStatus::OutOfRange), + static_cast(o.status)); } void test_CompareWire() { @@ -320,7 +398,11 @@ int test_cyclic_counter() { RUN_TEST(ae::test_cyclic_counter::test_MultipleEpochs); RUN_TEST(ae::test_cyclic_counter::test_OldServer); RUN_TEST(ae::test_cyclic_counter::test_ExhaustiveU8); - RUN_TEST(ae::test_cyclic_counter::test_WireTraitsProjection); + RUN_TEST(ae::test_cyclic_counter::test_WireProjectionAndContextualDecode); + RUN_TEST( + ae::test_cyclic_counter::test_ContextualRestoreAdvanceOldWrapAmbiguous); + RUN_TEST(ae::test_cyclic_counter::test_TruncatedInputU16); + RUN_TEST(ae::test_cyclic_counter::test_DecodeOutOfRangeNearBounds); RUN_TEST(ae::test_cyclic_counter::test_CompareWire); RUN_TEST(ae::test_cyclic_counter::test_IncrementAndCompare); return UNITY_END(); From 3ced7e4f94be1e8dbb15fc0ba04a6e7b6eb1b849 Mon Sep 17 00:00:00 2001 From: aethernet-io Date: Sat, 29 Aug 2026 15:11:26 -0700 Subject: [PATCH 4/4] Document numeric footprints and cyclic counter Co-authored-by: Cursor --- .github/workflows/ci-cd-tests.yml | 33 +- README.md | 110 +- ae-numeric/details/segmented_compiler.h | 631 ++++++++---- ae-numeric/details/segmented_curves.h | 147 +-- ae-numeric/details/segmented_format.h | 1 - .../details/segmented_formula_backend.h | 936 ++++++++++++++---- ae-numeric/details/segmented_lookup_backend.h | 104 -- ae-numeric/details/segmented_math.h | 833 +++++++++++++--- ae-numeric/fixed_math.h | 455 ++++++--- ae-numeric/fixed_point.h | 538 ++++++++-- ae-numeric/integer_math.h | 263 +++++ ae-numeric/segmented_number.h | 188 ++-- .../segmented_number_floating_runtime.h | 38 +- docs/benchmark_results.txt | 15 + docs/benchmarks.md | 73 ++ docs/footprint.md | 217 ++++ docs/footprint_results.json | 398 ++++++++ tests/CMakeLists.txt | 183 +++- tests/benchmark/numeric_bench.cpp | 177 ++++ tests/footprint/component_formats.h | 88 ++ tests/footprint/dump_layout.cpp | 79 ++ tests/footprint/dump_meta.cpp | 72 ++ tests/footprint/minimal_all.cpp | 63 ++ tests/footprint/minimal_battery.cpp | 20 + tests/footprint/minimal_body.inc | 70 ++ tests/footprint/minimal_co2.cpp | 20 + tests/footprint/minimal_combined.cpp | 20 + tests/footprint/minimal_connect.cpp | 20 + tests/footprint/minimal_empty.cpp | 41 + tests/footprint/minimal_exponential.cpp | 20 + tests/footprint/minimal_geometric.cpp | 20 + tests/footprint/minimal_humidity.cpp | 20 + tests/footprint/minimal_ramp.cpp | 20 + tests/footprint/minimal_rssi.cpp | 20 + tests/footprint/minimal_rx.cpp | 20 + tests/footprint/minimal_temperature.cpp | 20 + tests/footprint/minimal_thermometer.cpp | 61 ++ tests/footprint/minimal_tiered.cpp | 20 + tests/footprint/minimal_uniform.cpp | 20 + tests/footprint/segmented_footprint.cpp | 18 +- tests/main.cpp | 4 +- tests/segmented_reference_math.h | 143 +++ tests/segmented_test_formats.h | 44 +- tests/test-fixed-math.cpp | 63 +- tests/test-integer-math.cpp | 72 ++ tests/test-segmented-number-core.cpp | 13 +- tests/test-segmented-number-formats.cpp | 182 +++- .../test-segmented-number-formula-lookup.cpp | 104 -- tests/test-segmented-number-schema.cpp | 125 +++ tests/test-segmented-number-size.cpp | 5 - tools/generate_footprint_docs.py | 384 +++++++ tools/measure_esp32c6_footprint.py | 223 +++++ 52 files changed, 6120 insertions(+), 1334 deletions(-) delete mode 100644 ae-numeric/details/segmented_lookup_backend.h create mode 100644 docs/benchmark_results.txt create mode 100644 docs/benchmarks.md create mode 100644 docs/footprint.md create mode 100644 docs/footprint_results.json create mode 100644 tests/benchmark/numeric_bench.cpp create mode 100644 tests/footprint/component_formats.h create mode 100644 tests/footprint/dump_layout.cpp create mode 100644 tests/footprint/dump_meta.cpp create mode 100644 tests/footprint/minimal_all.cpp create mode 100644 tests/footprint/minimal_battery.cpp create mode 100644 tests/footprint/minimal_body.inc create mode 100644 tests/footprint/minimal_co2.cpp create mode 100644 tests/footprint/minimal_combined.cpp create mode 100644 tests/footprint/minimal_connect.cpp create mode 100644 tests/footprint/minimal_empty.cpp create mode 100644 tests/footprint/minimal_exponential.cpp create mode 100644 tests/footprint/minimal_geometric.cpp create mode 100644 tests/footprint/minimal_humidity.cpp create mode 100644 tests/footprint/minimal_ramp.cpp create mode 100644 tests/footprint/minimal_rssi.cpp create mode 100644 tests/footprint/minimal_rx.cpp create mode 100644 tests/footprint/minimal_temperature.cpp create mode 100644 tests/footprint/minimal_thermometer.cpp create mode 100644 tests/footprint/minimal_tiered.cpp create mode 100644 tests/footprint/minimal_uniform.cpp create mode 100644 tests/segmented_reference_math.h delete mode 100644 tests/test-segmented-number-formula-lookup.cpp create mode 100644 tests/test-segmented-number-schema.cpp create mode 100644 tools/generate_footprint_docs.py create mode 100644 tools/measure_esp32c6_footprint.py diff --git a/.github/workflows/ci-cd-tests.yml b/.github/workflows/ci-cd-tests.yml index af37f54..42d079e 100644 --- a/.github/workflows/ci-cd-tests.yml +++ b/.github/workflows/ci-cd-tests.yml @@ -23,21 +23,34 @@ jobs: matrix: config: + # windows-latest (Server 2025) ships Visual Studio 2026 and the + # "Visual Studio 18 2026" CMake generator as of June 2026. - { name: "Windows MSVC", os: windows-latest, shell: "powershell", generator: "Visual Studio 18 2026", - arch: "x64", + cmake_args: "-A x64", cc: "cl", cxx: "cl", build_type: Release, } + - { + name: "Windows LLVM Clang", + os: windows-latest, + shell: "powershell", + generator: "Visual Studio 18 2026", + cmake_args: "-A x64 -T ClangCL", + cc: "clang-cl", + cxx: "clang-cl", + build_type: Release, + } - { name: "Windows MinGW", os: windows-latest, shell: "msys2", generator: "Ninja", + cmake_args: "", cc: "gcc", cxx: "g++", build_type: Release, @@ -47,23 +60,35 @@ jobs: os: ubuntu-latest, shell: "bash", generator: "Unix Makefiles", + cmake_args: "", cc: "gcc", cxx: "g++", build_type: Release, } + - { + name: "Ubuntu LLVM Clang", + os: ubuntu-latest, + shell: "bash", + generator: "Unix Makefiles", + cmake_args: "", + cc: "clang", + cxx: "clang++", + build_type: Release, + } - { name: "macOS Apple-Clang", os: macos-latest, shell: "bash", generator: "Unix Makefiles", + cmake_args: "", cc: "clang", cxx: "clang++", build_type: Release, } steps: - - name: Set Windows environment - if: ${{ (matrix.config.os == 'windows-latest' && matrix.config.cc == 'cl') }} + - name: Set Windows MSVC environment + if: ${{ (matrix.config.os == 'windows-latest' && matrix.config.shell == 'powershell') }} uses: ilammy/msvc-dev-cmd@v1 - name: Set MinGW environment @@ -88,7 +113,7 @@ jobs: run: > cmake -B build-numerics -G "${{ matrix.config.generator }}" - -A "${{ matrix.config.arch }}" + ${{ matrix.config.cmake_args }} -DCMAKE_CXX_COMPILER=${{ matrix.config.cxx }} -DCMAKE_C_COMPILER=${{ matrix.config.cc }} -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} diff --git a/README.md b/README.md index 9eb5877..2745c51 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ They are used across the Æthernet C++ client to represent durations, counters, 8. [Combined Types](#combined-types) 9. [SegmentedNumber](#segmentednumber) 10. [CyclicCounter](#cycliccounter) -11. [Integration Notes](#integration-notes) -12. [Running Tests](#running-tests) +11. [Footprint and benchmarks](#footprint-and-benchmarks) +12. [Integration Notes](#integration-notes) +13. [Running Tests](#running-tests) --- @@ -302,14 +303,14 @@ This is a linear scale: every raw step is one millisecond. Use `Exponential` ins ## SegmentedNumber -`SegmentedNumber` is a header-only piecewise quantized number. The object stores only the physical runtime value. A dense packed rank is computed when encoding and is serialized through a compiled `uint8_t` or `TieredInt` wire type. +`SegmentedNumber` is a header-only piecewise quantized physical value. The object stores only the runtime `FixedPoint` (or opt-in floating runtime). Wire rank is computed at encode time and serialized through a compiled `uint8_t` or `TieredInt` wire type. -The description splits four layers: +Architecture: -1. runtime representation (`runtime::Fixed` or opt-in `runtime::Floating`); -2. mathematical curves of representable values; -3. assignment of those codes to wire lengths 1/2/4/8 bytes; -4. the serializable packed rank. +* Mathematical mapping (which physical values exist) and wire-tier placement (1/2/4/8-byte assignment) are independent layers. +* Fractional math on the production path uses only our `FixedPoint` with underlying Rep no wider than 32 bits. +* There are no per-code lookup tables, no runtime heap, no homemade Q30/Q31 storage, and no 64-bit arithmetic helpers on the mathematical ESP32-C6 path. +* The number of materialized constants does not grow with the number of codes; shared `Log2`/`Exp2` FixedPoint tables are common across formats. Bounds and steps are written with `ae::Decimal` / `ae::Ratio`, not `double`. @@ -347,56 +348,99 @@ static_assert(Temperature::kCodeCount == 1021); static_assert(Temperature::kMaxWireBytes == 2); ``` -`Compile` is `SegmentedNumber`. The object does not store the wire rank. Encode with `TryEncode` / `TryFromRuntime`; out-of-range input is rejected unless `Saturating` is used. Comparisons use the runtime value, never the packed rank: rank order need not follow physical order (the temperature window uses 1-byte codes in the middle and 2-byte codes on both tails). +`Compile` is `SegmentedNumber`. Encode with `TryEncode` / `TryFromRuntime`; out-of-range input is rejected unless `Saturating` is used. Comparisons use the runtime value, never the packed rank (temperature packs 1-byte codes in the center and 2-byte codes on both tails). -Curve primitives: `UniformStep`, `UniformValues`, `ExponentialValues`, `GeometricStep`, `LinearStepRamp`. Allocation helpers: `Intervals`, `FillTier`, `MinimumIntervals`, `AutoSplit`, and `ContinuousExponential` with `WireCuts` / `OptimizeCuts`. +### Curves -Two compute backends: +**UniformStep** — constant physical step between adjacent codes over a closed range. Encode/decode are O(1) in the number of codes (affine map with FixedPoint multiply/divide). Uses FixedPoint scale conversion only; no `Log2`/`Exp2`. -* `compute::Formula` (default) — small per-segment coefficients, no per-code table, no runtime `float`/`double` for `Fixed` runtime. Segment selection is O(S). Uniform and linear-ramp paths are O(1) (integer square root for the ramp). Exponential and geometric decode use integer exponentiation-by-squaring of a compiled ratio (O(log n) multiplies). Encode of those curves binary-searches the selected segment and checks neighboring codes. Serialization is O(1), at most 8 bytes, heap = 0; -* `compute::Lookup` — consteval decoded-raw table, O(1) decode and O(log N) encode, used as a Formula oracle. Flash/data is O(N). +**UniformValues** — packs a fixed count of evenly spaced values into a range. Same O(1) affine FixedPoint path as UniformStep; interval count is prescribed rather than implied by step size. -Shared physical endpoints are encoded once. The segment with the smaller wire size owns the joint; if the sizes match, the previous physical segment owns it. Unused packed ranks deserialize with `bytes_read == 0`. +**LinearStepRamp** — step size grows linearly along the segment (coarse then fine, or the reverse). Encode/decode solve a quadratic in FixedPoint (`Sqrt` / wide multiply-divide helpers bounded to 32-bit limbs). Complexity is O(1) in code count aside from a fixed Newton/`Sqrt` budget. + +**ExponentialValues** — values follow a geometric progression in physical space (constant ratio). Encode/decode use `Log2`/`Exp2` FixedPoint primitives; O(1) relative to code count (fixed iteration count for log/exp). + +**GeometricStep** — adjacent steps form a geometric series (useful for tails that must meet a prescribed endpoint step). Encode/decode use `Log2`/`Exp2` plus geometric weight helpers; still O(1) in code count. + +**ContinuousExponential** — single exponential-style continuum used inside cut optimizers / `WireCuts`. Same FixedPoint `Log2`/`Exp2` core; used when placing cuts rather than as a standalone sensor format by itself. + +**AutoSplit** — compile-time splitter that partitions a continuum into exponential-style pieces under an objective (for example continuous absolute step plus minimax relative error). Runs only at compile time; runtime path is the resulting ExponentialValues / GeometricStep / LinearStepRamp segments. + +Shared physical endpoints are encoded once (smaller wire size owns the joint). Unused packed ranks deserialize with `bytes_read == 0`. + +`sizeof(CompiledSegment)` is a compile-time C++ type size (~68 B on ILP32). It is **not** flash per segment: descriptors do not materialize as `68 × segments` in `.rodata`. See [docs/footprint.md](docs/footprint.md). Floating runtime is opt-in and does not change the wire ABI: ```cpp #include - -using FloatSpec = ae::seg::Format< - ae::seg::runtime::Floating, - ae::seg::wire::AutoTiered>, - ae::seg::compute::Formula, - typename Spec::layout_type>; ``` -Release footprint binaries (section GC, volatile sinks) are the `footprint-*` / `segmented-footprint` targets in `tests/`. - --- ## CyclicCounter -`CyclicCounter` keeps a full unsigned counter locally and sends only its low bits on the wire: +`CyclicCounter` keeps the full counter as `ValueType` at runtime and puts only the low bits of that counter on the wire as `WireType`. The default value is zero. The object contains only `ValueType value_`, so `sizeof(CyclicCounter) == sizeof(Value)`. + +Restoration is always relative to the current full value: missing messages do not break recovery as long as the absolute distance stays within the unambiguous half-range. An older truncated sample from another peer can still be restored and compared without changing the local base. `TryAdvance` never decreases that base. The distance must be strictly less than half the wire range; exactly half the range is ambiguous. + +```text +current = 1001 +received wire = 237 +restored = 1005 +``` + +```text +current = 1023 +received wire = 0 +restored = 1024 +``` ```text -wire = value mod (max(WireType) + 1) +current = 1008 +received wire corresponding to 1003 +restored = 1003 +current remains 1008 ``` -Example with `CyclicCounter`: +**Stateless deserialization of a full `CyclicCounter` is forbidden.** Without a current full value, epoch / high bits cannot be recovered. There is no `wire_traits`, and `Deserialize(…)` does not compile. Read a `WireType` first, then call `TryRestore`, `TryAdvance`, `TryDeserializeAndRestore`, or `TryDeserializeAndAdvance` on an existing counter. ```cpp -Counter counter{1001}; -std::uint8_t wire = counter.WireValue(); // 233 -// ... later, after gaps, peer sends wire 237 ... -auto restored = counter.TryRestore(237); // 1005; counter still 1001 -counter.TryAdvance(237); // counter becomes 1005 +using Counter = + ae::CyclicCounter; + +Counter counter; // value = 0 + +counter.Set(1001); + +std::uint8_t wire = counter.WireValue(); + +auto restored = counter.TryRestore(wire); +auto advanced = counter.TryAdvance(wire); ``` -Wire wrap uses the same rule: `1023`/`255` then wire `0` restores `1024`. +Supported configurations (ESP32-C6 `-Os`, object-only; see [docs/footprint.md](docs/footprint.md) for `-O2` and full section breakdown): -**CyclicCounter cannot be deserialized statelessly** because the truncated wire value does not contain the epoch/high bits. There is no `wire_traits` and `Deserialize(…)` does not compile. Deserialize the `WireType` first (or use `TryDeserializeAndRestore` / `TryDeserializeAndAdvance` on an existing counter) and restore relative to a live full value. +| Wire → Value | Runtime B | Wire B | Max unambiguous distance | `.text` | `.rodata` | Heap | +|---|---:|---:|---:|---:|---:|---| +| `uint8_t` → `uint16_t` | 2 | 1 | 127 | 224 | 0 | 0 | +| `uint8_t` → `uint32_t` | 4 | 1 | 127 | 182 | 0 | 0 | +| `uint16_t` → `uint32_t` | 4 | 2 | 32767 | 136 | 0 | 0 | -Restoration is unambiguous only for absolute distances strictly less than half the wire space (`127` for `uint8_t`, `32767` for `uint16_t`). Distance exactly half (`128` / `32768`) is ambiguous. `TryAdvance` updates the local base only when the restored value is strictly newer. `sizeof(CyclicCounter) == sizeof(ValueType)`. +--- + +## Footprint and benchmarks + +* [docs/footprint.md](docs/footprint.md) — ESP32-C6 `.text` / `.rodata` / RAM, constant tables, object sizes, stack usage, code sharing. +* [docs/benchmarks.md](docs/benchmarks.md) — desktop encode/decode/serialize timings (nanoseconds, not MCU cycles). + +Refresh generated tables: + +```bash +cmake --build build-dev --target segmented-footprint-obj +python tools/measure_esp32c6_footprint.py --repo . +python tools/generate_footprint_docs.py --repo . +``` --- diff --git a/ae-numeric/details/segmented_compiler.h b/ae-numeric/details/segmented_compiler.h index 20ca55a..fce79d7 100644 --- a/ae-numeric/details/segmented_compiler.h +++ b/ae-numeric/details/segmented_compiler.h @@ -23,13 +23,12 @@ #include #include -#include - #include "ae-numeric/details/segmented_curves.h" #include "ae-numeric/details/segmented_format.h" #include "ae-numeric/details/segmented_math.h" #include "ae-numeric/fixed_math.h" #include "ae-numeric/fixed_point.h" +#include "ae-numeric/integer_math.h" #include "ae-numeric/tiered_int.h" namespace ae::seg::segmented_compiler_internal { @@ -39,17 +38,43 @@ using segmented_curves_internal::FlattenLayout; using segmented_curves_internal::kMaxDrafts; using segmented_curves_internal::StepMode; using segmented_math_internal::AutoSplitTwoExp; -using segmented_math_internal::ExpRatio; -using segmented_math_internal::GeomSum; +using segmented_math_internal::CeilAbsRat; +using segmented_math_internal::ConvertFixed; +using segmented_math_internal::GeomInterp; +using segmented_math_internal::RatSub; +using segmented_math_internal::RoundRatQuotient; +using segmented_math_internal::WorkFromRaw; +using segmented_math_internal::Exp2Ratio; +using segmented_math_internal::Exp2Work; +using segmented_math_internal::Log2OfRat; +using segmented_math_internal::Log2RFromEndpoints; +using segmented_math_internal::Log2Ratio; +using segmented_math_internal::LogZero; using segmented_math_internal::MinRampIntervals; +using segmented_math_internal::MulWorkSame; +using segmented_math_internal::ScaleWorkByInt; +using segmented_math_internal::ScaleWorkByRatio; using segmented_math_internal::MixHash; +using segmented_math_internal::MixRat; +using segmented_math_internal::MulLogInt; using segmented_math_internal::OptimizeContinuousExp; +using segmented_math_internal::Rat; +using segmented_math_internal::RatAbsMax; +using segmented_math_internal::RatLess; +using segmented_math_internal::RatioOne; using segmented_math_internal::SegmentedSpecError; +using segmented_math_internal::SegLog; +using segmented_math_internal::SegPosWork; +using segmented_math_internal::SegRatio; +using segmented_math_internal::SegWork; using segmented_math_internal::SolveQForGeomSum; -using segmented_math_internal::ThreeTierMaxU8; using segmented_math_internal::TwoTierMaxU8; - -inline constexpr double kEps = 1.0e-12; +using segmented_math_internal::WorkAbs; +using segmented_math_internal::WorkFromRat; +using segmented_math_internal::WorkOne; +using segmented_math_internal::WorkPositive; +using segmented_math_internal::WorkToNearestInt; +using segmented_math_internal::WorkZero; template inline constexpr int kMaxBytesTag = -1; @@ -75,35 +100,34 @@ consteval int FormatMaxBytes() { return PolicyMaxBytes(static_cast(nullptr)); } -consteval bool NearlyEqual(double a, double b) { - double const s = gcem::abs(a) > gcem::abs(b) ? gcem::abs(a) : gcem::abs(b); - double const tol = kEps * (s > 1.0 ? s : 1.0); - return gcem::abs(a - b) <= tol; -} - -consteval int RoundN(double x) { - if (x < 0.0) { - return static_cast(x - 0.5); - } - return static_cast(x + 0.5); -} - consteval bool GeomFromUpper(CurveDraft const& d) { return d.step_mode == StepMode::kUpperExplicit || d.step_mode == StepMode::kUpperInherit; } -consteval double GeomUpperStep(CurveDraft const& d) { +consteval SegWork GeomUpperStep(CurveDraft const& d) { if (d.step_mode == StepMode::kUpperInherit) { return d.last_step; } - if (d.specified_step != 0.0) { + if (WorkPositive(d.specified_step)) { return d.specified_step; } return d.last_step; } -consteval double DecodeMath(CurveDraft const& d, int i) { +consteval SegWork LerpWork(SegWork a, SegWork b, int i, int n) { + if (n <= 0 || i <= 0) { + return a; + } + if (i >= n) { + return b; + } + SegWork const span = SubTo(b, a); + SegWork const t = ConvertFixed(SegPosWork::FromRatio(i, n)); + return AddTo(a, MulWorkSame(span, t)); +} + +consteval SegWork DecodeMath(CurveDraft const& d, int i) { if (d.intervals <= 0) { return d.begin; } @@ -115,30 +139,40 @@ consteval double DecodeMath(CurveDraft const& d, int i) { } if (d.kind == CurveKind::kUniformStep || d.kind == CurveKind::kUniformValues) { - return d.begin + (d.end - d.begin) * - static_cast(i) / - static_cast(d.intervals); + return LerpWork(d.begin, d.end, i, d.intervals); } if (d.kind == CurveKind::kExponentialValues) { - return d.begin * gcem::pow(d.r, static_cast(i)); + SegLog const arg = + AddTo(Log2OfRat(d.begin_rat), MulLogInt(d.log2_r, i)); + return Exp2Work(arg); } if (d.kind == CurveKind::kGeometricStep) { - if (GeomFromUpper(d)) { - double const se = GeomUpperStep(d); - return d.end - se * GeomSum(d.q, d.intervals - i); - } - return d.begin + d.step0 * GeomSum(d.q, i); + return GeomInterp(d.begin, d.end, d.log2_q, i, d.intervals, + GeomFromUpper(d)); + } + std::uint32_t const n_u = static_cast(i); + std::uint32_t const n_all = static_cast(d.intervals); + std::uint32_t const pair_i = n_u * (n_u - 1U) / 2U; + std::uint32_t const pair_n = n_all * (n_all - 1U) / 2U; + SegWork const num = AddTo(ScaleWorkByInt(d.step0, i), + ScaleWorkByInt(d.delta, static_cast(pair_i))); + SegWork const den = + AddTo(ScaleWorkByInt(d.step0, d.intervals), + ScaleWorkByInt(d.delta, static_cast(pair_n))); + if (den.RawValue() == static_cast(0)) { + return d.begin; } - return d.begin + static_cast(i) * d.step0 + - d.delta * static_cast(i) * static_cast(i - 1) / 2.0; + SegWork const span = SubTo(d.end, d.begin); + return AddTo(d.begin, MulWorkSame(span, DivTo(num, den))); } -consteval double FirstAbsStep(CurveDraft const& d) { - return gcem::abs(DecodeMath(d, 1) - DecodeMath(d, 0)); +consteval SegWork FirstAbsStep(CurveDraft const& d) { + return WorkAbs(SubTo(DecodeMath(d, 1), DecodeMath(d, 0))); } -consteval double LastAbsStep(CurveDraft const& d) { - return gcem::abs(DecodeMath(d, d.intervals) - DecodeMath(d, d.intervals - 1)); +consteval SegWork LastAbsStep(CurveDraft const& d) { + return WorkAbs( + SubTo(DecodeMath(d, d.intervals), DecodeMath(d, d.intervals - 1))); } consteval void AssignOwnership(CurveDraft* d, int n) { @@ -149,10 +183,10 @@ consteval void AssignOwnership(CurveDraft* d, int n) { d[0].own_begin = true; d[n - 1].own_end = true; for (int i = 0; i < n - 1; ++i) { - if (!NearlyEqual(d[i].end, d[i + 1].begin)) { - if (d[i + 1].begin < d[i].end) { - SegmentedSpecError(); - } else { + if (d[i].end_rat.num != d[i + 1].begin_rat.num || + d[i].end_rat.den != d[i + 1].begin_rat.den) { + if (RatLess(d[i + 1].begin_rat, d[i].end_rat) || + RatLess(d[i].end_rat, d[i + 1].begin_rat)) { SegmentedSpecError(); } } @@ -178,56 +212,78 @@ consteval int StoredOf(CurveDraft const& d) { return d.intervals + (d.own_begin ? 1 : 0) + (d.own_end ? 1 : 0) - 1; } -consteval bool HasBytes(CurveDraft const* d, int n, int bytes) { - for (int i = 0; i < n; ++i) { - if (d[i].bytes == bytes || (d[i].is_cont_exp && bytes >= 1)) { - if (d[i].is_cont_exp) { - return bytes == 1 || bytes == 2 || bytes == 4; - } - if (d[i].bytes == bytes) { - return true; - } - } +consteval SegRatio SolveQFromStepRat(int intervals, SegWork span, SegWork step0, + Rat span_r, Rat step_r) { + if (step_r.num == 0 || step_r.den == 0) { + return SolveQForGeomSum(intervals, DivTo(span, step0)); } - return false; + std::int32_t const sn = span_r.num < 0 ? -span_r.num : span_r.num; + std::int32_t const sd = span_r.den < 0 ? -span_r.den : span_r.den; + std::int32_t const tn = step_r.num < 0 ? -step_r.num : step_r.num; + std::int32_t const td = step_r.den < 0 ? -step_r.den : step_r.den; + return SolveQForGeomSum(intervals, WorkFromRat({sn * td, sd * tn})); } consteval void ComputeCoeffsKnownN(CurveDraft& d) { if (d.intervals <= 0) { return; } - double const span = d.end - d.begin; + SegWork const span = SubTo(d.end, d.begin); if (d.kind == CurveKind::kUniformStep || d.kind == CurveKind::kUniformValues) { - d.step0 = span / static_cast(d.intervals); + Rat const span_r = RatSub(d.end_rat, d.begin_rat); + std::int32_t const den = span_r.den * d.intervals; + if (den == 0) { + SegmentedSpecError(); + return; + } + d.step0 = SegWork::FromRatio(span_r.num, den); d.last_step = d.step0; - d.delta = 0.0; - d.r = 1.0; - d.q = 1.0; + d.delta = WorkZero(); + d.r = RatioOne(); + d.q = RatioOne(); return; } if (d.kind == CurveKind::kExponentialValues) { - d.r = ExpRatio(d.begin, d.end, d.intervals); - d.step0 = d.begin * (d.r - 1.0); - d.last_step = d.end * (1.0 - 1.0 / d.r); + d.log2_r = Log2RFromEndpoints(d.begin_rat, d.end_rat, d.intervals); + d.r = Exp2Ratio(d.log2_r); + d.step0 = ScaleWorkByRatio(d.begin, SubTo(d.r, RatioOne())); + SegRatio const rel_last = + DivTo(SubTo(d.r, RatioOne()), d.r); + d.last_step = ScaleWorkByRatio(d.end, rel_last); return; } if (d.kind == CurveKind::kGeometricStep) { - if (d.step_mode == StepMode::kLowerExplicit && d.specified_step > 0.0) { + Rat const span_r = RatSub(d.end_rat, d.begin_rat); + if (d.step_mode == StepMode::kLowerExplicit && + WorkPositive(d.specified_step)) { d.step0 = d.specified_step; - d.q = SolveQForGeomSum(d.intervals, span / d.step0); - d.last_step = d.step0 * gcem::pow(d.q, d.intervals - 1); + d.q = SolveQFromStepRat(d.intervals, span, d.step0, span_r, + d.specified_step_rat); + d.log2_q = Log2Ratio(d.q); } else if (d.step_mode == StepMode::kUpperExplicit && - d.specified_step > 0.0) { + WorkPositive(d.specified_step)) { d.last_step = d.specified_step; - d.q = SolveQForGeomSum(d.intervals, span / d.last_step); - d.step0 = d.last_step * gcem::pow(d.q, d.intervals - 1); - } else if (d.step_mode == StepMode::kLowerInherit && d.step0 > 0.0) { - d.q = SolveQForGeomSum(d.intervals, span / d.step0); - d.last_step = d.step0 * gcem::pow(d.q, d.intervals - 1); - } else if (d.step_mode == StepMode::kUpperInherit && d.last_step > 0.0) { - d.q = SolveQForGeomSum(d.intervals, span / d.last_step); - d.step0 = d.last_step * gcem::pow(d.q, d.intervals - 1); + d.q = SolveQFromStepRat(d.intervals, span, d.step0, span_r, + d.specified_step_rat); + d.log2_q = Log2Ratio(d.q); + } else if (d.step_mode == StepMode::kLowerInherit && + WorkPositive(d.step0)) { + d.q = SolveQForGeomSum(d.intervals, DivTo(span, d.step0)); + d.log2_q = Log2Ratio(d.q); + } else if (d.step_mode == StepMode::kUpperInherit && + WorkPositive(d.last_step)) { + d.q = SolveQForGeomSum(d.intervals, DivTo(span, d.last_step)); + d.log2_q = Log2Ratio(d.q); + } + if (d.intervals >= 1) { + SegWork const qpow = + Exp2Work(MulLogInt(d.log2_q, d.intervals - 1)); + if (GeomFromUpper(d) && WorkPositive(d.last_step)) { + d.step0 = MulWorkSame(d.last_step, qpow); + } else if (WorkPositive(d.step0)) { + d.last_step = MulWorkSame(d.step0, qpow); + } } return; } @@ -238,25 +294,27 @@ consteval void ComputeCoeffsKnownN(CurveDraft& d) { if (d.step_mode == StepMode::kUpperExplicit) { d.last_step = d.specified_step; } - if (d.step0 > 0.0 && d.last_step > 0.0) { - double const mean = 2.0 * span / static_cast(d.intervals); - (void)mean; - } - if (d.step0 > 0.0 && d.last_step <= 0.0 && d.intervals > 0) { - d.last_step = 2.0 * span / static_cast(d.intervals) - d.step0; + Rat const span_r = RatSub(d.end_rat, d.begin_rat); + SegWork const mean = SegWork::FromRatio( + span_r.num * 2, span_r.den * d.intervals); + if (WorkPositive(d.step0) && !WorkPositive(d.last_step) && + d.intervals > 0) { + d.last_step = SubTo(mean, d.step0); } - if (d.last_step > 0.0 && d.step0 <= 0.0 && d.intervals > 0 && - d.step_mode != StepMode::kLowerInherit) { - d.step0 = 2.0 * span / static_cast(d.intervals) - d.last_step; + if (WorkPositive(d.last_step) && !WorkPositive(d.step0) && + d.intervals > 0) { + d.step0 = SubTo(mean, d.last_step); } - if (d.intervals > 1 && d.step0 > 0.0 && d.last_step > 0.0) { - d.delta = (d.last_step - d.step0) / - static_cast(d.intervals - 1); + if (d.intervals > 1 && WorkPositive(d.step0) && WorkPositive(d.last_step)) { + d.delta = DivTo(SubTo(d.last_step, d.step0), + SegWork::FromRuntimeInteger(d.intervals - 1)); } - if (d.has_max_err_lower && d.step0 > 2.0 * d.max_err_lower + 1.0e-15) { + if (d.has_max_err_lower && + AddTo(d.max_err_lower, d.max_err_lower) < d.step0) { SegmentedSpecError(); } - if (d.has_max_err_upper && d.last_step > 2.0 * d.max_err_upper + 1.0e-12) { + if (d.has_max_err_upper && + AddTo(d.max_err_upper, d.max_err_upper) < d.last_step) { SegmentedSpecError(); } } @@ -266,14 +324,18 @@ consteval bool TryInherit(CurveDraft* d, int n) { bool changed = false; for (int i = 0; i < n; ++i) { if (d[i].step_mode == StepMode::kLowerInherit && i > 0) { - if (d[i - 1].last_step > 0.0) { - d[i].step0 = d[i - 1].last_step; + SegWork prev_last = d[i - 1].last_step; + if (!WorkPositive(prev_last) && d[i - 1].intervals > 0) { + prev_last = LastAbsStep(d[i - 1]); + } + if (WorkPositive(prev_last)) { + d[i].step0 = prev_last; changed = true; } } if (d[i].step_mode == StepMode::kUpperInherit && i + 1 < n) { - double nxt = 0.0; - if (d[i + 1].step0 > 0.0) { + SegWork nxt = WorkZero(); + if (WorkPositive(d[i + 1].step0)) { nxt = d[i + 1].step0; } else if (d[i + 1].intervals > 0 && (d[i + 1].kind == CurveKind::kUniformValues || @@ -281,7 +343,7 @@ consteval bool TryInherit(CurveDraft* d, int n) { d[i + 1].kind == CurveKind::kExponentialValues)) { nxt = FirstAbsStep(d[i + 1]); } - if (nxt > 0.0) { + if (WorkPositive(nxt)) { d[i].last_step = nxt; changed = true; } @@ -299,7 +361,10 @@ struct LogicalPlan { std::uint32_t n8 = 0; std::uint32_t code_count = 0; int max_bytes = 1; - double max_abs = 0.0; + std::int32_t max_abs_ceil = 1; + Rat declared_min{}; + Rat declared_max{}; + // Compile-time FNV-1a schema identity only — not encode/decode math. std::uint64_t schema_hash = 0; }; @@ -315,7 +380,6 @@ consteval void AssignWire(CurveDraft* d, int n, LogicalPlan& plan) { d[i].stored = StoredOf(d[i]); d[i].math_first = d[i].own_begin ? 0 : 1; } else if (!d[i].own_begin && d[i].math_first == 0) { - // FillTier pre-assigns stored but leaves math_first at 0. d[i].math_first = 1; } if (d[i].stored <= 0) { @@ -335,9 +399,28 @@ consteval void AssignWire(CurveDraft* d, int n, LogicalPlan& plan) { } } +consteval void PushContExpSlice(LogicalPlan& out, int& w, CurveDraft const& s, + int math_lo, int math_hi, int bytes, + int stored) { + CurveDraft c = s; + c.is_cont_exp = false; + c.bytes = bytes; + c.math_first = math_lo; + c.own_begin = true; + c.own_end = true; + c.stored = stored; + c.phys_begin = DecodeMath(s, math_lo); + c.phys_end = DecodeMath(s, math_hi); + out.segs[static_cast(w)] = c; + ++w; +} + consteval LogicalPlan SplitContExp(LogicalPlan in) { LogicalPlan out{}; out.max_bytes = in.max_bytes; + out.max_abs_ceil = in.max_abs_ceil; + out.declared_min = in.declared_min; + out.declared_max = in.declared_max; int w = 0; for (int i = 0; i < in.count; ++i) { CurveDraft const& s = in.segs[static_cast(i)]; @@ -348,27 +431,41 @@ consteval LogicalPlan SplitContExp(LogicalPlan in) { int const n = s.intervals; int const a = s.last_1; int const b = s.last_2; - auto push_slice = [&](int math_lo, int math_hi, int bytes, int stored) { - CurveDraft c = s; - c.is_cont_exp = false; - c.bytes = bytes; - c.math_first = math_lo; - c.own_begin = true; - c.own_end = true; - c.stored = stored; - c.phys_begin = DecodeMath(s, math_lo); - c.phys_end = DecodeMath(s, math_hi); - out.segs[static_cast(w++)] = c; - }; - push_slice(0, a, 1, a + 1); - push_slice(a + 1, b, 2, b - a); - push_slice(b + 1, n, 4, n - b); + PushContExpSlice(out, w, s, 0, a, 1, a + 1); + PushContExpSlice(out, w, s, a + 1, b, 2, b - a); + PushContExpSlice(out, w, s, b + 1, n, 4, n - b); } out.count = w; AssignWire(out.segs.data(), out.count, out); return out; } +consteval std::uint64_t HashDraft(std::uint64_t h, CurveDraft const& d) { + h = MixHash(h, static_cast(d.kind)); + h = MixRat(h, d.begin_rat); + h = MixRat(h, d.end_rat); + h = MixHash(h, static_cast(d.intervals)); + h = MixHash(h, static_cast(d.bytes)); + h = MixHash(h, static_cast(d.step_mode)); + h = MixRat(h, d.specified_step_rat); + h = MixHash(h, d.fill_tier ? 1U : 0U); + h = MixHash(h, d.min_intervals ? 1U : 0U); + h = MixHash(h, d.has_max_err_upper ? 1U : 0U); + h = MixRat(h, d.max_err_upper_rat); + h = MixHash(h, d.has_max_err_lower ? 1U : 0U); + h = MixRat(h, d.max_err_lower_rat); + h = MixHash(h, d.is_cont_exp ? 1U : 0U); + h = MixRat(h, d.cut1_rat); + h = MixRat(h, d.cut2_rat); + h = MixHash(h, static_cast(d.last_1)); + h = MixHash(h, static_cast(d.last_2)); + h = MixHash(h, d.own_begin ? 1U : 0U); + h = MixHash(h, d.own_end ? 1U : 0U); + h = MixHash(h, static_cast(d.stored)); + h = MixHash(h, static_cast(d.math_first)); + return h; +} + template consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { LogicalPlan plan{}; @@ -381,18 +478,24 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { SegmentedSpecError(); } + plan.declared_min = plan.segs[0].begin_rat; + plan.declared_max = plan.segs[0].end_rat; + Rat max_abs = RatAbsMax(plan.segs[0].begin_rat, plan.segs[0].end_rat); for (int i = 0; i < plan.count; ++i) { CurveDraft& d = plan.segs[static_cast(i)]; if (d.bytes > max_bytes && d.bytes != 0) { SegmentedSpecError(); } - double const mag = - gcem::abs(d.begin) > gcem::abs(d.end) ? gcem::abs(d.begin) - : gcem::abs(d.end); - if (mag > plan.max_abs) { - plan.max_abs = mag; + max_abs = RatAbsMax(max_abs, d.begin_rat); + max_abs = RatAbsMax(max_abs, d.end_rat); + if (RatLess(d.begin_rat, plan.declared_min)) { + plan.declared_min = d.begin_rat; + } + if (RatLess(plan.declared_max, d.end_rat)) { + plan.declared_max = d.end_rat; } } + plan.max_abs_ceil = CeilAbsRat(max_abs); for (int i = 0; i < plan.count; ++i) { CurveDraft& d = plan.segs[static_cast(i)]; @@ -409,12 +512,16 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { } CurveDraft& a = plan.segs[static_cast(i)]; CurveDraft& b = plan.segs[static_cast(i + 1)]; - if (!NearlyEqual(a.end, b.begin)) { + if (a.end_rat.num != b.begin_rat.num || + a.end_rat.den != b.begin_rat.den) { SegmentedSpecError(); } - auto const sp = AutoSplitTwoExp(a.begin, a.end, b.end, a.total_values - 1); + auto const sp = + AutoSplitTwoExp(a.begin_rat, a.end_rat, b.end_rat, a.total_values - 1); a.intervals = sp.n1; b.intervals = sp.n2; + a.log2_r = sp.log2_r1; + b.log2_r = sp.log2_r2; a.r = sp.r1; b.r = sp.r2; i = j - 1; @@ -430,17 +537,19 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { has4 ? static_cast(TwoTierMaxU8(static_cast(last1)) - 1U) : last1 + 1; - auto const ce = OptimizeContinuousExp(d.begin, d.end, d.cut1, d.cut2, last1, - last1 + 2, 2500, max_last2); + auto const ce = OptimizeContinuousExp(d.begin_rat, d.end_rat, d.cut1_rat, + d.cut2_rat, last1, last1 + 2, 1100, + max_last2); d.intervals = ce.intervals; d.last_1 = ce.last_1; d.last_2 = ce.last_2; + d.log2_r = ce.log2_r; d.r = ce.r; } if (d.kind == CurveKind::kUniformStep && d.intervals < 0 && - d.specified_step > 0.0) { - double const n = (d.end - d.begin) / d.specified_step; - d.intervals = RoundN(n); + (WorkPositive(d.specified_step) || d.specified_step_rat.num != 0)) { + d.intervals = RoundRatQuotient(RatSub(d.end_rat, d.begin_rat), + d.specified_step_rat); if (d.intervals < 1) { SegmentedSpecError(); } @@ -451,7 +560,6 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { bool has2 = false; bool has4 = false; - bool has8 = false; for (int i = 0; i < plan.count; ++i) { if (plan.segs[static_cast(i)].is_cont_exp) { has2 = true; @@ -463,11 +571,7 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { if (plan.segs[static_cast(i)].bytes == 4) { has4 = true; } - if (plan.segs[static_cast(i)].bytes == 8) { - has8 = true; - } } - (void)has8; int n1_known = 0; for (int i = 0; i < plan.count; ++i) { @@ -492,10 +596,9 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { } else if (d.bytes == 2) { std::uint32_t const b0 = n1_known > 0 ? static_cast(n1_known - 1) : 0; - std::uint64_t const tmax = TwoTierMaxU8(b0); - cap = has4 ? static_cast(tmax - static_cast(n1_known)) - : static_cast(tmax + 1U - - static_cast(n1_known)); + std::uint32_t const tmax = TwoTierMaxU8(b0); + cap = has4 ? static_cast(tmax - n1_known) + : static_cast(tmax + 1U - n1_known); } if (cap < 1) { SegmentedSpecError(); @@ -504,7 +607,7 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { d.intervals = cap - (d.own_begin ? 1 : 0) - (d.own_end ? 1 : 0) + 1; } - for (int pass = 0; pass < 8; ++pass) { + for (int pass = 0; pass < 2; ++pass) { for (int i = 0; i < plan.count; ++i) { ComputeCoeffsKnownN(plan.segs[static_cast(i)]); } @@ -514,14 +617,18 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { for (int i = 0; i < plan.count; ++i) { CurveDraft& d = plan.segs[static_cast(i)]; if (d.min_intervals) { - if (d.step0 <= 0.0 || !d.has_max_err_upper) { + if (!WorkPositive(d.step0) && i > 0) { + d.step0 = LastAbsStep(plan.segs[static_cast(i - 1)]); + } + if (!WorkPositive(d.step0) || !d.has_max_err_upper) { SegmentedSpecError(); } - d.intervals = MinRampIntervals(d.end - d.begin, d.step0, d.max_err_upper); + d.intervals = MinRampIntervals(SubTo(d.end, d.begin), d.step0, + d.max_err_upper); } } - for (int pass = 0; pass < 4; ++pass) { + for (int pass = 0; pass < 1; ++pass) { for (int i = 0; i < plan.count; ++i) { ComputeCoeffsKnownN(plan.segs[static_cast(i)]); } @@ -532,19 +639,20 @@ consteval LogicalPlan MakeUnsplitPlan(int max_bytes) { if (d.intervals < 1 && !d.is_cont_exp) { SegmentedSpecError(); } - if (d.step_mode == StepMode::kLowerInherit && d.step0 <= 0.0) { + if (d.step_mode == StepMode::kLowerInherit && !WorkPositive(d.step0)) { SegmentedSpecError(); } } - plan.schema_hash = MixHash(0xcbf29ce484222325ULL, - static_cast(plan.count)); + plan.schema_hash = + MixHash(0xcbf29ce484222325ULL, static_cast(plan.count)); + plan.schema_hash = + MixHash(plan.schema_hash, static_cast(max_bytes)); + plan.schema_hash = MixRat(plan.schema_hash, plan.declared_min); + plan.schema_hash = MixRat(plan.schema_hash, plan.declared_max); for (int i = 0; i < plan.count; ++i) { - CurveDraft const& d = plan.segs[static_cast(i)]; - plan.schema_hash = MixHash(plan.schema_hash, - static_cast(d.intervals)); - plan.schema_hash = MixHash(plan.schema_hash, - static_cast(d.bytes)); + plan.schema_hash = + HashDraft(plan.schema_hash, plan.segs[static_cast(i)]); } return plan; } @@ -563,29 +671,39 @@ consteval LogicalPlan CompileLogical() { if (plan.n1 == 0) { SegmentedSpecError(); } - plan.max_abs = unsplit.max_abs; + plan.max_abs_ceil = unsplit.max_abs_ceil; + plan.declared_min = unsplit.declared_min; + plan.declared_max = unsplit.declared_max; plan.schema_hash = MixHash(unsplit.schema_hash, plan.code_count); + plan.schema_hash = MixHash(plan.schema_hash, plan.n1); + plan.schema_hash = MixHash(plan.schema_hash, plan.n2); + plan.schema_hash = MixHash(plan.schema_hash, plan.n4); + plan.schema_hash = MixHash(plan.schema_hash, plan.n8); + for (int i = 0; i < plan.count; ++i) { + plan.schema_hash = + HashDraft(plan.schema_hash, plan.segs[static_cast(i)]); + } return plan; } struct CompiledSegment { - std::int64_t physical_begin_raw = 0; - std::int64_t physical_end_raw = 0; + std::uint32_t physical_begin_raw = 0; + std::uint32_t physical_end_raw = 0; std::uint32_t wire_code_begin = 0; std::uint32_t code_count = 0; CurveKind curve_kind = CurveKind::kUniformStep; std::uint8_t wire_bytes = 1; std::int32_t intervals = 0; std::int32_t math_first = 0; - std::int64_t curve_begin_raw = 0; - std::int64_t curve_end_raw = 0; - std::int64_t step0_raw = 0; - std::int64_t last_step_raw = 0; - std::int64_t delta_raw = 0; - std::int32_t log2_r_raw = 0; - std::int32_t log2_q_raw = 0; - std::int32_t log2_begin_raw = 0; - std::int32_t ratio_raw = 0; + std::uint32_t curve_begin_raw = 0; + std::uint32_t curve_end_raw = 0; + std::uint32_t step0_raw = 0; + std::uint32_t last_step_raw = 0; + std::uint32_t delta_raw = 0; + SegLog log2_r = LogZero(); + SegLog log2_q = LogZero(); + SegLog log2_begin = LogZero(); + SegLog log2_end = LogZero(); std::uint8_t from_upper = 0; }; @@ -635,9 +753,7 @@ using WireTypeOf = typename WireSelKind< PlanHolder::kPlan.n4 - 1U>::type; template -inline constexpr double kMaxAbsBound = PlanHolder::kPlan.max_abs == 0.0 - ? 1.0 - : PlanHolder::kPlan.max_abs; +inline constexpr auto kMaxAbsBound = PlanHolder::kPlan.max_abs_ceil; template struct LogicalTypeSel; @@ -661,17 +777,92 @@ template using FixedRuntimeOf = LogicalTypeOf; template -consteval std::int64_t RawAt(CurveDraft const& d, int i) { - return static_cast(RT::FromDouble(DecodeMath(d, i)).RawValue()); +inline constexpr bool kRtSigned = + std::is_signed_v; + +template +consteval std::uint32_t PackRtRaw(typename RT::rep_value_type v) { + if constexpr (kRtSigned) { + return static_cast(static_cast(v)); + } else { + return static_cast(v); + } +} + +template +consteval bool StoredRawLess(std::uint32_t a, std::uint32_t b) { + if constexpr (kRtSigned) { + return static_cast(a) < static_cast(b); + } else { + return a < b; + } +} + +template +consteval std::uint32_t StoredRawAbsDiff(std::uint32_t a, std::uint32_t b) { + if constexpr (kRtSigned) { + auto const ia = static_cast(a); + auto const ib = static_cast(b); + auto const ua = static_cast(ia); + auto const ub = static_cast(ib); + return ia >= ib ? ua - ub : ub - ua; + } else { + return a >= b ? a - b : b - a; + } +} + +template +consteval std::uint32_t RawFromWork(SegWork w) { + return PackRtRaw(ConvertFixed(w).RawValue()); +} + +template +consteval std::uint32_t RawFromRat(Rat r) { + return PackRtRaw(RT::FromRatio(r.num, r.den).RawValue()); +} + +template +consteval std::uint32_t RawAt(CurveDraft const& d, int i) { + if (i <= 0) { + return RawFromRat(d.begin_rat); + } + if (i >= d.intervals) { + return RawFromRat(d.end_rat); + } + std::uint32_t raw = RawFromWork(DecodeMath(d, i)); + std::uint32_t const b = RawFromRat(d.begin_rat); + std::uint32_t const e = RawFromRat(d.end_rat); + bool const rising = !StoredRawLess(e, b); + if (raw == e) { + if constexpr (kRtSigned) { + auto v = static_cast(raw); + v += rising ? -1 : 1; + raw = static_cast(v); + } else if (rising) { + raw -= 1U; + } else { + raw += 1U; + } + } else if (raw == b) { + if constexpr (kRtSigned) { + auto v = static_cast(raw); + v += rising ? 1 : -1; + raw = static_cast(v); + } else if (rising) { + raw += 1U; + } else { + raw -= 1U; + } + } + return raw; } template consteval CompiledSegment CompileOne(CurveDraft const& d) { - using Log = segmented_math_internal::SegFixedMathPolicy::log_type; CompiledSegment c{}; c.physical_begin_raw = RawAt(d, d.math_first); c.physical_end_raw = RawAt(d, d.math_first + d.stored - 1); - if (c.physical_end_raw < c.physical_begin_raw) { + if (StoredRawLess(c.physical_end_raw, c.physical_begin_raw)) { auto const t = c.physical_begin_raw; c.physical_begin_raw = c.physical_end_raw; c.physical_end_raw = t; @@ -682,29 +873,53 @@ consteval CompiledSegment CompileOne(CurveDraft const& d) { c.wire_bytes = static_cast(d.bytes); c.intervals = d.intervals; c.math_first = d.math_first; - c.curve_begin_raw = RawAt(d, 0); - c.curve_end_raw = RawAt(d, d.intervals); - if (d.intervals >= 1) { - c.step0_raw = RawAt(d, 1) - RawAt(d, 0); - c.last_step_raw = - RawAt(d, d.intervals) - RawAt(d, d.intervals - 1); - } - if (d.intervals >= 2) { - c.delta_raw = (RawAt(d, 2) - RawAt(d, 1)) - c.step0_raw; + c.curve_begin_raw = RawFromRat(d.begin_rat); + c.curve_end_raw = RawFromRat(d.end_rat); + c.step0_raw = RawFromWork(d.step0); + c.last_step_raw = RawFromWork(d.last_step); + c.delta_raw = RawFromWork(d.delta); + if (d.kind == CurveKind::kLinearStepRamp && d.intervals > 0) { + std::uint32_t const span = + StoredRawAbsDiff(c.curve_end_raw, c.curve_begin_raw); + std::uint32_t const n = static_cast(d.intervals); + std::uint32_t mean2u = 0; + if (!integer_math::MulDivU32Nearest(span, 2U, n, mean2u) || + mean2u > static_cast( + std::numeric_limits::max())) { + SegmentedSpecError(); + } + bool const rising = + !StoredRawLess(c.curve_end_raw, c.curve_begin_raw); + std::int32_t const mean2 = + rising ? static_cast(mean2u) + : -static_cast(mean2u); + auto const last_i = static_cast(c.last_step_raw); + auto const step0_i = static_cast(c.step0_raw); + bool const keep_last = d.step_mode == StepMode::kUpperExplicit || + d.step_mode == StepMode::kUpperInherit; + std::int32_t new_step0 = step0_i; + std::int32_t new_last = last_i; + if (keep_last) { + new_step0 = mean2 - last_i; + } else { + new_last = mean2 - step0_i; + } + c.step0_raw = static_cast(new_step0); + c.last_step_raw = static_cast(new_last); + if (n > 1U) { + c.delta_raw = static_cast( + fixed_point_internal::RoundDivNearest( + new_last - new_step0, static_cast(n - 1U))); + } } c.from_upper = GeomFromUpper(d) ? 1 : 0; - if (d.kind == CurveKind::kExponentialValues && d.begin > 0.0 && d.r > 0.0) { - c.log2_r_raw = static_cast( - Log::FromDouble(gcem::log(d.r) / gcem::log(2.0)).RawValue()); - c.log2_begin_raw = static_cast( - Log::FromDouble(gcem::log(d.begin) / gcem::log(2.0)).RawValue()); - c.ratio_raw = segmented_math_internal::RatioToQ30(d.r); - } - if (d.kind == CurveKind::kGeometricStep && d.q > 1.0) { - c.log2_q_raw = static_cast( - Log::FromDouble(gcem::log(d.q) / gcem::log(2.0)).RawValue()); - c.ratio_raw = static_cast( - segmented_math_internal::SegPow::FromDouble(d.q).RawValue()); + if (d.kind == CurveKind::kExponentialValues && d.begin_rat.num > 0) { + c.log2_r = d.log2_r; + c.log2_begin = Log2OfRat(d.begin_rat); + c.log2_end = Log2OfRat(d.end_rat); + } + if (d.kind == CurveKind::kGeometricStep) { + c.log2_q = d.log2_q; } return c; } @@ -716,11 +931,9 @@ consteval std::array MakeCompiledSegments() { for (int i = 0; i < kPlan.count; ++i) { CompiledSegment const c = CompileOne(kPlan.segs[static_cast(i)]); - std::int64_t span = c.physical_end_raw - c.physical_begin_raw; - if (span < 0) { - span = -span; - } - if (c.code_count > static_cast(span) + 1U) { + std::uint32_t const span = + StoredRawAbsDiff(c.physical_end_raw, c.physical_begin_raw); + if (c.code_count > span + 1U) { SegmentedSpecError(); } out[static_cast(i)] = c; @@ -729,8 +942,9 @@ consteval std::array MakeCompiledSegments() { CompiledSegment const key = out[static_cast(i)]; int j = i; while (j > 0 && - (out[static_cast(j - 1)].physical_begin_raw > - key.physical_begin_raw || + (StoredRawLess( + key.physical_begin_raw, + out[static_cast(j - 1)].physical_begin_raw) || (out[static_cast(j - 1)].physical_begin_raw == key.physical_begin_raw && out[static_cast(j - 1)].wire_code_begin > @@ -743,6 +957,45 @@ consteval std::array MakeCompiledSegments() { return out; } +template +struct CompiledSegHolder { + static constexpr std::array kAll = + MakeCompiledSegments(); +}; + +template +consteval std::array MakeExactCompiledSegments() { + constexpr auto all = CompiledSegHolder::kAll; + std::array out{}; + for (std::size_t i = 0; i < N; ++i) { + out[i] = all[i]; + } + return out; +} + +consteval std::size_t PackedDescriptorBytes(CurveKind kind) { + if (kind == CurveKind::kExponentialValues) { + return 32; + } + if (kind == CurveKind::kGeometricStep) { + return 32; + } + if (kind == CurveKind::kLinearStepRamp) { + return 40; + } + return 24; +} + +template +consteval std::size_t FormulaCoefficientBytes() { + constexpr LogicalPlan kPlan = PlanHolder::kPlan; + std::size_t n = 0; + for (int i = 0; i < kPlan.count; ++i) { + n += PackedDescriptorBytes(kPlan.segs[static_cast(i)].kind); + } + return n; +} + } // namespace ae::seg::segmented_compiler_internal #endif // AE_NUMERIC_DETAILS_SEGMENTED_COMPILER_H_ diff --git a/ae-numeric/details/segmented_curves.h b/ae-numeric/details/segmented_curves.h index f46726b..ffa6f46 100644 --- a/ae-numeric/details/segmented_curves.h +++ b/ae-numeric/details/segmented_curves.h @@ -26,8 +26,16 @@ namespace ae::seg::segmented_curves_internal { +using segmented_math_internal::LogZero; +using segmented_math_internal::NumberToRat; +using segmented_math_internal::Rat; +using segmented_math_internal::RatioOne; using segmented_math_internal::SegmentedSpecError; -using segmented_math_internal::ValueToDouble; +using segmented_math_internal::SegLog; +using segmented_math_internal::SegRatio; +using segmented_math_internal::SegWork; +using segmented_math_internal::WorkFromRat; +using segmented_math_internal::WorkZero; inline constexpr int kMaxDrafts = 16; @@ -41,37 +49,46 @@ enum class StepMode : std::uint8_t { struct CurveDraft { CurveKind kind = CurveKind::kUniformStep; - double begin = 0.0; - double end = 0.0; + Rat begin_rat{}; + Rat end_rat{}; + SegWork begin = WorkZero(); + SegWork end = WorkZero(); int bytes = 1; int intervals = -1; StepMode step_mode = StepMode::kNone; - double specified_step = 0.0; + Rat specified_step_rat{}; + SegWork specified_step = WorkZero(); bool fill_tier = false; bool min_intervals = false; - double max_err_upper = 0.0; + Rat max_err_upper_rat{}; + SegWork max_err_upper = WorkZero(); bool has_max_err_upper = false; - double max_err_lower = 0.0; + Rat max_err_lower_rat{}; + SegWork max_err_lower = WorkZero(); bool has_max_err_lower = false; int autosplit_id = 0; int total_values = 0; bool is_cont_exp = false; - double cut1 = 0.0; - double cut2 = 0.0; + Rat cut1_rat{}; + Rat cut2_rat{}; + SegWork cut1 = WorkZero(); + SegWork cut2 = WorkZero(); int last_1 = -1; int last_2 = -1; - double r = 1.0; - double q = 1.0; - double step0 = 0.0; - double last_step = 0.0; - double delta = 0.0; + SegLog log2_r = LogZero(); + SegLog log2_q = LogZero(); + SegRatio r = RatioOne(); + SegRatio q = RatioOne(); + SegWork step0 = WorkZero(); + SegWork last_step = WorkZero(); + SegWork delta = WorkZero(); bool own_begin = false; bool own_end = false; int stored = 0; int math_first = 0; std::uint32_t wire_begin = 0; - double phys_begin = 0.0; - double phys_end = 0.0; + SegWork phys_begin = WorkZero(); + SegWork phys_end = WorkZero(); }; template @@ -153,29 +170,29 @@ template inline constexpr bool kIsMathCurveV> = true; struct OptAcc { - double begin = 0.0; - double end = 0.0; + Rat begin{}; + Rat end{}; bool has_range = false; int bytes = -1; int intervals = -1; int total_values = -1; StepMode step_mode = StepMode::kNone; - double specified_step = 0.0; + Rat specified_step{}; bool fill_tier = false; bool min_intervals = false; - double max_err_upper = 0.0; + Rat max_err_upper{}; bool has_max_err_upper = false; - double max_err_lower = 0.0; + Rat max_err_lower{}; bool has_max_err_lower = false; - double cut1 = 0.0; - double cut2 = 0.0; + Rat cut1{}; + Rat cut2{}; int ncuts = 0; }; template consteval void ApplyOne(OptAcc& a, Range) { - a.begin = ValueToDouble(); - a.end = ValueToDouble(); + a.begin = NumberToRat(); + a.end = NumberToRat(); a.has_range = true; } @@ -200,7 +217,7 @@ consteval void ApplyOne(OptAcc& a, MinimumIntervals) { a.min_intervals = true; } template consteval void ApplyOne(OptAcc& a, Step) { - a.specified_step = ValueToDouble(); + a.specified_step = NumberToRat(); a.step_mode = StepMode::kLowerExplicit; } @@ -210,7 +227,7 @@ consteval void ApplyOne(OptAcc& a, StepAtLower) { a.step_mode = StepMode::kLowerInherit; } else { a.step_mode = StepMode::kLowerExplicit; - a.specified_step = ValueToDouble(); + a.specified_step = NumberToRat(); } } @@ -220,28 +237,28 @@ consteval void ApplyOne(OptAcc& a, StepAtUpper) { a.step_mode = StepMode::kUpperInherit; } else { a.step_mode = StepMode::kUpperExplicit; - a.specified_step = ValueToDouble(); + a.specified_step = NumberToRat(); } } template consteval void ApplyOne(OptAcc& a, MaxAbsErrorAtLower) { - a.max_err_lower = ValueToDouble(); + a.max_err_lower = NumberToRat(); a.has_max_err_lower = true; } template consteval void ApplyOne(OptAcc& a, MaxAbsErrorAtUpper) { - a.max_err_upper = ValueToDouble(); + a.max_err_upper = NumberToRat(); a.has_max_err_upper = true; } template consteval void ApplyOne(OptAcc& a, ApproximateCut) { if (a.ncuts == 0) { - a.cut1 = ValueToDouble(); + a.cut1 = NumberToRat(); } else { - a.cut2 = ValueToDouble(); + a.cut2 = NumberToRat(); } ++a.ncuts; } @@ -270,14 +287,17 @@ consteval OptAcc ParseOpts() { return a; } -consteval CurveDraft DraftFromAcc(CurveKind kind, OptAcc const& a, int bytes_fallback) { +consteval CurveDraft DraftFromAcc(CurveKind kind, OptAcc const& a, + int bytes_fallback) { CurveDraft d{}; d.kind = kind; if (!a.has_range) { SegmentedSpecError(); } - d.begin = a.begin; - d.end = a.end; + d.begin_rat = a.begin; + d.end_rat = a.end; + d.begin = WorkFromRat(a.begin); + d.end = WorkFromRat(a.end); if (d.end < d.begin) { SegmentedSpecError(); } @@ -287,15 +307,20 @@ consteval CurveDraft DraftFromAcc(CurveKind kind, OptAcc const& a, int bytes_fal } d.intervals = a.intervals; d.step_mode = a.step_mode; - d.specified_step = a.specified_step; + d.specified_step_rat = a.specified_step; + d.specified_step = WorkFromRat(a.specified_step); d.fill_tier = a.fill_tier; d.min_intervals = a.min_intervals; - d.max_err_upper = a.max_err_upper; + d.max_err_upper_rat = a.max_err_upper; + d.max_err_upper = WorkFromRat(a.max_err_upper); d.has_max_err_upper = a.has_max_err_upper; - d.max_err_lower = a.max_err_lower; + d.max_err_lower_rat = a.max_err_lower; + d.max_err_lower = WorkFromRat(a.max_err_lower); d.has_max_err_lower = a.has_max_err_lower; - d.cut1 = a.cut1; - d.cut2 = a.cut2; + d.cut1_rat = a.cut1; + d.cut2_rat = a.cut2; + d.cut1 = WorkFromRat(a.cut1); + d.cut2 = WorkFromRat(a.cut2); return d; } @@ -305,27 +330,31 @@ struct DraftsOf; template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { - out[i++] = DraftFromAcc(CurveKind::kUniformStep, ParseOpts(), bytes_fb); + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { + out[i++] = + DraftFromAcc(CurveKind::kUniformStep, ParseOpts(), bytes_fb); } }; template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { - out[i++] = - DraftFromAcc(CurveKind::kUniformValues, ParseOpts(), bytes_fb); + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { + out[i++] = DraftFromAcc(CurveKind::kUniformValues, ParseOpts(), + bytes_fb); } }; template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { - CurveDraft d = - DraftFromAcc(CurveKind::kExponentialValues, ParseOpts(), bytes_fb); - if (d.begin <= 0.0 || d.end <= 0.0) { + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { + CurveDraft d = DraftFromAcc(CurveKind::kExponentialValues, + ParseOpts(), bytes_fb); + if (d.begin_rat.num <= 0 || d.end_rat.num <= 0) { SegmentedSpecError(); } out[i++] = d; @@ -335,28 +364,31 @@ struct DraftsOf> { template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { - out[i++] = - DraftFromAcc(CurveKind::kGeometricStep, ParseOpts(), bytes_fb); + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { + out[i++] = DraftFromAcc(CurveKind::kGeometricStep, ParseOpts(), + bytes_fb); } }; template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { - out[i++] = - DraftFromAcc(CurveKind::kLinearStepRamp, ParseOpts(), bytes_fb); + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { + out[i++] = DraftFromAcc(CurveKind::kLinearStepRamp, ParseOpts(), + bytes_fb); } }; template struct DraftsOf> { static constexpr int kCount = 1; - static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, int bytes_fb) { + static consteval void Fill(CurveDraft* out, int& i, int /*as_id*/, + int bytes_fb) { CurveDraft d = DraftFromAcc(CurveKind::kExponentialValues, ParseOpts(), bytes_fb); - if (d.begin <= 0.0 || d.end <= 0.0) { + if (d.begin_rat.num <= 0 || d.end_rat.num <= 0) { SegmentedSpecError(); } d.is_cont_exp = true; @@ -389,7 +421,8 @@ consteval void FillIfCurve(CurveDraft* out, int& i, int as_id, int bytes) { template struct DraftsOf> { static constexpr int kCount = CountAutoCurves::value; - static consteval void Fill(CurveDraft* out, int& i, int as_id, int /*bytes_fb*/) { + static consteval void Fill(CurveDraft* out, int& i, int as_id, + int /*bytes_fb*/) { OptAcc const pack = ParseOpts(); int const bytes = pack.bytes; int const total = pack.total_values; diff --git a/ae-numeric/details/segmented_format.h b/ae-numeric/details/segmented_format.h index 08e42a3..d041fee 100644 --- a/ae-numeric/details/segmented_format.h +++ b/ae-numeric/details/segmented_format.h @@ -63,7 +63,6 @@ struct AutoTiered { namespace compute { struct Formula {}; -struct Lookup {}; } // namespace compute template diff --git a/ae-numeric/details/segmented_formula_backend.h b/ae-numeric/details/segmented_formula_backend.h index f1eefc9..b306463 100644 --- a/ae-numeric/details/segmented_formula_backend.h +++ b/ae-numeric/details/segmented_formula_backend.h @@ -17,304 +17,816 @@ #ifndef AE_NUMERIC_DETAILS_SEGMENTED_FORMULA_BACKEND_H_ #define AE_NUMERIC_DETAILS_SEGMENTED_FORMULA_BACKEND_H_ +#include #include #include +#include +#include #include "ae-numeric/details/segmented_compiler.h" #include "ae-numeric/details/segmented_math.h" +#include "ae-numeric/fixed_math.h" #include "ae-numeric/fixed_point.h" #include "ae-numeric/integer_math.h" namespace ae::seg::segmented_formula_internal { using segmented_compiler_internal::CompiledSegment; -using SegPow = segmented_math_internal::SegPow; +using segmented_compiler_internal::MakeCompiledSegments; +using segmented_compiler_internal::PlanHolder; +using segmented_math_internal::ConvertFixed; +using segmented_math_internal::Exp2MinusOne; +using segmented_math_internal::Exp2Pos; +using segmented_math_internal::GeomUnitWeightPos; +using segmented_math_internal::MulWorkSame; +using segmented_math_internal::Log2Pos; +using segmented_math_internal::Log2Work; +using segmented_math_internal::LogZero; +using segmented_math_internal::MulLogInt; +using segmented_math_internal::SegLog; +using segmented_math_internal::SegPosWork; +using segmented_math_internal::SegWork; +using segmented_math_internal::WorkAbs; +using segmented_math_internal::WorkOne; +using segmented_math_internal::WorkPositive; +using segmented_math_internal::WorkZero; -constexpr std::int64_t ClampI64(std::int64_t v, std::int64_t lo, - std::int64_t hi) { - if (v < lo) { - return lo; +#if defined(_MSC_VER) +#define AE_SEG_NOINLINE __declspec(noinline) +#else +#define AE_SEG_NOINLINE __attribute__((noinline)) +#endif + +inline constexpr int kNeighborRadius = 3; +inline constexpr int kRampRefineRadius = 2; + +using StoredRaw = std::uint32_t; +using WorkRaw = std::int32_t; + +template +inline constexpr bool kRtSigned = + std::is_signed_v; + +template +constexpr StoredRaw PackRtRaw(typename RT::rep_value_type v) { + if constexpr (kRtSigned) { + return static_cast(static_cast(v)); + } else { + return static_cast(v); } - if (v > hi) { - return hi; +} + +template +constexpr typename RT::rep_value_type UnpackRtRaw(StoredRaw v) { + using RV = typename RT::rep_value_type; + if constexpr (kRtSigned) { + return static_cast(static_cast(v)); + } else { + return static_cast(v); + } +} + +template +constexpr bool StoredLess(StoredRaw a, StoredRaw b) { + if constexpr (kSigned) { + return static_cast(a) < static_cast(b); + } else { + return a < b; + } +} + +template +constexpr std::uint32_t AbsStoredDiff(StoredRaw a, StoredRaw b) { + if constexpr (kSigned) { + auto const ia = static_cast(a); + auto const ib = static_cast(b); + auto const ua = static_cast(ia); + auto const ub = static_cast(ib); + return ia >= ib ? ua - ub : ub - ua; + } else { + return a >= b ? a - b : b - a; } - return v; } template -constexpr T FromI64Raw(std::int64_t raw) { - auto const clamped = ClampI64(raw, static_cast(T::kRawMin), - static_cast(T::kRawMax)); +constexpr T FromStoredRaw(StoredRaw raw) { return T::FromRaw( fixed_point_internal::RepFromRawValue( - static_cast(clamped))); + UnpackRtRaw(raw))); } -constexpr SegPow PowFromRatioRaw(std::int32_t ratio_raw) { - return FromI64Raw(static_cast(ratio_raw)); +template +constexpr SegWork WorkFromStored(StoredRaw raw) { + return ConvertFixed(FromStoredRaw(raw)); } -// Exponentiation by squaring in SegPow. e == 0 => 1. O(log e) muls, no heap. -constexpr SegPow PowUint(SegPow base, unsigned e) { - SegPow result = SegPow::FromRuntimeInteger(1); - SegPow b = base; - while (e > 0U) { - if ((e & 1U) != 0U) { - result = MulTo(result, b); - } - e >>= 1U; - if (e > 0U) { - b = MulTo(b, b); - } +template +constexpr WorkRaw DeltaToWork(StoredRaw packed) { + auto const src = static_cast(packed); + return fixed_point_internal::ConvertRawScaleTo( + src, RT::kScaleExp, SegWork::kScaleExp, SegWork::kRawMin, + SegWork::kRawMax); +} + +constexpr int RoundDivLog(SegLog num, SegLog den) { + auto const d = den.RawValue(); + if (d == static_cast(0)) { + return 0; } - return result; + return static_cast( + fixed_point_internal::RoundDivNearest(num.RawValue(), d)); } -constexpr std::int64_t LerpRaw(std::int64_t a, std::int64_t b, int i, int n) { +template +constexpr StoredRaw AddMag(StoredRaw base, std::uint32_t mag, bool subtract) { + if constexpr (kSigned) { + auto const b = static_cast(base); + auto const m = static_cast(mag); + return static_cast(subtract ? b - m : b + m); + } else { + return subtract ? base - mag : base + mag; + } +} + +template +constexpr StoredRaw LerpRaw(StoredRaw a, StoredRaw b, int i, int n) { if (n <= 0 || i <= 0) { return a; } if (i >= n) { return b; } - std::int64_t const diff = b - a; - bool const neg = diff < 0; - std::uint64_t out = 0; - if (!integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(diff), - static_cast(i), - static_cast(n), out)) { + bool const neg = StoredLess(b, a); + std::uint32_t out = 0; + if (!integer_math::MulDivU32Nearest(AbsStoredDiff(a, b), + static_cast(i), + static_cast(n), out)) { return i >= n / 2 ? b : a; } - auto const mag = static_cast(out); - return neg ? a - mag : a + mag; + return AddMag(a, out, neg); } -constexpr std::uint64_t RatioPowQ30(std::uint64_t base, unsigned e) { - std::uint64_t const one = - std::uint64_t{1} << segmented_math_internal::kSegRatioQ; - std::uint64_t result = one; - std::uint64_t b = base; - while (e > 0U) { - if ((e & 1U) != 0U) { - std::uint64_t out = 0; - if (!integer_math::MulDivU64Nearest(result, b, one, out)) { - return std::numeric_limits::max(); - } - result = out; - } - e >>= 1U; - if (e > 0U) { - std::uint64_t out = 0; - if (!integer_math::MulDivU64Nearest(b, b, one, out)) { - return std::numeric_limits::max(); - } - b = out; - } +template +constexpr StoredRaw AvoidEndpointCollision(StoredRaw raw, StoredRaw begin, + StoredRaw end, int math_i, + int intervals) { + if (math_i <= 0 || math_i >= intervals) { + return raw; + } + bool const rising = !StoredLess(end, begin); + if (raw == end) { + return AddMag(raw, 1U, rising); } - return result; + if (raw == begin) { + return AddMag(raw, 1U, !rising); + } + return raw; } template -constexpr std::int64_t ExpValueRaw(CompiledSegment const& s, int math_i) { +constexpr StoredRaw Exp2PosToStored(SegLog arg) { + SegPosWork const w = Exp2Pos(arg); + using RV = typename RT::rep_value_type; + RV const aligned = fixed_point_internal::ConvertRawScaleTo( + w.RawValue(), SegPosWork::kScaleExp, RT::kScaleExp, RT::kRawMin, + RT::kRawMax); + return PackRtRaw(aligned); +} + +template +constexpr StoredRaw LinearValueAt(StoredRaw begin, StoredRaw end, + WorkRaw step0_w, WorkRaw delta_w, int math_i, + int intervals) { if (math_i <= 0) { - return s.curve_begin_raw; - } - if (math_i >= s.intervals) { - return s.curve_end_raw; - } - if (s.ratio_raw <= 0 || s.curve_begin_raw <= 0) { - return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + return begin; + } + if (math_i >= intervals) { + return end; + } + std::uint32_t const n = static_cast(math_i); + std::uint32_t const n_all = static_cast(intervals); + std::uint32_t const pair = n * (n - 1U) / 2U; + std::uint32_t const den_pair = n_all * (n_all - 1U) / 2U; + std::uint32_t num_a = 0; + std::uint32_t num_b = 0; + std::uint32_t den_a = 0; + std::uint32_t den_b = 0; + if (!integer_math::MulU32Checked(n, integer_math::AbsI32ToU32(step0_w), + num_a) || + !integer_math::MulU32Checked(integer_math::AbsI32ToU32(delta_w), pair, + num_b) || + !integer_math::MulU32Checked(n_all, integer_math::AbsI32ToU32(step0_w), + den_a) || + !integer_math::MulU32Checked(integer_math::AbsI32ToU32(delta_w), den_pair, + den_b)) { + return end; + } + std::uint32_t num = 0; + std::uint32_t den = 0; + bool const step_delta_same = (step0_w < 0) == (delta_w < 0); + if (step_delta_same) { + if (!integer_math::AddU32Checked(num_a, num_b, num) || + !integer_math::AddU32Checked(den_a, den_b, den)) { + return end; + } + } else { + num = num_a >= num_b ? num_a - num_b : num_b - num_a; + den = den_a >= den_b ? den_a - den_b : den_b - den_a; } - std::uint64_t const p = - RatioPowQ30(static_cast(s.ratio_raw), - static_cast(math_i)); - std::uint64_t const one = - std::uint64_t{1} << segmented_math_internal::kSegRatioQ; - std::uint64_t out = 0; - if (!integer_math::MulDivU64Nearest( - integer_math::AbsI64ToU64(s.curve_begin_raw), p, one, out)) { - return s.curve_end_raw; + if (den == 0U) { + return begin; } - if (out > static_cast(std::numeric_limits::max())) { - return s.curve_end_raw; + std::uint32_t out = 0; + if (!integer_math::MulDivU32Nearest(AbsStoredDiff(end, begin), num, + den, out)) { + return end; } - return static_cast(out); + return AddMag(begin, out, StoredLess(end, begin)); } -constexpr std::int64_t LinearValueRaw(CompiledSegment const& s, int math_i) { +template +constexpr StoredRaw GeomValueAt(StoredRaw begin, StoredRaw end, SegLog log_q, + int math_i, int intervals, bool from_upper) { if (math_i <= 0) { - return s.curve_begin_raw; + return begin; + } + if (math_i >= intervals) { + return end; + } + int const k = from_upper ? (intervals - math_i) : math_i; + SegPosWork const w = GeomUnitWeightPos(log_q, k, intervals); + SegPosWork const one = SegPosWork::FromRuntimeInteger(1); + std::uint32_t off = 0; + integer_math::MulDivU32Nearest(AbsStoredDiff(end, begin), + w.RawValue(), one.RawValue(), off); + bool const neg = StoredLess(end, begin); + if (from_upper) { + return AddMag(end, off, !neg); + } + return AddMag(begin, off, neg); +} + +template +constexpr StoredRaw ExpValueAt(StoredRaw begin, StoredRaw end, SegLog log2_begin, + SegLog log2_r, int math_i, int intervals) { + if (math_i <= 0) { + return begin; } - if (math_i >= s.intervals) { - return s.curve_end_raw; + if (math_i >= intervals) { + return end; } - std::int64_t const n = math_i; - std::int64_t raw = s.curve_begin_raw; - raw += n * s.step0_raw; - raw += s.delta_raw * n * (n - 1) / 2; - return raw; + SegLog const arg = AddTo(log2_begin, MulLogInt(log2_r, math_i)); + return Exp2PosToStored(arg); } -constexpr std::int64_t GeomValueRaw(CompiledSegment const& s, int math_i) { - if (math_i <= 0) { - return s.curve_begin_raw; +template +constexpr int LinearApproxAt(StoredRaw begin, StoredRaw end, int intervals, + StoredRaw raw) { + if (begin == end || intervals <= 0) { + return 0; } - if (math_i >= s.intervals) { - return s.curve_end_raw; + std::uint32_t out = 0; + integer_math::MulDivU32Nearest(AbsStoredDiff(raw, begin), + static_cast(intervals), + AbsStoredDiff(end, begin), out); + return static_cast(out); +} + +constexpr int ExpApproxPos(SegPosWork x, SegLog lr, SegLog lb) { + if (x.RawValue() == static_cast(0)) { + return 0; } - std::int64_t const span = s.curve_end_raw - s.curve_begin_raw; - if (s.ratio_raw <= 0 || span == 0) { - return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + return RoundDivLog(SubTo(Log2Pos(x), lb), lr); +} + +AE_SEG_NOINLINE constexpr int GeomApproxWork(SegWork x, SegWork begin, SegWork end, + SegLog log_q, int intervals, int math_first, + bool from_upper) { + if (log_q.RawValue() <= static_cast(0)) { + return 0; } - int const n = s.from_upper != 0 ? (s.intervals - math_i) : math_i; - SegPow const q = PowFromRatioRaw(s.ratio_raw); - SegPow const qn = PowUint(q, static_cast(n)); - SegPow const qN = PowUint(q, static_cast(s.intervals)); - std::int64_t const one = - static_cast(SegPow::FromRuntimeInteger(1).RawValue()); - std::int64_t const num = static_cast(qn.RawValue()) - one; - std::int64_t const den = static_cast(qN.RawValue()) - one; - if (den == 0 || num < 0) { - return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + SegWork const span = SubTo(end, begin); + if (span.RawValue() == static_cast(0)) { + return math_first; } - bool const neg = span < 0; - std::uint64_t out = 0; - if (!integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(span), - integer_math::AbsI64ToU64(num), - integer_math::AbsI64ToU64(den), out)) { - return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + SegWork pos = from_upper ? SubTo(end, x) : SubTo(x, begin); + SegWork const abs_span = WorkAbs(span); + if (pos.RawValue() < static_cast(0)) { + pos = WorkZero(); + } else if (pos.RawValue() > abs_span.RawValue()) { + pos = abs_span; } - auto const mag = static_cast(out); - std::int64_t const offset = neg ? -mag : mag; - if (s.from_upper != 0) { - return s.curve_end_raw - offset; + SegWork const t = DivTo(pos, abs_span); + SegWork const qn_m1 = Exp2MinusOne(MulLogInt(log_q, intervals)); + SegWork const arg = AddTo(WorkOne(), MulWorkSame(t, qn_m1)); + if (!WorkPositive(arg)) { + return from_upper ? intervals : 0; } - return s.curve_begin_raw + offset; + int const k = RoundDivLog(Log2Work(arg), log_q); + if (from_upper) { + return intervals - k; + } + return k; } -template -constexpr std::int64_t DecodeMathRaw(CompiledSegment const& s, int math_i) { - if (s.curve_kind == CurveKind::kExponentialValues) { - return ExpValueRaw(s, math_i); - } - if (s.curve_kind == CurveKind::kGeometricStep) { - return GeomValueRaw(s, math_i); +constexpr std::uint32_t LinearShapeMag(int i, std::int32_t step0, + std::int32_t delta) { + if (i <= 0) { + return 0; } - if (s.curve_kind == CurveKind::kLinearStepRamp) { - return LinearValueRaw(s, math_i); + std::uint32_t const p = + static_cast(i) * static_cast(i - 1) / 2U; + std::uint32_t a = 0; + std::uint32_t b = 0; + if (!integer_math::MulU32Checked(static_cast(i), + integer_math::AbsI32ToU32(step0), a) || + !integer_math::MulU32Checked(integer_math::AbsI32ToU32(delta), p, b)) { + return std::numeric_limits::max(); + } + std::uint32_t s = 0; + bool const same = (step0 < 0) == (delta < 0); + if (same) { + if (!integer_math::AddU32Checked(a, b, s)) { + return std::numeric_limits::max(); + } + } else { + s = a >= b ? a - b : b - a; } - return LerpRaw(s.curve_begin_raw, s.curve_end_raw, math_i, s.intervals); + return s; } -template -constexpr std::int64_t DecodeRankRaw(CompiledSegment const* segs, int nseg, - std::uint32_t rank) { - for (int i = 0; i < nseg; ++i) { - CompiledSegment const& s = segs[i]; - if (rank >= s.wire_code_begin && - rank < s.wire_code_begin + s.code_count) { - int const local = static_cast(rank - s.wire_code_begin); - int const math_i = s.math_first + local; - return DecodeMathRaw(s, math_i); +constexpr bool DiscWide(std::uint32_t A, std::uint32_t B, std::uint32_t T, + bool at_neg, std::uint32_t& dhi, std::uint32_t& dlo) { + std::uint32_t bb_hi = 0; + std::uint32_t bb_lo = 0; + integer_math::MulU32Wide(B, B, bb_hi, bb_lo); + std::uint32_t at_hi = 0; + std::uint32_t at_lo = 0; + integer_math::MulU32Wide(A, T, at_hi, at_lo); + if (!integer_math::ShlU32WideChecked(at_hi, at_lo, 3U)) { + return false; + } + if (at_neg) { + if (bb_hi < at_hi || (bb_hi == at_hi && bb_lo < at_lo)) { + return false; } - } - return segs[0].curve_begin_raw; + bool const borrow = bb_lo < at_lo; + dlo = bb_lo - at_lo; + dhi = bb_hi - at_hi - (borrow ? 1U : 0U); + return true; + } + dlo = bb_lo + at_lo; + std::uint32_t const carry = dlo < bb_lo ? 1U : 0U; + dhi = bb_hi + at_hi + carry; + if (dhi < bb_hi) { + return false; + } + return true; } -constexpr int LinearApproxIndex(CompiledSegment const& s, std::int64_t raw) { - std::int64_t const span = s.curve_end_raw - s.curve_begin_raw; - if (span == 0 || s.intervals <= 0) { +template +AE_SEG_NOINLINE constexpr int LinearRampApproxRuntime( + StoredRaw begin, StoredRaw end, std::int32_t step0, std::int32_t delta, + int intervals, StoredRaw raw) { + if (delta == 0 || intervals <= 0) { + return LinearApproxAt(begin, end, intervals, raw); + } + if (begin == end) { return 0; } - std::uint64_t out = 0; - std::int64_t const pos = raw - s.curve_begin_raw; - integer_math::MulDivU64Nearest(integer_math::AbsI64ToU64(pos), - static_cast(s.intervals), - integer_math::AbsI64ToU64(span), out); - return static_cast(out); + std::uint32_t const n_all = static_cast(intervals); + std::uint32_t const pair = n_all * (n_all - 1U) / 2U; + std::uint32_t abs_step0_n = 0; + std::uint32_t abs_delta_pair = 0; + if (!integer_math::MulU32Checked(n_all, integer_math::AbsI32ToU32(step0), + abs_step0_n) || + !integer_math::MulU32Checked(integer_math::AbsI32ToU32(delta), pair, + abs_delta_pair)) { + return LinearApproxAt(begin, end, intervals, raw); + } + std::uint32_t abs_den = 0; + if ((step0 < 0) == (delta < 0)) { + if (!integer_math::AddU32Checked(abs_step0_n, abs_delta_pair, abs_den)) { + return LinearApproxAt(begin, end, intervals, raw); + } + } else if (abs_step0_n >= abs_delta_pair) { + abs_den = abs_step0_n - abs_delta_pair; + } else { + abs_den = abs_delta_pair - abs_step0_n; + } + if (abs_den == 0U) { + return LinearApproxAt(begin, end, intervals, raw); + } + std::uint32_t T = 0; + if (!integer_math::MulDivU32Nearest(abs_den, AbsStoredDiff(raw, begin), + AbsStoredDiff(end, begin), T)) { + return LinearApproxAt(begin, end, intervals, raw); + } + std::uint32_t const T_full = T; + std::uint32_t A = integer_math::AbsI32ToU32(delta); + std::int32_t const two_b = step0 + step0 - delta; + std::uint32_t B = integer_math::AbsI32ToU32(two_b); + bool const at_neg = + (delta < 0) != (StoredLess(raw, begin) != + StoredLess(end, begin)); + std::uint32_t dhi = 0; + std::uint32_t dlo = 0; + int guard = 0; + while (!DiscWide(A, B, T, at_neg, dhi, dlo) && guard < 32) { + A >>= 1U; + B >>= 1U; + T >>= 1U; + ++guard; + if ((A | B | T) == 0U) { + return LinearApproxAt(begin, end, intervals, raw); + } + } + if (!DiscWide(A, B, T, at_neg, dhi, dlo)) { + return LinearApproxAt(begin, end, intervals, raw); + } + std::uint32_t const root = integer_math::SqrtU32Wide(dhi, dlo); + std::uint32_t den = 0; + if (!integer_math::AddU32Checked(B, root, den) || den == 0U) { + return LinearApproxAt(begin, end, intervals, raw); + } + std::uint32_t n_u = 0; + if (!integer_math::MulDivU32Nearest(T, 4U, den, n_u) || + n_u > static_cast(std::numeric_limits::max() / 2)) { + return LinearApproxAt(begin, end, intervals, raw); + } + int n = static_cast(n_u); + int best = n; + std::uint32_t best_e = std::numeric_limits::max(); + for (int d = -kRampRefineRadius; d <= kRampRefineRadius; ++d) { + int const cand = n + d; + std::uint32_t const shape = LinearShapeMag(cand, step0, delta); + std::uint32_t const e = shape >= T_full ? shape - T_full : T_full - shape; + if (e < best_e) { + best_e = e; + best = cand; + } + } + return best; +} + +struct SegScalars { + CurveKind kind = CurveKind::kUniformStep; + std::uint32_t wire_begin = 0; + std::uint32_t code_count = 0; + int intervals = 0; + int math_first = 0; + StoredRaw begin = 0; + StoredRaw end = 0; + StoredRaw step0 = 0; + StoredRaw delta = 0; + SegLog log2_r = LogZero(); + SegLog log2_q = LogZero(); + SegLog log2_begin = LogZero(); + std::uint8_t from_upper = 0; +}; + +template +consteval SegScalars MakeSegScalars() { + CompiledSegment const p = + segmented_compiler_internal::CompiledSegHolder::kAll + [static_cast(I)]; + SegScalars s{}; + s.kind = p.curve_kind; + s.wire_begin = p.wire_code_begin; + s.code_count = p.code_count; + s.intervals = p.intervals; + s.math_first = p.math_first; + s.begin = p.curve_begin_raw; + s.end = p.curve_end_raw; + s.step0 = p.step0_raw; + s.delta = p.delta_raw; + s.log2_r = p.log2_r; + s.log2_q = p.log2_q; + s.log2_begin = p.log2_begin; + s.from_upper = p.from_upper; + return s; } +template +struct SegN { + static constexpr SegScalars k = MakeSegScalars(); + static constexpr bool kSigned = kRtSigned; + static constexpr CurveKind kKind = k.kind; + static constexpr std::uint32_t kWireBegin = k.wire_begin; + static constexpr std::uint32_t kCodeCount = k.code_count; + static constexpr int kIntervals = k.intervals; + static constexpr int kMathFirst = k.math_first; + static constexpr StoredRaw kBegin = k.begin; + static constexpr StoredRaw kEnd = k.end; + static constexpr StoredRaw kStep0 = k.step0; + static constexpr StoredRaw kDelta = k.delta; + static constexpr WorkRaw kStep0W = DeltaToWork(k.step0); + static constexpr WorkRaw kDeltaW = DeltaToWork(k.delta); + static constexpr SegLog kLog2R = k.log2_r; + static constexpr SegLog kLog2Q = k.log2_q; + static constexpr SegLog kLog2Begin = k.log2_begin; + static constexpr std::uint8_t kFromUpper = k.from_upper; +}; + template -constexpr int ClosestMathIndex(CompiledSegment const& s, std::int64_t raw) { - int const first = s.math_first; - int const last = s.math_first + static_cast(s.code_count) - 1; - if (last <= first) { - return first; - } - int lo = first; - int hi = last; - while (lo < hi) { - int const mid = lo + (hi - lo + 1) / 2; - if (DecodeMathRaw(s, mid) <= raw) { - lo = mid; - } else { - hi = mid - 1; - } - } - return lo; +constexpr StoredRaw RtToStored(typename RT::rep_value_type v) { + return PackRtRaw(v); } template -constexpr int ApproxIndex(CompiledSegment const& s, std::int64_t raw) { - if (s.intervals <= 1) { +constexpr typename RT::rep_value_type StoredToRt(StoredRaw v) { + return UnpackRtRaw(v); +} + +template +constexpr StoredRaw DecodeMath(int math_i) { + StoredRaw raw = 0; + if constexpr (S::kKind == CurveKind::kExponentialValues) { + raw = ExpValueAt(S::kBegin, S::kEnd, S::kLog2Begin, S::kLog2R, math_i, + S::kIntervals); + } else if constexpr (S::kKind == CurveKind::kGeometricStep) { + raw = GeomValueAt(S::kBegin, S::kEnd, S::kLog2Q, math_i, + S::kIntervals, S::kFromUpper != 0); + } else if constexpr (S::kKind == CurveKind::kLinearStepRamp) { + raw = LinearValueAt(S::kBegin, S::kEnd, S::kStep0W, S::kDeltaW, + math_i, S::kIntervals); + } else { + raw = LerpRaw(S::kBegin, S::kEnd, math_i, S::kIntervals); + } + return AvoidEndpointCollision(raw, S::kBegin, S::kEnd, math_i, + S::kIntervals); +} + +template +constexpr int ApproxIndex(StoredRaw raw) { + if (S::kIntervals <= 1) { return 0; } - if (s.curve_kind == CurveKind::kExponentialValues || - s.curve_kind == CurveKind::kGeometricStep) { - return ClosestMathIndex(s, raw); - } - if (s.curve_kind == CurveKind::kLinearStepRamp && s.delta_raw != 0) { - std::int64_t const a = s.delta_raw; - std::int64_t const b = 2 * s.step0_raw - s.delta_raw; - std::int64_t const cc = 2 * (s.curve_begin_raw - raw); - std::int64_t disc = b * b - 4 * a * cc; - if (disc < 0) { - disc = 0; - } - std::uint64_t const root = - integer_math::SqrtU64(static_cast(disc)); - std::int64_t const den = 2 * a; - if (den == 0) { - return 0; + if constexpr (S::kKind == CurveKind::kExponentialValues) { + SegLog const lr = S::kLog2R; + if (lr.RawValue() == static_cast(0)) { + return LinearApproxAt(S::kBegin, S::kEnd, S::kIntervals, raw); } - std::int64_t const num = -b + static_cast(root); - return static_cast(num / den); + return ExpApproxPos(ConvertFixed(FromStoredRaw(raw)), lr, + S::kLog2Begin); + } else if constexpr (S::kKind == CurveKind::kGeometricStep) { + return GeomApproxWork(WorkFromStored(raw), WorkFromStored(S::kBegin), + WorkFromStored(S::kEnd), S::kLog2Q, S::kIntervals, + S::kMathFirst, S::kFromUpper != 0); + } else if constexpr (S::kKind == CurveKind::kLinearStepRamp) { + return LinearRampApproxRuntime( + S::kBegin, S::kEnd, static_cast(S::kStep0), + static_cast(S::kDelta), S::kIntervals, raw); + } else { + return LinearApproxAt(S::kBegin, S::kEnd, S::kIntervals, raw); } - return LinearApproxIndex(s, raw); } -template -constexpr std::uint32_t EncodeRaw(CompiledSegment const* segs, int nseg, - std::uint32_t code_count, std::int64_t raw) { +template +constexpr void ConsiderRank(std::uint32_t& best, std::uint32_t& best_d, + int last, int j, StoredRaw raw) { + if (j < S::kMathFirst || j > last) { + return; + } + StoredRaw const dec = DecodeMath(j); + std::uint32_t const d = AbsStoredDiff(dec, raw); + std::uint32_t const rank = + S::kWireBegin + static_cast(j - S::kMathFirst); + if (d < best_d || (d == best_d && rank < best)) { + best_d = d; + best = rank; + } +} + +template +constexpr bool RankInSeg(std::uint32_t rank, StoredRaw& out) { + using S = SegN; + if (rank >= S::kWireBegin && rank < S::kWireBegin + S::kCodeCount) { + int const local = static_cast(rank - S::kWireBegin); + out = DecodeMath(S::kMathFirst + local); + return true; + } + return false; +} + +template +constexpr StoredRaw DecodeRankSeq(std::uint32_t /*rank*/, + std::integer_sequence) { + using S0 = SegN; + // Invalid rank is a caller contract violation; keep a debug assert only. + // Do not call std::abort() — that would pull a libc dependency into the + // mathematical footprint objects on bare-metal targets. + assert(false && "invalid segmented rank"); + return S0::kBegin; +} + +template +constexpr StoredRaw DecodeRankSeq(std::uint32_t rank, + std::integer_sequence) { + StoredRaw out = 0; + if (RankInSeg(rank, out)) { + return out; + } + return DecodeRankSeq(rank, std::integer_sequence{}); +} + +template +constexpr void AccSegRange(StoredRaw& mn, StoredRaw& mx) { + using S = SegN; + int const first = S::kMathFirst; + int const last = first + static_cast(S::kCodeCount) - 1; + StoredRaw const a = DecodeMath(first); + StoredRaw const b = DecodeMath(last); + if (StoredLess>(a, mn)) { + mn = a; + } + if (StoredLess>(b, mn)) { + mn = b; + } + if (StoredLess>(mx, a)) { + mx = a; + } + if (StoredLess>(mx, b)) { + mx = b; + } +} + +template +constexpr void AccRangeSeq(StoredRaw& /*mn*/, StoredRaw& /*mx*/, + std::integer_sequence) {} + +template +constexpr void AccRangeSeq(StoredRaw& mn, StoredRaw& mx, + std::integer_sequence) { + AccSegRange(mn, mx); + AccRangeSeq(mn, mx, std::integer_sequence{}); +} + +template +constexpr void EncodeSeg(std::uint32_t& best, std::uint32_t& best_d, + StoredRaw raw) { + using S = SegN; + int approx = ApproxIndex(raw); + int const last = S::kMathFirst + static_cast(S::kCodeCount) - 1; + if (approx < S::kMathFirst) { + approx = S::kMathFirst; + } + if (approx > last) { + approx = last; + } + int const lo_j = (approx < S::kMathFirst + kNeighborRadius) + ? S::kMathFirst + : (approx - kNeighborRadius); + int const hi_j = + (approx + kNeighborRadius > last) ? last : (approx + kNeighborRadius); + for (int j = lo_j; j <= hi_j; ++j) { + ConsiderRank(best, best_d, last, j, raw); + } + ConsiderRank(best, best_d, last, S::kMathFirst, raw); + ConsiderRank(best, best_d, last, last, raw); +} + +template +constexpr void EncodeRec(std::uint32_t& /*best*/, std::uint32_t& /*best_d*/, + StoredRaw /*raw*/, std::integer_sequence) {} + +template +constexpr void EncodeRec(std::uint32_t& best, std::uint32_t& best_d, + StoredRaw raw, + std::integer_sequence) { + EncodeSeg(best, best_d, raw); + EncodeRec(best, best_d, raw, std::integer_sequence{}); +} + +template +using SegIndexSeq = + std::make_integer_sequence::kPlan.count>; + +template +AE_SEG_NOINLINE constexpr typename RT::rep_value_type DecodeRankRaw( + std::uint32_t rank) { + return StoredToRt(DecodeRankSeq(rank, SegIndexSeq{})); +} + +template +AE_SEG_NOINLINE constexpr std::uint32_t EncodeRaw( + std::uint32_t code_count, typename RT::rep_value_type raw) { std::uint32_t best = 0; - std::uint64_t best_d = std::numeric_limits::max(); - for (int si = 0; si < nseg; ++si) { - CompiledSegment const& s = segs[si]; - int approx = ApproxIndex(s, raw); - int const last = s.math_first + static_cast(s.code_count) - 1; - if (approx < s.math_first) { - approx = s.math_first; + std::uint32_t best_d = std::numeric_limits::max(); + EncodeRec(best, best_d, RtToStored(raw), SegIndexSeq{}); + if (best >= code_count) { + return code_count - 1U; + } + return best; +} + +template +constexpr typename RT::rep_value_type ScanRepresentable(bool want_min) { + using S0 = SegN; + StoredRaw mn = DecodeMath(S0::kMathFirst); + StoredRaw mx = mn; + AccRangeSeq(mn, mx, SegIndexSeq{}); + return StoredToRt(want_min ? mn : mx); +} + +template +int RampIndexErrorOnCodes() { + using S = SegN; + if constexpr (S::kKind != CurveKind::kLinearStepRamp) { + return 0; + } else { + int max_err = 0; + int const last = S::kMathFirst + static_cast(S::kCodeCount) - 1; + for (int true_j = S::kMathFirst; true_j <= last; ++true_j) { + StoredRaw const raw = DecodeMath(true_j); + int const approx = LinearRampApproxRuntime( + S::kBegin, S::kEnd, static_cast(S::kStep0), + static_cast(S::kDelta), S::kIntervals, raw); + int err = approx - true_j; + if (err < 0) { + err = -err; + } + if (err > max_err) { + max_err = err; + } } - if (approx > last) { - approx = last; + return max_err; + } +} + +template +int RampIndexErrorDense() { + using S = SegN; + if constexpr (S::kKind != CurveKind::kLinearStepRamp) { + return 0; + } else { + int max_err = 0; + int const last = S::kMathFirst + static_cast(S::kCodeCount) - 1; + StoredRaw lo = S::kBegin; + StoredRaw hi = S::kEnd; + if (StoredLess(hi, lo)) { + StoredRaw const t = lo; + lo = hi; + hi = t; } - int const lo_j = (approx < s.math_first + 4) ? s.math_first : (approx - 4); - int const hi_j = (approx + 4 > last) ? last : (approx + 4); - for (int j = lo_j; j <= hi_j; ++j) { - std::int64_t const dec = DecodeMathRaw(s, j); - std::uint64_t const d = integer_math::AbsI64ToU64(dec - raw); - std::uint32_t const rank = - s.wire_code_begin + static_cast(j - s.math_first); - if (d < best_d || (d == best_d && rank < best)) { - best_d = d; - best = rank; + StoredRaw const span = AbsStoredDiff(hi, lo); + StoredRaw step = span / 4096U; + if (step < 1U) { + step = 1U; + } + for (StoredRaw raw = lo; !StoredLess(hi, raw); ) { + int const approx = LinearRampApproxRuntime( + S::kBegin, S::kEnd, static_cast(S::kStep0), + static_cast(S::kDelta), S::kIntervals, raw); + int true_j = S::kMathFirst; + std::uint32_t best_d = std::numeric_limits::max(); + for (int j = S::kMathFirst; j <= last; ++j) { + std::uint32_t const d = + AbsStoredDiff(DecodeMath(j), raw); + if (d < best_d) { + best_d = d; + true_j = j; + } + } + int err = approx - true_j; + if (err < 0) { + err = -err; + } + if (err > max_err) { + max_err = err; + } + if (raw > std::numeric_limits::max() - step) { + break; } + raw += step; } + return max_err; } - if (best >= code_count) { - return code_count - 1U; - } - return best; +} + +template +int MaxRampIndexErrorRec(std::integer_sequence, bool dense) { + (void)dense; + return 0; +} + +template +int MaxRampIndexErrorRec(std::integer_sequence, bool dense) { + int const a = dense ? RampIndexErrorDense() + : RampIndexErrorOnCodes(); + int const b = MaxRampIndexErrorRec( + std::integer_sequence{}, dense); + return a > b ? a : b; +} + +template +int MaxRampIndexError() { + return MaxRampIndexErrorRec(SegIndexSeq{}, false); +} + +template +int MaxRampIndexErrorDenseInputs() { + return MaxRampIndexErrorRec(SegIndexSeq{}, true); } } // namespace ae::seg::segmented_formula_internal diff --git a/ae-numeric/details/segmented_lookup_backend.h b/ae-numeric/details/segmented_lookup_backend.h deleted file mode 100644 index 2edaea5..0000000 --- a/ae-numeric/details/segmented_lookup_backend.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2026 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ -#define AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ - -#include -#include -#include -#include - -#include "ae-numeric/details/segmented_compiler.h" -#include "ae-numeric/details/segmented_formula_backend.h" -#include "ae-numeric/integer_math.h" - -namespace ae::seg::segmented_lookup_internal { - -template -struct LookupTables { - std::array decoded{}; - std::array order{}; -}; - -template -consteval LookupTables MakeLookupTables() { - using segmented_compiler_internal::MakeCompiledSegments; - using segmented_compiler_internal::PlanHolder; - LookupTables t{}; - constexpr auto kSegs = MakeCompiledSegments(); - constexpr int kNs = PlanHolder::kPlan.count; - for (std::size_t i = 0; i < N; ++i) { - t.decoded[i] = segmented_formula_internal::DecodeRankRaw( - kSegs.data(), kNs, static_cast(i)); - t.order[i] = static_cast(i); - } - for (std::size_t i = 1; i < N; ++i) { - std::uint32_t const key = t.order[i]; - std::int64_t const keyv = t.decoded[key]; - std::size_t j = i; - while (j > 0 && t.decoded[t.order[j - 1]] > keyv) { - t.order[j] = t.order[j - 1]; - --j; - } - t.order[j] = key; - } - return t; -} - -template -constexpr std::uint32_t LookupEncode(LookupTables const& t, - std::int64_t raw) { - if (N == 0) { - return 0; - } - std::size_t lo = 0; - std::size_t hi = N; - while (lo < hi) { - std::size_t const mid = lo + (hi - lo) / 2U; - if (t.decoded[t.order[mid]] < raw) { - lo = mid + 1U; - } else { - hi = mid; - } - } - std::uint32_t best = t.order[lo < N ? lo : N - 1U]; - std::uint64_t best_d = integer_math::AbsI64ToU64(t.decoded[best] - raw); - auto consider = [&](std::size_t idx) { - if (idx >= N) { - return; - } - std::uint32_t const rank = t.order[idx]; - std::uint64_t const d = - integer_math::AbsI64ToU64(t.decoded[rank] - raw); - if (d < best_d || (d == best_d && rank < best)) { - best_d = d; - best = rank; - } - }; - if (lo > 0) { - consider(lo - 1U); - } - consider(lo); - if (lo + 1U < N) { - consider(lo + 1U); - } - return best; -} - -} // namespace ae::seg::segmented_lookup_internal - -#endif // AE_NUMERIC_DETAILS_SEGMENTED_LOOKUP_BACKEND_H_ diff --git a/ae-numeric/details/segmented_math.h b/ae-numeric/details/segmented_math.h index 7f18a1c..34d605a 100644 --- a/ae-numeric/details/segmented_math.h +++ b/ae-numeric/details/segmented_math.h @@ -17,148 +17,614 @@ #ifndef AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ #define AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ -#include #include #include #include -#include - #include "ae-numeric/decimal.h" #include "ae-numeric/details/segmented_format.h" +#include "ae-numeric/fixed_math.h" #include "ae-numeric/fixed_point.h" #include "ae-numeric/integer_math.h" namespace ae::seg::segmented_math_internal { -struct SegFixedMathPolicy { - using log_type = FixedPoint; - using mant_type = FixedPoint; - using mul_intermediate_type = std::int64_t; - static constexpr int kLogIterations = 16; - static constexpr int kExp2FractionBits = 16; -}; +using SegPolicy = fixed_math::Segmented32MathPolicy; +// Max covers RX 24 h (86400 s). int32 is used for signed physical values +// (temperature). Positive Exp2/Log2 use uint32 for extra fraction bits. +using SegWork = FixedPoint; +using SegPosWork = FixedPoint; +using SegRatio = FixedPoint; +using SegLog = SegPolicy::log_type; + +static_assert(sizeof(typename SegWork::rep_value_type) <= 4); +static_assert(sizeof(typename SegPosWork::rep_value_type) <= 4); +static_assert(sizeof(typename SegRatio::rep_value_type) <= 4); +static_assert(sizeof(typename SegLog::rep_value_type) <= 4); inline void SegmentedSpecError() {} -// High-resolution ratio (r, q ≈ 1). Geometric decode uses SegPow -// exponentiation-by-squaring (max 256 covers q^N up to ~23). Exponential -// decode uses a Q30 integer ratio (kSegRatioQ) instead. -using SegPow = FixedPoint; +struct Rat { + std::int32_t num = 0; + std::int32_t den = 1; +}; + +template +consteval Rat NumberToRat() { + constexpr auto n = NumberRatio::num; + constexpr auto d = NumberRatio::den; + static_assert(n >= std::numeric_limits::min() && + n <= std::numeric_limits::max()); + static_assert(d >= std::numeric_limits::min() && + d <= std::numeric_limits::max()); + return {static_cast(n), static_cast(d)}; +} + +consteval std::int32_t AbsI32(std::int32_t v) { + return v < 0 ? static_cast(-v) : v; +} + +consteval bool RatLess(Rat a, Rat b) { + // a.n/a.d < b.n/b.d <=> a.n*b.d < b.n*a.d (positive dens in our DSL) + std::uint32_t an = integer_math::AbsI32ToU32(a.num); + std::uint32_t bd = integer_math::AbsI32ToU32(b.den); + std::uint32_t bn = integer_math::AbsI32ToU32(b.num); + std::uint32_t ad = integer_math::AbsI32ToU32(a.den); + bool const a_neg = a.num < 0; + bool const b_neg = b.num < 0; + if (a_neg != b_neg) { + return a_neg; + } + int const cmp = integer_math::CmpMulU32(an, bd, bn, ad); + return a_neg ? cmp > 0 : cmp < 0; +} + +consteval bool RatAbsLess(Rat a, Rat b) { + return integer_math::CmpMulU32(integer_math::AbsI32ToU32(a.num), + integer_math::AbsI32ToU32(b.den), + integer_math::AbsI32ToU32(b.num), + integer_math::AbsI32ToU32(a.den)) < 0; +} + +consteval Rat RatAbsMax(Rat a, Rat b) { + return RatAbsLess(a, b) ? b : a; +} + +consteval std::int32_t CeilAbsRat(Rat r) { + std::int32_t const n = AbsI32(r.num); + std::int32_t const d = r.den <= 0 ? 1 : r.den; + std::int32_t const q = static_cast( + (static_cast(n) + static_cast(d) - 1U) / + static_cast(d)); + return q < 1 ? 1 : q; +} + +consteval SegWork WorkFromRat(Rat r) { + return SegWork::FromRatio(r.num, r.den); +} + +constexpr SegLog LogZero() { + return SegLog::FromRuntimeInteger(0); +} + +constexpr SegWork WorkZero() { + return SegWork::FromRuntimeInteger(0); +} + +constexpr SegWork WorkOne() { + return SegWork::FromRuntimeInteger(1); +} + +constexpr SegRatio RatioOne() { + return SegRatio::FromRuntimeInteger(1); +} + +constexpr SegLog WorkLogFromRaw(typename SegLog::rep_value_type raw) { + return SegLog::FromRaw( + fixed_point_internal::RepFromRawValue( + SegLog::ClampRaw(raw))); +} -inline constexpr int kSegRatioQ = 30; +constexpr SegWork WorkFromRaw(typename SegWork::rep_value_type raw) { + return SegWork::FromRaw( + fixed_point_internal::RepFromRawValue( + SegWork::ClampRaw(raw))); +} + +template +constexpr To ConvertFixed(From x) { + static_assert(sizeof(typename From::rep_value_type) <= 4); + static_assert(sizeof(typename To::rep_value_type) <= 4); + using ToR = typename To::rep_value_type; + ToR const aligned = fixed_point_internal::ConvertRawScaleTo( + x.RawValue(), From::kScaleExp, To::kScaleExp, To::kRawMin, To::kRawMax); + return To::FromRaw( + fixed_point_internal::RepFromRawValue(aligned)); +} + +consteval Rat RatSub(Rat a, Rat b) { + // (a.n*b.d - b.n*a.d) / (a.d*b.d); all DSL bounds fit int32. + std::uint32_t p1h = 0; + std::uint32_t p1l = 0; + std::uint32_t p2h = 0; + std::uint32_t p2l = 0; + integer_math::MulI32Wide(a.num, b.den, p1h, p1l); + integer_math::MulI32Wide(b.num, a.den, p2h, p2l); + // Subtract p2 from p1 in two's complement wide form. + std::uint32_t nl = p1l - p2l; + std::uint32_t const borrow = p1l < p2l ? 1U : 0U; + std::uint32_t nh = p1h - p2h - borrow; + bool const neg = (nh & 0x80000000U) != 0U; + if (neg) { + integer_math::NegU32Wide(nh, nl); + } + if (nh != 0U || nl > static_cast( + std::numeric_limits::max())) { + SegmentedSpecError(); + return {}; + } + std::uint32_t dh = 0; + std::uint32_t dl = 0; + integer_math::MulI32Wide(a.den, b.den, dh, dl); + if (dh != 0U || dl == 0U || + dl > static_cast( + std::numeric_limits::max())) { + SegmentedSpecError(); + return {}; + } + auto num = static_cast(nl); + if (neg) { + num = -num; + } + return {num, static_cast(dl)}; +} -consteval std::int32_t RatioToQ30(double r) { - double const scaled = r * static_cast(std::uint64_t{1} << kSegRatioQ); - if (scaled < 1.0 || - scaled > static_cast(std::numeric_limits::max())) { +consteval int RoundRatQuotient(Rat span, Rat step) { + if (step.num == 0 || step.den == 0 || span.den == 0) { + SegmentedSpecError(); + return 1; + } + std::uint32_t nh = 0; + std::uint32_t nl = 0; + std::uint32_t dh = 0; + std::uint32_t dl = 0; + integer_math::MulU32Wide(integer_math::AbsI32ToU32(span.num), + integer_math::AbsI32ToU32(step.den), nh, nl); + integer_math::MulU32Wide(integer_math::AbsI32ToU32(span.den), + integer_math::AbsI32ToU32(step.num), dh, dl); + if (dh != 0U) { + while (dh != 0U) { + integer_math::ShrU32Wide(nh, nl, 1U, false); + integer_math::ShrU32Wide(dh, dl, 1U, false); + } + } + if (dl == 0U) { SegmentedSpecError(); + return 1; + } + std::uint32_t q = 0; + std::uint32_t r = 0; + if (!integer_math::DivU32Wide(nh, nl, dl, q, r) || q > 100000U) { + SegmentedSpecError(); + return 1; + } + if (r >= dl - r && q != std::numeric_limits::max()) { + ++q; + } + if (q < 1U) { + return 1; + } + return static_cast(q); +} + +constexpr SegWork MulWorkSame(SegWork a, SegWork b) { + return fixed_point_internal::MulFixedPoint(a, b); +} + +constexpr SegWork ScaleWorkByRatio(SegWork w, SegRatio r) { + // Multiply in-place: do not ConvertFixed(r). SegWork Max is 86400, + // so a ratio near 1 would keep only ~15 significant bits after conversion. + return fixed_point_internal::MulFixedPoint(w, r); +} + +constexpr SegWork ScaleWorkByInt(SegWork w, int n) { + if (n == 0 || w.RawValue() == static_cast(0)) { + return WorkZero(); + } + bool const negative = (w.RawValue() < 0) != (n < 0); + std::uint32_t hi = 0; + std::uint32_t lo = 0; + integer_math::MulU32Wide(integer_math::AbsI32ToU32(w.RawValue()), + integer_math::AbsI32ToU32(static_cast(n)), + hi, lo); + if (hi != 0U || + lo > static_cast(std::numeric_limits::max())) { + return negative ? SegWork::FromRaw(SegWork::kRawMin) + : SegWork::FromRaw(SegWork::kRawMax); + } + auto out = static_cast(lo); + if (negative) { + out = -out; + } + return WorkFromRaw(out); +} + +constexpr bool WorkPositive(SegWork x) { + return x.RawValue() > static_cast(0); +} + +constexpr bool WorkNonPositive(SegWork x) { + return x.RawValue() <= static_cast(0); +} + +constexpr SegWork WorkAbs(SegWork x) { + if (x.RawValue() < static_cast(0)) { + return SubTo(WorkZero(), x); + } + return x; +} + +consteval int WorkToNearestInt(SegWork x) { + auto const one = WorkOne().RawValue(); + if (one == 0) { return 0; } - return static_cast(scaled + 0.5); + return static_cast(fixed_point_internal::RoundDivNearest( + x.RawValue(), one)); } -template -consteval double ValueToDouble() { - if constexpr (kIsDecimalV) { - double const mag = static_cast( - T::kMantissa < 0 ? -T::kMantissa : T::kMantissa); - double const scaled = - T::kExponent10 >= 0 - ? mag * static_cast(Pow10u(static_cast( - T::kExponent10))) - : mag / static_cast(Pow10u(static_cast( - -T::kExponent10))); - return T::kMantissa < 0 ? -scaled : scaled; - } else if constexpr (kIsRatioV) { - return static_cast(T::kNum) / static_cast(T::kDen); - } else { +consteval int CeilDivWork(SegWork num, SegWork den) { + auto const a = num.RawValue(); + auto const b = den.RawValue(); + if (b <= 0 || a <= 0) { + SegmentedSpecError(); + return 1; + } + std::uint32_t const ua = integer_math::AbsI32ToU32(a); + std::uint32_t const ub = integer_math::AbsI32ToU32(b); + std::uint32_t q = ua / ub; + if (ua % ub != 0U) { + if (q == std::numeric_limits::max()) { + SegmentedSpecError(); + return 100000; + } + ++q; + } + if (q < 1U) { + return 1; + } + if (q > 100000U) { SegmentedSpecError(); - return 0.0; + return 100000; } + return static_cast(q); } -consteval double GeomSum(double q, int n) { - if (n <= 0) { - return 0.0; +constexpr SegLog Log2Work(SegWork x) { + if (x.RawValue() <= static_cast(0)) { + return SegLog::FromRaw(SegLog::kRawMin); } - if (gcem::abs(q - 1.0) < 1.0e-18) { - return static_cast(n); + return fixed_math::Log2To( + ConvertFixed(x)); +} + +constexpr SegLog Log2Pos(SegPosWork x) { + if (x.RawValue() == static_cast(0)) { + return SegLog::FromRaw(SegLog::kRawMin); } - return (gcem::pow(q, n) - 1.0) / (q - 1.0); + return fixed_math::Log2To(x); +} + +constexpr SegLog Log2Ratio(SegRatio x) { + return fixed_math::Log2To(x); } -// Solve (q^n - 1)/(q-1) = S for q > 1. -consteval double SolveQForGeomSum(int n, double sum) { - if (n <= 0 || sum <= 0.0) { +consteval SegLog Log2OfRat(Rat r) { + if (r.num <= 0 || r.den <= 0) { SegmentedSpecError(); - return 1.0; + return SegLog::FromRuntimeInteger(0); + } + SegLog const ln = + Log2Pos(SegPosWork::FromInteger(r.num)); + SegLog const ld = + Log2Pos(SegPosWork::FromInteger(r.den)); + return SubTo(ln, ld); +} + +constexpr SegPosWork Exp2Pos(SegLog y) { + return fixed_math::Exp2To(y); +} + +constexpr SegWork Exp2Work(SegLog y) { + return ConvertFixed(Exp2Pos(y)); +} + +constexpr SegRatio Exp2Ratio(SegLog y) { + return fixed_math::Exp2To(y); +} + +constexpr SegLog MulLogInt(SegLog x, int n) { + return fixed_math::ScaleLogByInt(x, n); +} + +constexpr SegLog DivLogInt(SegLog x, int n) { + return fixed_math::DivLogByInt(x, n); +} + +constexpr SegLog AbsLog(SegLog x) { + if (x.RawValue() < static_cast(0)) { + return SubTo(SegLog::FromRuntimeInteger(0), x); } - double lo = 1.0 + 1.0e-18; - double hi = 2.0; - for (int i = 0; i < 40 && GeomSum(hi, n) < sum; ++i) { - hi *= 2.0; + return x; +} + +consteval SegLog Log2RFromEndpoints(Rat begin, Rat end, int intervals) { + if (intervals <= 0) { + SegmentedSpecError(); + return SegLog::FromRuntimeInteger(0); } - for (int i = 0; i < 80; ++i) { - double const mid = 0.5 * (lo + hi); - if (GeomSum(mid, n) < sum) { - lo = mid; - } else { - hi = mid; + SegLog const span = SubTo(Log2OfRat(end), Log2OfRat(begin)); + return DivLogInt(span, intervals); +} + +consteval SegRatio ExpRatioOf(Rat begin, Rat end, int intervals) { + return Exp2Ratio(Log2RFromEndpoints(begin, end, intervals)); +} + +constexpr bool LogMulSaturates(SegLog x, int n) { + if (n <= 1) { + return false; + } + auto const raw = x.RawValue(); + if (raw <= static_cast(0)) { + return false; + } + auto const maxr = static_cast(SegLog::kRawMax); + auto const nu = static_cast(n); + return static_cast(raw) > maxr / nu; +} + +constexpr SegPosWork Exp2PosMinusOne(SegLog y) { + SegPosWork const e = Exp2Pos(y); + SegPosWork const one = SegPosWork::FromRuntimeInteger(1); + if (e.RawValue() > one.RawValue()) { + return SubTo(e, one); + } + return SegPosWork::FromRuntimeInteger(0); +} + +constexpr SegWork Exp2MinusOne(SegLog y) { + return ConvertFixed(Exp2PosMinusOne(y)); +} + +// (q^k - 1) / (q^n - 1) as a SegWork in [0, 1]. Avoids dividing by (q - 1). +constexpr SegPosWork GeomUnitWeightPos(SegLog log2_q, int k, int n) { + if (n <= 0 || k <= 0) { + return SegPosWork::FromRuntimeInteger(0); + } + if (k >= n) { + return SegPosWork::FromRuntimeInteger(1); + } + if (log2_q.RawValue() <= static_cast(0) || + LogMulSaturates(log2_q, n)) { + return SegPosWork::FromRatio(k, n); + } + SegPosWork const den = Exp2PosMinusOne(MulLogInt(log2_q, n)); + if (den.RawValue() == static_cast(0)) { + return SegPosWork::FromRatio(k, n); + } + SegPosWork const num = Exp2PosMinusOne(MulLogInt(log2_q, k)); + if (num.RawValue() == static_cast(0)) { + return SegPosWork::FromRuntimeInteger(0); + } + return DivTo(num, den); +} + +constexpr SegWork GeomUnitWeight(SegLog log2_q, int k, int n) { + return ConvertFixed(GeomUnitWeightPos(log2_q, k, n)); +} + +constexpr SegWork GeomInterp(SegWork begin, SegWork end, SegLog log2_q, int i, + int n, bool from_upper) { + if (n <= 0 || i <= 0) { + return begin; + } + if (i >= n) { + return end; + } + SegWork const span = SubTo(end, begin); + int const k = from_upper ? (n - i) : i; + SegWork const w = GeomUnitWeight(log2_q, k, n); + SegWork const offset = MulWorkSame(span, w); + if (from_upper) { + return SubTo(end, offset); + } + return AddTo(begin, offset); +} + +constexpr SegWork GeomSumFromLog(SegLog log2_q, int n) { + if (n <= 0) { + return WorkZero(); + } + if (log2_q.RawValue() <= static_cast(0)) { + return SegWork::FromRuntimeInteger(n); + } + if (LogMulSaturates(log2_q, n)) { + return SegWork::FromRaw(SegWork::kRawMax); + } + SegWork const den = Exp2MinusOne(log2_q); + if (!WorkPositive(den)) { + return SegWork::FromRuntimeInteger(n); + } + SegWork const num = Exp2MinusOne(MulLogInt(log2_q, n)); + if (!WorkPositive(num)) { + return WorkZero(); + } + return DivTo(num, den); +} + +consteval SegWork GeomSumByTerms(SegRatio q, int n) { + if (n <= 0) { + return WorkZero(); + } + SegWork acc = WorkZero(); + SegWork term = WorkOne(); + for (int i = 0; i < n; ++i) { + SegWork const next = AddTo(acc, term); + if (next.RawValue() < acc.RawValue() && WorkPositive(term)) { + return SegWork::FromRaw(SegWork::kRawMax); + } + acc = next; + if (i + 1 == n) { + break; + } + term = ScaleWorkByRatio(term, q); + if (term.RawValue() == static_cast(0)) { + break; } } - return 0.5 * (lo + hi); + return acc; } -consteval double ExpRatio(double begin, double end, int intervals) { - if (begin <= 0.0 || end <= 0.0 || intervals <= 0) { +consteval SegWork GeomSumQ(SegRatio q, int n) { + return GeomSumFromLog(Log2Ratio(q), n); +} + +consteval SegRatio RatioFromRawU32(std::uint32_t raw) { + if (raw > static_cast(SegRatio::kRawMax)) { + raw = static_cast(SegRatio::kRawMax); + } + return SegRatio::FromRaw( + fixed_point_internal::RepFromRawValue( + static_cast(raw))); +} + +consteval SegRatio SolveQForGeomSum(int n, SegWork sum) { + if (n <= 0 || !WorkPositive(sum)) { SegmentedSpecError(); - return 1.0; + return RatioOne(); + } + SegWork const n_as_work = SegWork::FromRuntimeInteger(n); + if (!(n_as_work < sum)) { + return RatioOne(); + } + SegRatio lo = RatioOne(); + SegRatio hi = SegRatio::FromRatio(3, 2); + for (int i = 0; i < 8 && GeomSumByTerms(hi, n) < sum; ++i) { + std::uint32_t hi2 = 0; + if (!integer_math::MulU32Checked(hi.RawValue(), 2U, hi2)) { + hi = SegRatio::FromRaw(SegRatio::kRawMax); + break; + } + hi = RatioFromRawU32(hi2); + } + for (int i = 0; i < 40; ++i) { + std::uint32_t const lv = lo.RawValue(); + std::uint32_t const hv = hi.RawValue(); + std::uint32_t const mid_raw = lv + (hv - lv) / 2U; + SegRatio const mid = RatioFromRawU32(mid_raw); + if (GeomSumByTerms(mid, n) < sum) { + lo = mid; + } else { + hi = mid; + } } - return gcem::pow(end / begin, 1.0 / static_cast(intervals)); + std::uint32_t const lv = lo.RawValue(); + std::uint32_t const hv = hi.RawValue(); + return RatioFromRawU32(lv + (hv - lv) / 2U); } struct AutoSplitResult { int n1 = 0; int n2 = 0; - double r1 = 1.0; - double r2 = 1.0; + SegLog log2_r1 = LogZero(); + SegLog log2_r2 = LogZero(); + SegRatio r1 = RatioOne(); + SegRatio r2 = RatioOne(); }; -consteval AutoSplitResult AutoSplitTwoExp(double begin, double mid, double end, +struct JumpScore { + std::uint32_t abs_d = 0; + std::uint32_t min_s = 1; +}; + +consteval JumpScore MakeJumpScoreRatio(SegRatio step_before, + SegRatio step_after) { + std::uint32_t const a = step_before.RawValue(); + std::uint32_t const b = step_after.RawValue(); + JumpScore s{}; + s.abs_d = a > b ? a - b : b - a; + s.min_s = (a < b ? a : b); + if (s.min_s == 0) { + s.min_s = 1; + } + return s; +} + +consteval bool JumpLess(JumpScore a, JumpScore b) { + return integer_math::CmpMulU32(a.abs_d, b.min_s, b.abs_d, a.min_s) < 0; +} + +consteval bool BetterAutoSplit(bool have, JumpScore jump, JumpScore best_jump, + SegRatio err, SegRatio best_err, int n1, + int best_n1) { + if (!have) { + return true; + } + if (JumpLess(jump, best_jump)) { + return true; + } + if (JumpLess(best_jump, jump)) { + return false; + } + if (err < best_err) { + return true; + } + if (best_err < err) { + return false; + } + return n1 < best_n1; +} + +consteval AutoSplitResult AutoSplitTwoExp(Rat begin, Rat mid, Rat end, int total_intervals) { AutoSplitResult best{}; bool have = false; - double best_jump = 0.0; - double best_err = 0.0; + JumpScore best_jump{}; + SegRatio best_err = RatioOne(); + SegLog const log_b = Log2OfRat(begin); + SegLog const log_m = Log2OfRat(mid); + SegLog const log_e = Log2OfRat(end); for (int n1 = 1; n1 < total_intervals; ++n1) { int const n2 = total_intervals - n1; - double const r1 = ExpRatio(begin, mid, n1); - double const r2 = ExpRatio(mid, end, n2); - double const step_before = mid * (1.0 - 1.0 / r1); - double const step_after = mid * (r2 - 1.0); - double const smaller = - step_before < step_after ? step_before : step_after; - if (smaller <= 0.0) { + SegLog const log_r1 = DivLogInt(SubTo(log_m, log_b), n1); + SegLog const log_r2 = DivLogInt(SubTo(log_e, log_m), n2); + SegRatio const r1 = Exp2Ratio(log_r1); + SegRatio const r2 = Exp2Ratio(log_r2); + if (!(RatioOne() < r1) || !(RatioOne() < r2)) { continue; } - double const jump = gcem::abs(step_after - step_before) / smaller; - double const err1 = gcem::sqrt(r1) - 1.0; - double const err2 = gcem::sqrt(r2) - 1.0; - double const max_err = err1 > err2 ? err1 : err2; - bool const better = !have || jump < best_jump - 1.0e-18 || - (gcem::abs(jump - best_jump) <= 1.0e-18 && - (max_err < best_err - 1.0e-18 || - (gcem::abs(max_err - best_err) <= 1.0e-18 && - n1 < best.n1))); - if (better) { + SegRatio const step_before = + DivTo(SubTo(r1, RatioOne()), r1); + SegRatio const step_after = SubTo(r2, RatioOne()); + if (step_before.RawValue() == 0 || step_after.RawValue() == 0) { + continue; + } + JumpScore const jump = MakeJumpScoreRatio(step_before, step_after); + SegRatio const err1 = SubTo(r1, RatioOne()); + SegRatio const err2 = SubTo(r2, RatioOne()); + SegRatio const max_err = err1 > err2 ? err1 : err2; + if (BetterAutoSplit(have, jump, best_jump, max_err, best_err, n1, + best.n1)) { have = true; best_jump = jump; best_err = max_err; best.n1 = n1; best.n2 = n2; - best.r1 = r1; - best.r2 = r2; + best.log2_r1 = log_r1; + best.log2_r2 = log_r2; + best.r1 = Exp2Ratio(log_r1); + best.r2 = Exp2Ratio(log_r2); } } if (!have) { @@ -171,55 +637,94 @@ struct ContExpResult { int intervals = 0; int last_1 = 0; int last_2 = 0; - double r = 1.0; + SegLog log2_r = LogZero(); + SegRatio r = RatioOne(); }; -consteval double LogErr(double got, double want) { - return gcem::abs(gcem::log(got / want)); -} - -consteval int RoundNearestNonneg(double x) { - if (x < 0.0) { +consteval int RoundNearestNonnegLog(SegLog x) { + if (x.RawValue() < static_cast(0)) { return 0; } - return static_cast(x + 0.5); + auto const one = SegLog::FromRuntimeInteger(1).RawValue(); + if (one == 0) { + return 0; + } + auto const q = fixed_point_internal::RoundDivNearest(x.RawValue(), one); + if (q < 0) { + return 0; + } + return static_cast(q); } -// Fill the 1-byte tier (last code = last_1_max, typically 254), then choose -// total intervals so nearest-code region boundaries sit as close as possible -// to the requested physical cuts. -consteval ContExpResult OptimizeContinuousExp(double vmin, double vmax, - double cut1, double cut2, - int last_1_max, int min_n, - int max_n, int max_last_2) { +consteval ContExpResult OptimizeContinuousExp(Rat vmin, Rat vmax, Rat cut1, + Rat cut2, int last_1_max, + int min_n, int max_n, + int max_last_2) { ContExpResult best{}; bool have = false; - double best_cut = 0.0; - double best_rel = 0.0; + SegLog best_cut = SegLog::FromRaw(SegLog::kRawMax); + SegLog best_rel = SegLog::FromRaw(SegLog::kRawMax); int const i1 = last_1_max; + SegLog const log_vmin = Log2OfRat(vmin); + SegLog const log_vmax = Log2OfRat(vmax); + SegLog const log_cut1 = Log2OfRat(cut1); + SegLog const log_cut2 = Log2OfRat(cut2); + SegLog const span_log = SubTo(log_vmax, log_vmin); + auto const span_raw = span_log.RawValue(); for (int n = min_n; n <= max_n; ++n) { - if (n <= i1 + 1) { + if (n <= i1 + 1 || span_raw == 0) { + continue; + } + SegLog const log2_r = DivLogInt(span_log, n); + SegLog const half = DivLogInt(log2_r, 2); + auto const dcut_raw = + SubTo(log_cut2, log_vmin).RawValue(); + std::uint32_t const n_u = static_cast(n); + std::uint32_t const dcut_u = integer_math::AbsI32ToU32(dcut_raw); + std::uint32_t const span_u = integer_math::AbsI32ToU32(span_raw); + if (span_u == 0U || span_u > (std::numeric_limits::max() >> 1U)) { continue; } - double const r = ExpRatio(vmin, vmax, n); - double const span_log = gcem::log(vmax / vmin); - double const i2f = static_cast(n) * gcem::log(cut2 / vmin) / - span_log - - 0.5; - int const i2 = RoundNearestNonneg(i2f); + std::uint32_t hi = 0; + std::uint32_t lo = 0; + integer_math::MulU32Wide(n_u, dcut_u, hi, lo); + if (!integer_math::ShlU32WideChecked(hi, lo, 1U)) { + continue; + } + if (lo < span_u) { + if (hi == 0U) { + continue; + } + --hi; + lo -= span_u; + } else { + lo -= span_u; + } + std::uint32_t q = 0; + std::uint32_t r = 0; + std::uint32_t const den = span_u << 1U; + if (!integer_math::DivU32Wide(hi, lo, den, q, r)) { + continue; + } + if (r >= den - r && q != std::numeric_limits::max()) { + ++q; + } + int const i2 = static_cast(q); if (i2 <= i1 || i2 >= n || i2 > max_last_2) { continue; } - double const b1 = vmin * gcem::pow(r, static_cast(i1) + 0.5); - double const b2 = vmin * gcem::pow(r, static_cast(i2) + 0.5); - double const cut = - LogErr(b1, cut1) > LogErr(b2, cut2) ? LogErr(b1, cut1) : LogErr(b2, cut2); - double const rel = gcem::sqrt(r) - 1.0; + SegLog const b1_log = + AddTo(log_vmin, AddTo(MulLogInt(log2_r, i1), half)); + SegLog const b2_log = + AddTo(log_vmin, AddTo(MulLogInt(log2_r, i2), half)); + SegLog const e1 = AbsLog(SubTo(b1_log, log_cut1)); + SegLog const e2 = AbsLog(SubTo(b2_log, log_cut2)); + SegLog const cut = e1 > e2 ? e1 : e2; + SegLog const rel = log2_r; bool const better = - !have || cut < best_cut - 1.0e-18 || - (gcem::abs(cut - best_cut) <= 1.0e-18 && - (rel < best_rel - 1.0e-18 || - (gcem::abs(rel - best_rel) <= 1.0e-18 && n < best.intervals))); + !have || cut < best_cut || + (cut == best_cut && + (rel < best_rel || (rel == best_rel && n < best.intervals))); if (better) { have = true; best_cut = cut; @@ -227,57 +732,105 @@ consteval ContExpResult OptimizeContinuousExp(double vmin, double vmax, best.intervals = n; best.last_1 = i1; best.last_2 = i2; - best.r = r; + best.log2_r = log2_r; } } if (!have) { SegmentedSpecError(); } + best.r = Exp2Ratio(best.log2_r); return best; } -consteval std::uint64_t TwoTierMaxU8(std::uint32_t b0) { - return (255ULL - b0 - 1ULL) * 256ULL + b0 + 1ULL + 255ULL; +consteval std::uint32_t TwoTierMaxU8(std::uint32_t b0) { + return (255U - b0 - 1U) * 256U + b0 + 1U + 255U; } -consteval std::uint64_t ThreeTierMaxU8(std::uint32_t b0, std::uint32_t b1) { - std::uint64_t const two = TwoTierMaxU8(b0); - constexpr std::uint64_t kWord2 = 65536ULL; - return (two - b1 - 1ULL) * kWord2 + b1 + 1ULL + (kWord2 - 1ULL); -} - -consteval int CeilPositive(double x) { - int const i = static_cast(x); - if (static_cast(i) < x) { - return i + 1; +consteval std::uint32_t ThreeTierMaxU8(std::uint32_t b0, std::uint32_t b1) { + std::uint32_t const two = TwoTierMaxU8(b0); + std::uint32_t const n = (two > b1 + 1U) ? (two - b1 - 1U) : 0U; + if (n > (std::numeric_limits::max() >> 16U)) { + return std::numeric_limits::max(); + } + std::uint32_t acc = n << 16U; + std::uint32_t out = 0; + if (!integer_math::AddU32Checked(acc, b1 + 1U + 65535U, out)) { + return std::numeric_limits::max(); } - return i; + return out; } -consteval int MinRampIntervals(double span, double step0, double max_err) { - if (span <= 0.0 || step0 <= 0.0 || max_err <= 0.0) { +consteval int MinRampIntervals(SegWork span, SegWork step0, SegWork max_err) { + if (!WorkPositive(span) || !WorkPositive(step0) || + !WorkPositive(max_err)) { SegmentedSpecError(); return 1; } - double const last = 2.0 * max_err; - double const den = step0 + last; - if (den <= 0.0) { + std::uint32_t const span_u = + integer_math::AbsI32ToU32(span.RawValue()); + std::uint32_t const step0_u = + integer_math::AbsI32ToU32(step0.RawValue()); + std::uint32_t const err_u = + integer_math::AbsI32ToU32(max_err.RawValue()); + std::uint32_t last_lim = 0; + if (!integer_math::AddU32Checked(err_u, err_u, last_lim)) { SegmentedSpecError(); return 1; } - int n = CeilPositive(2.0 * span / den); - if (n < 1) { - n = 1; + std::uint32_t den = 0; + if (!integer_math::AddU32Checked(step0_u, last_lim, den) || den == 0U) { + SegmentedSpecError(); + return 1; + } + std::uint32_t two_span = 0; + if (!integer_math::AddU32Checked(span_u, span_u, two_span)) { + SegmentedSpecError(); + return 1; + } + std::uint32_t n_u = two_span / den; + if (two_span % den != 0U) { + ++n_u; + } + int n = n_u < 1U ? 1 : static_cast(n_u); + for (int guard = 0; guard < 4096; ++guard) { + std::uint32_t mean = 0; + if (!integer_math::RoundDivU32(two_span, static_cast(n), + mean)) { + break; + } + std::int32_t const last_step = + static_cast(mean) - static_cast(step0_u); + if (last_step > static_cast(last_lim)) { + ++n; + continue; + } + if (last_step > 0) { + break; + } + if (n <= 1) { + break; + } + --n; } return n; } +// Schema-hash metadata only. kSchemaHash is computed at compile time and is +// not part of the SegmentedNumber encode/decode mathematical path. The FNV-1a +// mix uses a 64-bit state so existing golden hashes stay bit-identical; this +// is not runtime arithmetic. consteval std::uint64_t MixHash(std::uint64_t h, std::uint64_t v) { h ^= v; h *= 1099511628211ULL; return h; } +consteval std::uint64_t MixRat(std::uint64_t h, Rat r) { + h = MixHash(h, static_cast(r.num)); + h = MixHash(h, static_cast(r.den)); + return h; +} + } // namespace ae::seg::segmented_math_internal #endif // AE_NUMERIC_DETAILS_SEGMENTED_MATH_H_ diff --git a/ae-numeric/fixed_math.h b/ae-numeric/fixed_math.h index 914530d..93f84ba 100644 --- a/ae-numeric/fixed_math.h +++ b/ae-numeric/fixed_math.h @@ -22,8 +22,7 @@ #include #include #include - -#include +#include #include "ae-numeric/fixed_point.h" #include "ae-numeric/integer_math.h" @@ -34,13 +33,30 @@ namespace ae::fixed_math { inline constexpr int kDefaultLogIterations = 11; struct DefaultFixedMathPolicy { - using log_type = FixedPoint; - using mant_type = FixedPoint; - using mul_intermediate_type = std::int64_t; + using log_type = FixedPoint; + using mant_type = FixedPoint; static constexpr int kLogIterations = kDefaultLogIterations; static constexpr int kExp2FractionBits = kDefaultLogIterations; }; +// Higher-resolution policy for values near 1 and for SegmentedNumber. +// log_type Max is large enough for log2 of physical quantities (RX window, +// CO2) but too small to hold interval counts; multiply/divide logs by +// integer n with ScaleLogByInt / DivLogByInt. +struct HighPrecisionFixedMathPolicy { + using log_type = FixedPoint; + using mant_type = FixedPoint; + static constexpr int kLogIterations = 26; + static constexpr int kExp2FractionBits = 26; +}; + +// SegmentedNumber production math: FixedPoint Rep width <= 32 bits, no +// 64-bit runtime arithmetic in Log2/Exp2 instantiations. +struct Segmented32MathPolicy : HighPrecisionFixedMathPolicy { + static_assert(sizeof(typename log_type::rep_value_type) <= 4); + static_assert(sizeof(typename mant_type::rep_value_type) <= 4); +}; + namespace internal { template @@ -61,19 +77,19 @@ constexpr int scale_of() { template requires is_fixed_point_v && is_fixed_point_v constexpr Target cast_fixed(Source x) { - const std::int64_t src_raw = static_cast(x.RawValue()); - const std::int64_t aligned = fixed_point_internal::ConvertRawScale( - src_raw, Source::kScaleExp, Target::kScaleExp, - static_cast(Target::kRawMin), - static_cast(Target::kRawMax)); + using ToR = typename Target::rep_value_type; + ToR const aligned = fixed_point_internal::ConvertRawScaleTo( + x.RawValue(), Source::kScaleExp, Target::kScaleExp, Target::kRawMin, + Target::kRawMax); return Target::FromRaw( fixed_point_internal::RepFromRawValue( - static_cast(aligned))); + aligned)); } template consteval Log InvPow2LogEntry() { - return Log::FromDouble(1.0 / static_cast(std::uint64_t{1} << I)); + static_assert(I >= 1 && I < 31); + return Log::FromRatio(1, static_cast(1) << I); } template @@ -83,63 +99,106 @@ consteval std::array MakeInvPow2Table( } template -constexpr Log InvPow2Log(int i) { - static_assert(Iterations >= 1); - constexpr auto kTable = +struct InvPow2TableHolder { + static constexpr auto kTable = MakeInvPow2Table(std::make_index_sequence{}); - return kTable[static_cast(i - 1)]; +}; + +template +constexpr Log InvPow2Log(int i) { + static_assert(Iterations >= 1 && Iterations < 31); + return InvPow2TableHolder::kTable[static_cast( + i - 1)]; } -template -consteval Mant Exp2FactorEntry() { - return Mant::FromDouble( - gcem::pow(2.0, 1.0 / static_cast(std::uint64_t{1} << I))); +template + requires is_fixed_point_v +constexpr T SqrtNewton(T x) { + if (x.RawValue() == static_cast(0)) { + return x; + } + T y = x; + T const one = T::FromRuntimeInteger(1); + T const two = T::FromRuntimeInteger(2); + if (x < one) { + y = one; + } + for (int i = 0; i < 16; ++i) { + if (y.RawValue() == static_cast(0)) { + break; + } + T const q = DivTo(x, y); + y = DivTo(AddTo(y, q), two); + } + return y; } template -consteval std::array MakeExp2FactorTable( - std::index_sequence) { - return {Exp2FactorEntry(Is + 1)>()...}; +constexpr std::array(Iterations)> +MakeExp2MantTable(std::index_sequence) { + static_assert(Iterations >= 1 && Iterations < 31); + std::array(Iterations)> t{ + ((void)Is, Mant::FromRuntimeInteger(0))...}; + Mant v = Mant::FromRuntimeInteger(2); + for (int i = 0; i < Iterations; ++i) { + v = SqrtNewton(v); + t[static_cast(i)] = v; + } + return t; } +template +struct Exp2MantTableHolder { + static constexpr auto kTable = MakeExp2MantTable( + std::make_index_sequence(Iterations)>{}); +}; + template constexpr Mant Exp2Factor(int i) { - static_assert(Iterations >= 1); - constexpr auto kTable = MakeExp2FactorTable( - std::make_index_sequence{}); - return kTable[static_cast(i - 1)]; + static_assert(Iterations >= 1 && Iterations < 31); + return Exp2MantTableHolder::kTable[static_cast( + i - 1)]; } template -constexpr std::int64_t FloorLogical(Log y) { - std::int64_t num = static_cast(y.RawValue()); - std::int64_t den = 1; - const int scale = Log::kScaleExp; - if constexpr (scale > 0) { +constexpr int FloorLogical32(Log y) { + static_assert(sizeof(typename Log::rep_value_type) <= 4); + auto const raw = static_cast(y.RawValue()); + int const scale = Log::kScaleExp; + if (scale >= 0) { + std::int32_t v = raw; for (int i = 0; i < scale; ++i) { - num *= 2; - } - } else if constexpr (scale < 0) { - for (int i = 0; i < -scale; ++i) { - den *= 2; + if (v > std::numeric_limits::max() / 2) { + return std::numeric_limits::max() / 4; + } + if (v < std::numeric_limits::min() / 2) { + return std::numeric_limits::min() / 4; + } + v *= 2; } + return static_cast(v); + } + unsigned const bits = static_cast(-scale); + if (bits >= 31U) { + return raw < 0 ? -1 : 0; } - if (num >= 0) { - return num / den; + auto const div = static_cast(1U << bits); + if (raw >= 0) { + return static_cast(raw / div); } - const std::int64_t q = num / den; - if (num % den == 0) { - return q; + std::int32_t const q = raw / div; + if ((raw % div) == 0) { + return static_cast(q); } - return q - 1; + return static_cast(q - 1); } template constexpr Mant NormalizeMantissa(X x, int& exponent_out) { int e = 0; X m = x; - const X one_x = X{1}; - const X two_x = X{2}; + const X one_x = X::FromRuntimeInteger(1); + const X two_x = X::FromRuntimeInteger(2); for (int guard = 0; guard < 64 && m >= two_x; ++guard) { m = DivTo(m, two_x); @@ -149,7 +208,7 @@ constexpr Mant NormalizeMantissa(X x, int& exponent_out) { guard < 64 && m.RawValue() != static_cast(0) && m < one_x; ++guard) { - m = MulTo(m, two_x); + m = AddTo(m, m); --e; } @@ -159,84 +218,17 @@ constexpr Mant NormalizeMantissa(X x, int& exponent_out) { template requires is_fixed_point_v -constexpr std::int64_t raw_one() { - return static_cast(Target::FromRuntimeInteger(1).RawValue()); -} - -template - requires is_fixed_point_v && is_fixed_point_v -constexpr std::int64_t align_raw_to_target(Source x) { - return fixed_point_internal::ConvertRawScale( - static_cast(x.RawValue()), Source::kScaleExp, - Target::kScaleExp, static_cast(Target::kRawMin), - static_cast(Target::kRawMax)); -} - -template - requires is_fixed_point_v -constexpr std::int64_t mul_raw_fixed(std::int64_t acc, std::int64_t factor) { - using Intermediate = typename Policy::mul_intermediate_type; - static_assert(sizeof(Intermediate) <= sizeof(std::int64_t), - "ae-numeric: mul_intermediate_type must be at most 64 bits"); - static_assert( - sizeof(typename Target::rep_value_type) <= 4, - "ae-numeric: fixed_math::Exp2/Mul path requires Target rep width " - "<= 32 bits; wider reps need an overflow-safe custom policy"); - - std::int64_t const one = raw_one(); - if (one == 0) { - return 0; - } - - bool const negative = (acc < 0) ^ (factor < 0) ^ (one < 0); - std::uint64_t const a = integer_math::AbsI64ToU64(acc); - std::uint64_t const f = integer_math::AbsI64ToU64(factor); - std::uint64_t const d = integer_math::AbsI64ToU64(one); - - std::uint64_t magnitude = 0; - if (!integer_math::MulDivU64Nearest(a, f, d, magnitude)) { - return negative ? static_cast(Target::kRawMin) - : static_cast(Target::kRawMax); - } - - if (!negative) { - if (magnitude > - static_cast(std::numeric_limits::max())) { - return static_cast(Target::kRawMax); - } - return static_cast(magnitude); - } - - if (magnitude > - static_cast(std::numeric_limits::max()) + - 1U) { - return static_cast(Target::kRawMin); - } - if (magnitude == - static_cast(std::numeric_limits::max()) + - 1U) { - return std::numeric_limits::min(); - } - return -static_cast(magnitude); -} - -template - requires is_fixed_point_v -constexpr std::int64_t apply_pow2_int_raw(std::int64_t acc, - std::int64_t int_part) { +constexpr Target apply_pow2_int(Target acc, int int_part) { + Target const two = Target::FromRuntimeInteger(2); if (int_part > 0) { - for (std::int64_t i = 0; i < int_part && i < 62; ++i) { - const std::int64_t shifted = acc * 2; - if (shifted > static_cast(Target::kRawMax)) { - return static_cast(Target::kRawMax); - } - acc = shifted; + for (int i = 0; i < int_part && i < 31; ++i) { + acc = AddTo(acc, acc); } return acc; } if (int_part < 0) { - for (std::int64_t i = 0; i < -int_part && i < 62; ++i) { - acc = fixed_point_internal::RoundDivNearest(acc, std::int64_t{2}); + for (int i = 0; i < -int_part && i < 31; ++i) { + acc = DivTo(acc, two); } } return acc; @@ -244,20 +236,32 @@ constexpr std::int64_t apply_pow2_int_raw(std::int64_t acc, template requires is_fixed_point_v -constexpr Target from_clamped_raw(std::int64_t raw) { - const std::int64_t min_raw = static_cast(Target::kRawMin); - const std::int64_t max_raw = static_cast(Target::kRawMax); - - if (raw < min_raw) { - raw = min_raw; - } - if (raw > max_raw) { - raw = max_raw; +constexpr Target from_clamped_i32(std::int32_t raw) { + using RV = typename Target::rep_value_type; + if constexpr (std::is_signed_v) { + auto const min_raw = static_cast(Target::kRawMin); + auto const max_raw = static_cast(Target::kRawMax); + if (raw < min_raw) { + raw = min_raw; + } + if (raw > max_raw) { + raw = max_raw; + } + return Target::FromRaw( + fixed_point_internal::RepFromRawValue( + static_cast(raw))); + } else { + if (raw < 0) { + return Target::FromRaw(Target::kRawMin); + } + auto const u = static_cast(raw); + if (u > static_cast(Target::kRawMax)) { + return Target::FromRaw(Target::kRawMax); + } + return Target::FromRaw( + fixed_point_internal::RepFromRawValue( + static_cast(u))); } - - return Target::FromRaw( - fixed_point_internal::RepFromRawValue( - static_cast(raw))); } // Precondition: x > 0. @@ -268,14 +272,14 @@ constexpr Target Log2To(X x) { assert(x.RawValue() != typename X::rep_value_type{0}); } - using Mant = typename Policy::mant_type; + using Mant = FixedPoint; using Log = Target; int exponent = 0; Mant mantissa = NormalizeMantissa(x, exponent); Log result = Log::FromRuntimeInteger(exponent); - const Mant two_m = Mant{2}; + const Mant two_m = Mant::FromRuntimeInteger(2); for (int i = 1; i <= Policy::kLogIterations; ++i) { mantissa = MulTo(mantissa, mantissa); @@ -291,33 +295,85 @@ constexpr Target Log2To(X x) { template requires internal::is_fixed_point_v && internal::is_fixed_point_v constexpr Target Exp2To(X y) { + static_assert(sizeof(typename Target::rep_value_type) <= 4); using Log = typename Policy::log_type; using Mant = typename Policy::mant_type; + static_assert(sizeof(typename Log::rep_value_type) <= 4); + static_assert(sizeof(typename Mant::rep_value_type) <= 4); const Log ly = cast_fixed(y); - const std::int64_t int_part = FloorLogical(ly); + const int int_part = FloorLogical32(ly); Log frac = SubTo(ly, Log::FromRuntimeInteger(int_part)); - std::int64_t acc = raw_one(); + Target acc = Target::FromRuntimeInteger(1); for (int i = 1; i <= Policy::kExp2FractionBits; ++i) { const Log bit = InvPow2Log(i); if (bit.RawValue() == typename Log::rep_value_type{0}) { break; } if (frac >= bit) { - const std::int64_t factor = align_raw_to_target( - Exp2Factor(i)); - acc = mul_raw_fixed(acc, factor); + acc = MulTo(acc, cast_fixed( + Exp2Factor(i))); frac = SubTo(frac, bit); } } - acc = apply_pow2_int_raw(acc, int_part); - return from_clamped_raw(acc); + return apply_pow2_int(acc, int_part); } } // namespace internal +template + requires internal::is_fixed_point_v +constexpr Log ScaleLogByInt(Log x, int n) { + static_assert(sizeof(typename Log::rep_value_type) <= 4); + if (n == 0 || x.RawValue() == static_cast(0)) { + return internal::from_clamped_i32(0); + } + bool const negative = + (std::is_signed_v && + x.RawValue() < static_cast(0)) != + (n < 0); + std::uint32_t a = 0; + if constexpr (std::is_signed_v) { + a = integer_math::AbsI32ToU32(static_cast(x.RawValue())); + } else { + a = static_cast(x.RawValue()); + } + std::uint32_t const nu = + n < 0 ? integer_math::AbsI32ToU32(static_cast(n)) + : static_cast(n); + std::uint32_t hi = 0; + std::uint32_t lo = 0; + integer_math::MulU32Wide(a, nu, hi, lo); + if (hi != 0U) { + return negative ? Log::FromRaw(Log::kRawMin) : Log::FromRaw(Log::kRawMax); + } + if (lo > static_cast(std::numeric_limits::max())) { + return negative ? Log::FromRaw(Log::kRawMin) : Log::FromRaw(Log::kRawMax); + } + std::int32_t out = static_cast(lo); + if (negative) { + out = -out; + } + return internal::from_clamped_i32(out); +} + +template + requires internal::is_fixed_point_v +constexpr Log DivLogByInt(Log x, int n) { + static_assert(sizeof(typename Log::rep_value_type) <= 4); + if (n == 0) { + return x.RawValue() >= static_cast(0) + ? Log::FromRaw(Log::kRawMax) + : Log::FromRaw(Log::kRawMin); + } + auto const num = static_cast(x.RawValue()); + auto const den = static_cast(n); + return internal::from_clamped_i32( + fixed_point_internal::RoundDivNearest(num, den)); +} + template requires internal::is_fixed_point_v && internal::is_fixed_point_v constexpr Target Log2To(X x) { @@ -342,6 +398,123 @@ constexpr Target Exp2To(X y) { return internal::Exp2To(y); } +template + requires internal::is_fixed_point_v +constexpr T Sqrt(T x) { + return internal::SqrtNewton(x); +} + +template + requires internal::is_fixed_point_v +constexpr T Log2(T x) { + return Log2To(x); +} + +template + requires internal::is_fixed_point_v +constexpr T Exp2(T y) { + return Exp2To(y); +} + +template + requires internal::is_fixed_point_v && + internal::is_fixed_point_v +constexpr Target PowIntTo(Base base, int n) { + if (n == 0) { + return Target::FromRuntimeInteger(1); + } + using Log = typename Policy::log_type; + Log const lb = Log2To(base); + Log const nlog = ScaleLogByInt(lb, n); + return Exp2To(nlog); +} + +template + requires internal::is_fixed_point_v +constexpr T PowInt(T base, int n) { + return PowIntTo(base, n); +} + +template + requires internal::is_fixed_point_v && + internal::is_fixed_point_v && + internal::is_fixed_point_v +constexpr Target PowTo(Base base, Exp exponent) { + using Log = typename Policy::log_type; + static_assert(sizeof(typename Log::rep_value_type) <= 4); + Log const lb = Log2To(base); + auto const exp_one = static_cast( + Exp::FromRuntimeInteger(1).RawValue()); + if (exp_one == 0) { + return Target::FromRuntimeInteger(1); + } + bool const lneg = std::is_signed_v && + lb.RawValue() < static_cast(0); + bool const eneg = + std::is_signed_v && + exponent.RawValue() < static_cast(0); + bool const negative = lneg != eneg; + std::uint32_t a = 0; + std::uint32_t f = 0; + std::uint32_t d = 0; + if constexpr (std::is_signed_v) { + a = integer_math::AbsI32ToU32(static_cast(lb.RawValue())); + } else { + a = static_cast(lb.RawValue()); + } + if constexpr (std::is_signed_v) { + f = integer_math::AbsI32ToU32( + static_cast(exponent.RawValue())); + } else { + f = static_cast(exponent.RawValue()); + } + d = integer_math::AbsI32ToU32(exp_one); + std::uint32_t mag = 0; + if (!integer_math::MulDivU32Nearest(a, f, d, mag)) { + mag = static_cast(std::numeric_limits::max()); + } + std::int32_t prod = 0; + if (mag > static_cast(std::numeric_limits::max())) { + prod = negative ? std::numeric_limits::min() + : std::numeric_limits::max(); + } else { + prod = static_cast(mag); + if (negative) { + prod = -prod; + } + } + Log const y = internal::from_clamped_i32(prod); + return Exp2To(y); +} + +template + requires internal::is_fixed_point_v +constexpr T Pow(T base, T exponent) { + return PowTo(base, exponent); +} + +template + requires internal::is_fixed_point_v +constexpr T NthRoot(T x, int n) { + if (n <= 0) { + return T::FromRuntimeInteger(1); + } + using Log = typename Policy::log_type; + Log const lx = Log2To(x); + return Exp2To(DivLogByInt(lx, n)); +} + +template + requires internal::is_fixed_point_v && + internal::is_fixed_point_v && + internal::is_fixed_point_v +constexpr Target LogTo(Base base, X x) { + using Log = typename Policy::log_type; + Log const lx = Log2To(x); + Log const lb = Log2To(base); + return DivTo(lx, lb); +} + } // namespace ae::fixed_math #endif // AE_NUMERIC_FIXED_MATH_H_ diff --git a/ae-numeric/fixed_point.h b/ae-numeric/fixed_point.h index 97b9ed3..0cd5e34 100644 --- a/ae-numeric/fixed_point.h +++ b/ae-numeric/fixed_point.h @@ -26,6 +26,7 @@ #include #include "ae-numeric/decimal.h" +#include "ae-numeric/integer_math.h" #include "ae-numeric/numeric_traits.h" #include "ae-numeric/runtime_numeric_traits.h" @@ -373,6 +374,58 @@ constexpr RepValue ConvertRawScale(RepValue raw, int source_scale_exp, return ClampRaw(raw, raw_min, raw_max); } +template +inline constexpr bool kRepAtMost32 = sizeof(RV) <= 4; + +template +constexpr ToR ConvertRawScaleTo(FromR raw, int source_scale_exp, + int target_scale_exp, ToR raw_min, + ToR raw_max) { + if constexpr (std::is_same_v) { + return ConvertRawScale(raw, source_scale_exp, target_scale_exp, raw_min, + raw_max); + } else if constexpr (kRepAtMost32 && kRepAtMost32) { + if constexpr (std::is_signed_v) { + std::int32_t src = 0; + if constexpr (std::is_signed_v) { + src = static_cast(raw); + } else { + auto const u = static_cast(raw); + constexpr auto kI32Max = static_cast( + std::numeric_limits::max()); + src = u > kI32Max ? std::numeric_limits::max() + : static_cast(u); + } + std::int32_t const aligned = ConvertRawScale( + src, source_scale_exp, target_scale_exp, + static_cast(raw_min), + static_cast(raw_max)); + return static_cast(aligned); + } else { + std::uint32_t src = 0; + if constexpr (std::is_signed_v) { + src = raw < static_cast(0) ? 0U + : static_cast(raw); + } else { + src = static_cast(raw); + } + std::uint32_t const aligned = ConvertRawScale( + src, source_scale_exp, target_scale_exp, + static_cast(raw_min), + static_cast(raw_max)); + if (aligned > static_cast(raw_max)) { + return raw_max; + } + return static_cast(aligned); + } + } else { + const std::int64_t aligned = ConvertRawScale( + static_cast(raw), source_scale_exp, target_scale_exp, + static_cast(raw_min), static_cast(raw_max)); + return static_cast(aligned); + } +} + template constexpr RepValue RoundDivNearest(RepValue num, RepValue den) { if (den == RepValue{0}) { @@ -430,6 +483,132 @@ constexpr RepValue RawFromRatioAtScale(std::int64_t num, std::int64_t den, return static_cast(clamped); } +// 32-bit-only logical→raw conversion for Rep <= 32 bits. Uses MulU32Wide / +// DivU32Wide / ShlU32WideChecked — no int64_t/uint64_t arithmetic types. +template +constexpr RepValue RawFromRatioAtScale32(std::int32_t num, std::int32_t den, + int scale_exp, RepValue raw_min, + RepValue raw_max) { + if (den == 0) { + return raw_min; + } + + bool const negative = (num < 0) != (den < 0); + std::uint32_t un = integer_math::AbsI32ToU32(num); + std::uint32_t ud = integer_math::AbsI32ToU32(den); + + std::uint32_t hi = 0; + std::uint32_t lo = un; + if (scale_exp < 0) { + if (!integer_math::ShlU32WideChecked( + hi, lo, static_cast(-scale_exp))) { + return negative ? raw_min : raw_max; + } + } else if (scale_exp > 0) { + std::uint32_t dhi = 0; + std::uint32_t dlo = ud; + if (!integer_math::ShlU32WideChecked(dhi, dlo, + static_cast(scale_exp))) { + return RepValue{0}; + } + if (dhi != 0U) { + // Divisor >= 2^32 and dividend < 2^32 ⇒ quotient 0 (after rounding: 0). + return RepValue{0}; + } + ud = dlo; + hi = 0; + lo = un; + } + + std::uint32_t q = 0; + std::uint32_t r = 0; + if (!integer_math::DivU32Wide(hi, lo, ud, q, r)) { + return negative ? raw_min : raw_max; + } + if (r >= ud - r) { + if (q == std::numeric_limits::max()) { + return negative ? raw_min : raw_max; + } + ++q; + } + + if (!negative) { + if (q > static_cast(raw_max)) { + return raw_max; + } + return static_cast(q); + } + if constexpr (std::is_signed_v) { + auto const min_mag = + integer_math::AbsI32ToU32(static_cast(raw_min)); + if (q > min_mag) { + return raw_min; + } + return static_cast(-static_cast(q)); + } else { + return raw_min; + } +} + +template +constexpr bool LogicalWithinDeclaredMax32(std::int32_t num, std::int32_t den) { + if (den <= 0) { + return false; + } + constexpr std::int64_t max_num64 = BoundRatio::num; + constexpr std::int64_t max_den64 = BoundRatio::den; + static_assert(max_num64 > 0 && max_den64 > 0); + static_assert(max_num64 <= std::numeric_limits::max()); + static_assert(max_den64 <= std::numeric_limits::max()); + constexpr auto max_num = static_cast(max_num64); + constexpr auto max_den = static_cast(max_den64); + if constexpr (!kIsSigned) { + if (num < 0) { + return false; + } + } + return integer_math::CmpMulU32(integer_math::AbsI32ToU32(num), max_den, + max_num, integer_math::AbsI32ToU32(den)) <= 0; +} + +template +constexpr std::pair ClampLogicalRational32( + std::int32_t num, std::int32_t den) { + if (den <= 0) { + den = 1; + } + if (LogicalWithinDeclaredMax32(num, den)) { + return {num, den}; + } + constexpr auto max_num = static_cast(BoundRatio::num); + constexpr auto max_den = static_cast(BoundRatio::den); + if constexpr (kIsSigned) { + if (num < 0) { + return {-max_num, max_den}; + } + } + return {max_num, max_den}; +} + +template +constexpr typename numeric_traits::rep_value_type +MakeRawFromLogicalRuntime32(std::int32_t num, std::int32_t den) { + using RepValue = typename numeric_traits::rep_value_type; + static_assert(sizeof(RepValue) <= 4, + "MakeRawFromLogicalRuntime32 requires Rep <= 32 bits"); + constexpr RepValue kRawMax = numeric_traits::kRawMax; + constexpr RepValue kRawMin = + kIsSigned ? static_cast(-static_cast( + static_cast(kRawMax))) + : RepValue{0}; + constexpr int kScaleExp = + ComputeScaleExp(static_cast(kRawMax), BoundRatio::num, + BoundRatio::den); + auto const clamped = ClampLogicalRational32(num, den); + return RawFromRatioAtScale32(clamped.first, clamped.second, kScaleExp, + kRawMin, kRawMax); +} + template constexpr typename numeric_traits::rep_value_type MakeRawFromLogical( std::int64_t num, std::int64_t den) { @@ -558,6 +737,8 @@ class FixedPoint { static constexpr int kFractionBits = kScaleExp < 0 ? -kScaleExp : 0; static constexpr int kLeftShift = kScaleExp > 0 ? kScaleExp : 0; + constexpr FixedPoint() = default; + static constexpr fixed_point_internal::Rational kRepresentableMaxRational = fixed_point_internal::ScaleRationalByPow2( {static_cast(kStorageRawMax), 1}, kScaleExp); @@ -585,22 +766,36 @@ class FixedPoint { return FromRaw(raw); } + // For Rep<=32 always use the 32-bit-only logical→raw path so runtime + // encode/decode cannot pull in int64 division helpers (__divdi3 etc.). + // Compile-time values for SegmentedNumber fit int32 (Rat / interval counts). + // Wider-Rep FixedPoint (e.g. Instant) keeps the legacy int64 converter. static constexpr FixedPoint FromRatio(std::int64_t num, std::int64_t den) { - return FixedPoint(fixed_point_internal::logical_storage_t{}, num, den); + if constexpr (sizeof(rep_value_type) <= 4) { + return FixedPoint( + fixed_point_internal::raw_storage_t{}, + fixed_point_internal::RepFromRawValue( + fixed_point_internal::MakeRawFromLogicalRuntime32( + static_cast(num), + static_cast(den)))); + } else { + return FixedPoint(fixed_point_internal::logical_storage_t{}, num, den); + } } static constexpr FixedPoint FromInteger(std::int64_t value) { - return FixedPoint(fixed_point_internal::logical_storage_t{}, value, 1); + return FromRatio(value, static_cast(1)); } // Explicit runtime conversion: clamps to the declared logical range. static constexpr FixedPoint FromRuntimeInteger(std::int64_t value) { - return FixedPoint(fixed_point_internal::logical_storage_t{}, value, 1); + return FromRatio(value, static_cast(1)); } // Explicit saturating runtime conversion: clamps to the declared range. static constexpr FixedPoint Saturating(std::int64_t value) { - return FixedPoint(fixed_point_internal::logical_storage_t{}, value, 1); + return FromRatio(value, static_cast(1)); } // Checked runtime conversion: nullopt when value exceeds the declared range. @@ -610,7 +805,7 @@ class FixedPoint { 1)) { return std::nullopt; } - return FixedPoint(fixed_point_internal::logical_storage_t{}, value, 1); + return FromRatio(value, static_cast(1)); } static consteval FixedPoint FromDouble(double value) { @@ -651,7 +846,10 @@ class FixedPoint { static constexpr To Cast(const FixedPoint& value) { return To::FromRaw( fixed_point_internal::RepFromRawValue( - To::AlignRawFromScale(value.RawValue(), kScaleExp))); + fixed_point_internal::ConvertRawScaleTo< + typename To::rep_value_type>(value.RawValue(), kScaleExp, + To::kScaleExp, To::kRawMin, + To::kRawMax))); } constexpr Rep Raw() const { @@ -702,48 +900,213 @@ class FixedPoint { namespace fixed_point_internal { +template +constexpr RV SaturatingAdd32(RV a, RV b, RV raw_min, RV raw_max) { + if constexpr (std::is_signed_v) { + auto const ua = static_cast(static_cast(a)); + auto const ub = static_cast(static_cast(b)); + auto const us = ua + ub; + auto const sum = static_cast(us); + bool const a_pos = a > RV{0}; + bool const b_pos = b > RV{0}; + bool const a_neg = a < RV{0}; + bool const b_neg = b < RV{0}; + if (a_pos && b_pos && sum < 0) { + return raw_max; + } + if (a_neg && b_neg && sum >= 0) { + return raw_min; + } + return ClampRaw(static_cast(sum), raw_min, raw_max); + } else { + auto const ua = static_cast(a); + auto const ub = static_cast(b); + std::uint32_t s = 0; + if (!integer_math::AddU32Checked(ua, ub, s)) { + return raw_max; + } + if (s > static_cast(raw_max)) { + return raw_max; + } + return static_cast(s); + } +} + +template +constexpr RV SaturatingSub32(RV a, RV b, RV raw_min, RV raw_max) { + if constexpr (std::is_signed_v) { + auto const ua = static_cast(static_cast(a)); + auto const ub = static_cast(static_cast(b)); + auto const ud = ua - ub; + auto const diff = static_cast(ud); + bool const a_pos = a >= RV{0}; + bool const b_neg = b < RV{0}; + bool const a_neg = a < RV{0}; + bool const b_pos = b > RV{0}; + if (a_pos && b_neg && diff < 0) { + return raw_max; + } + if (a_neg && b_pos && diff >= 0) { + return raw_min; + } + return ClampRaw(static_cast(diff), raw_min, raw_max); + } else { + auto const ua = static_cast(a); + auto const ub = static_cast(b); + if (ua < ub) { + return raw_min; + } + auto const d = ua - ub; + if (d > static_cast(raw_max)) { + return raw_max; + } + return static_cast(d); + } +} + +template +constexpr FixedPoint FromWideProduct32( + std::uint32_t hi, std::uint32_t lo, bool negative, int scale_adjust) { + using Result = FixedPoint; + using RV = typename Result::rep_value_type; + if (scale_adjust > 0) { + if (!integer_math::ShlU32WideChecked(hi, lo, + static_cast(scale_adjust))) { + return negative ? Result::FromRaw(Result::kRawMin) + : Result::FromRaw(Result::kRawMax); + } + } else if (scale_adjust < 0) { + integer_math::ShrU32Wide(hi, lo, static_cast(-scale_adjust), + true); + } + if (!negative) { + if (hi != 0U) { + return Result::FromRaw(Result::kRawMax); + } + if constexpr (std::is_signed_v) { + if (lo > static_cast(Result::kRawMax)) { + return Result::FromRaw(Result::kRawMax); + } + } else if (lo > static_cast(Result::kRawMax)) { + return Result::FromRaw(Result::kRawMax); + } + return Result::FromRaw(RepFromRawValue(static_cast(lo))); + } + // Negative: (hi,lo) is an unsigned magnitude. + if (hi != 0U) { + return Result::FromRaw(Result::kRawMin); + } + if constexpr (!std::is_signed_v) { + return Result::FromRaw(Result::kRawMin); + } else { + auto const min_mag = integer_math::AbsI32ToU32( + static_cast(Result::kRawMin)); + if (lo > min_mag) { + return Result::FromRaw(Result::kRawMin); + } + if (lo == min_mag) { + return Result::FromRaw(Result::kRawMin); + } + return Result::FromRaw(RepFromRawValue( + static_cast(-static_cast(lo)))); + } +} + template constexpr FixedPoint AddFixedPoint(L lhs, R rhs) { using Result = FixedPoint; - const auto lhs_raw = Result::AlignRawFromScale(lhs.RawValue(), L::kScaleExp); - const auto rhs_raw = Result::AlignRawFromScale(rhs.RawValue(), R::kScaleExp); - const std::int64_t sum = - static_cast(lhs_raw) + static_cast(rhs_raw); - return Result::FromRaw(RepFromRawValue( - Result::ClampRaw(static_cast(sum)))); + using RV = typename Result::rep_value_type; + if constexpr (kRepAtMost32 && kRepAtMost32 && + kRepAtMost32) { + RV const lhs_raw = ConvertRawScaleTo( + lhs.RawValue(), L::kScaleExp, Result::kScaleExp, Result::kRawMin, + Result::kRawMax); + RV const rhs_raw = ConvertRawScaleTo( + rhs.RawValue(), R::kScaleExp, Result::kScaleExp, Result::kRawMin, + Result::kRawMax); + return Result::FromRaw(RepFromRawValue( + SaturatingAdd32(lhs_raw, rhs_raw, Result::kRawMin, Result::kRawMax))); + } else { + const auto lhs_raw = + Result::AlignRawFromScale(lhs.RawValue(), L::kScaleExp); + const auto rhs_raw = + Result::AlignRawFromScale(rhs.RawValue(), R::kScaleExp); + const std::int64_t sum = + static_cast(lhs_raw) + static_cast(rhs_raw); + return Result::FromRaw(RepFromRawValue( + Result::ClampRaw(static_cast(sum)))); + } } template constexpr FixedPoint SubFixedPoint(L lhs, R rhs) { using Result = FixedPoint; - const auto lhs_raw = Result::AlignRawFromScale(lhs.RawValue(), L::kScaleExp); - const auto rhs_raw = Result::AlignRawFromScale(rhs.RawValue(), R::kScaleExp); - const std::int64_t diff = - static_cast(lhs_raw) - static_cast(rhs_raw); - return Result::FromRaw(RepFromRawValue( - Result::ClampRaw(static_cast(diff)))); + using RV = typename Result::rep_value_type; + if constexpr (kRepAtMost32 && kRepAtMost32 && + kRepAtMost32) { + RV const lhs_raw = ConvertRawScaleTo( + lhs.RawValue(), L::kScaleExp, Result::kScaleExp, Result::kRawMin, + Result::kRawMax); + RV const rhs_raw = ConvertRawScaleTo( + rhs.RawValue(), R::kScaleExp, Result::kScaleExp, Result::kRawMin, + Result::kRawMax); + return Result::FromRaw(RepFromRawValue( + SaturatingSub32(lhs_raw, rhs_raw, Result::kRawMin, Result::kRawMax))); + } else { + const auto lhs_raw = + Result::AlignRawFromScale(lhs.RawValue(), L::kScaleExp); + const auto rhs_raw = + Result::AlignRawFromScale(rhs.RawValue(), R::kScaleExp); + const std::int64_t diff = + static_cast(lhs_raw) - static_cast(rhs_raw); + return Result::FromRaw(RepFromRawValue( + Result::ClampRaw(static_cast(diff)))); + } } template constexpr FixedPoint MulFixedPoint(L lhs, R rhs) { using Result = FixedPoint; + using RV = typename Result::rep_value_type; const int scale_adjust = L::kScaleExp + R::kScaleExp - Result::kScaleExp; - - std::int64_t num = static_cast(lhs.RawValue()) * - static_cast(rhs.RawValue()); - - if constexpr (scale_adjust > 0) { - for (int i = 0; i < scale_adjust; ++i) { - num *= 2; + if constexpr (kRepAtMost32 && kRepAtMost32 && + kRepAtMost32) { + bool const lneg = std::is_signed_v && + lhs.RawValue() < static_cast(0); + bool const rneg = std::is_signed_v && + rhs.RawValue() < static_cast(0); + std::uint32_t a = 0; + std::uint32_t b = 0; + if constexpr (std::is_signed_v) { + a = integer_math::AbsI32ToU32(static_cast(lhs.RawValue())); + } else { + a = static_cast(lhs.RawValue()); } - } else if constexpr (scale_adjust < 0) { - for (int i = 0; i < -scale_adjust; ++i) { - num = RoundDivNearest(num, std::int64_t{2}); + if constexpr (std::is_signed_v) { + b = integer_math::AbsI32ToU32(static_cast(rhs.RawValue())); + } else { + b = static_cast(rhs.RawValue()); } + std::uint32_t hi = 0; + std::uint32_t lo = 0; + integer_math::MulU32Wide(a, b, hi, lo); + return FromWideProduct32(hi, lo, lneg != rneg, + scale_adjust); + } else { + std::int64_t num = static_cast(lhs.RawValue()) * + static_cast(rhs.RawValue()); + if constexpr (scale_adjust > 0) { + for (int i = 0; i < scale_adjust; ++i) { + num *= 2; + } + } else if constexpr (scale_adjust < 0) { + for (int i = 0; i < -scale_adjust; ++i) { + num = RoundDivNearest(num, std::int64_t{2}); + } + } + return Result::FromRaw(RepFromRawValue( + Result::ClampRaw(static_cast(num)))); } - - return Result::FromRaw(RepFromRawValue( - Result::ClampRaw(static_cast(num)))); } } // namespace fixed_point_internal @@ -783,34 +1146,35 @@ consteval FixedPoint operator+(T lhs, template constexpr Target AddTo(L lhs, R rhs) { - using Sum = decltype(lhs + rhs); - const Sum sum = lhs + rhs; - return Sum::template Cast(sum); + return fixed_point_internal::AddFixedPoint(lhs, rhs); } template constexpr Target SubTo(L lhs, R rhs) { - using Diff = decltype(lhs - rhs); - const Diff diff = lhs - rhs; - return Diff::template Cast(diff); + return fixed_point_internal::SubFixedPoint(lhs, rhs); } template constexpr Target MulTo(L lhs, R rhs) { - using Prod = decltype(lhs * rhs); - const Prod prod = lhs * rhs; - return Prod::template Cast(prod); + return fixed_point_internal::MulFixedPoint(lhs, rhs); } template constexpr Target DivTo(L lhs, R rhs) { + using TR = typename Target::rep_value_type; + using LR = typename L::rep_value_type; + using RR = typename R::rep_value_type; const auto lhs_raw = lhs.RawValue(); const auto rhs_raw = rhs.RawValue(); - if (rhs_raw == static_cast(0)) { + if (rhs_raw == static_cast(0)) { if constexpr (Target::kIsSigned) { - return lhs_raw >= static_cast(0) - ? Target::FromRaw(Target::kRawMax) + bool const pos = !std::is_signed_v || + lhs_raw >= static_cast(0); + return pos ? Target::FromRaw(Target::kRawMax) : Target::FromRaw(Target::kRawMin); } return Target::FromRaw(Target::kRawMax); @@ -818,24 +1182,82 @@ constexpr Target DivTo(L lhs, R rhs) { const int scale_adjust = L::kScaleExp - R::kScaleExp - Target::kScaleExp; - std::int64_t num = static_cast(lhs_raw); - std::int64_t den = static_cast(rhs_raw); - - if constexpr (scale_adjust > 0) { - for (int i = 0; i < scale_adjust; ++i) { - num *= 2; + if constexpr (fixed_point_internal::kRepAtMost32 && + fixed_point_internal::kRepAtMost32 && + fixed_point_internal::kRepAtMost32) { + bool const lneg = + std::is_signed_v && lhs_raw < static_cast(0); + bool const rneg = + std::is_signed_v && rhs_raw < static_cast(0); + bool const negative = lneg != rneg; + std::uint32_t a = 0; + std::uint32_t d = 0; + if constexpr (std::is_signed_v) { + a = integer_math::AbsI32ToU32(static_cast(lhs_raw)); + } else { + a = static_cast(lhs_raw); } - } else if constexpr (scale_adjust < 0) { - for (int i = 0; i < -scale_adjust; ++i) { - den *= 2; + if constexpr (std::is_signed_v) { + d = integer_math::AbsI32ToU32(static_cast(rhs_raw)); + } else { + d = static_cast(rhs_raw); } - } + std::uint32_t hi = 0; + std::uint32_t lo = a; + std::uint32_t dhi = 0; + std::uint32_t dlo = d; + if (scale_adjust > 0) { + if (!integer_math::ShlU32WideChecked( + hi, lo, static_cast(scale_adjust))) { + return negative ? Target::FromRaw(Target::kRawMin) + : Target::FromRaw(Target::kRawMax); + } + } else if (scale_adjust < 0) { + if (!integer_math::ShlU32WideChecked( + dhi, dlo, static_cast(-scale_adjust))) { + return Target::FromRaw(static_cast(0)); + } + } + while (dhi != 0U) { + integer_math::ShrU32Wide(hi, lo, 1U, false); + integer_math::ShrU32Wide(dhi, dlo, 1U, false); + } + if (dlo == 0U) { + return negative ? Target::FromRaw(Target::kRawMin) + : Target::FromRaw(Target::kRawMax); + } + std::uint32_t q = 0; + std::uint32_t rem = 0; + if (!integer_math::DivU32Wide(hi, lo, dlo, q, rem)) { + return negative ? Target::FromRaw(Target::kRawMin) + : Target::FromRaw(Target::kRawMax); + } + if (rem >= dlo - rem) { + if (q != std::numeric_limits::max()) { + ++q; + } + } + return fixed_point_internal::FromWideProduct32< + typename Target::rep_type, Target::kDeclaredMax>(0U, q, negative, 0); + } else { + std::int64_t num = static_cast(lhs_raw); + std::int64_t den = static_cast(rhs_raw); - const auto quotient = fixed_point_internal::RoundDivNearest(num, den); - return Target::FromRaw( - fixed_point_internal::RepFromRawValue( - Target::ClampRaw( - static_cast(quotient)))); + if constexpr (scale_adjust > 0) { + for (int i = 0; i < scale_adjust; ++i) { + num *= 2; + } + } else if constexpr (scale_adjust < 0) { + for (int i = 0; i < -scale_adjust; ++i) { + den *= 2; + } + } + + const auto quotient = fixed_point_internal::RoundDivNearest(num, den); + return Target::FromRaw( + fixed_point_internal::RepFromRawValue( + Target::ClampRaw(static_cast(quotient)))); + } } template diff --git a/ae-numeric/integer_math.h b/ae-numeric/integer_math.h index 7a8d03f..1bc34d6 100644 --- a/ae-numeric/integer_math.h +++ b/ae-numeric/integer_math.h @@ -286,6 +286,269 @@ AE_INTEGER_MATH_CONSTEXPR std::uint64_t SqrtU64(std::uint64_t n) noexcept { return x0; } +AE_INTEGER_MATH_CONSTEXPR std::uint32_t AbsI32ToU32(std::int32_t value) noexcept { + if (value >= 0) { + return static_cast(value); + } + return static_cast(-(value + 1)) + 1U; +} + +AE_INTEGER_MATH_CONSTEXPR bool MulU32Checked(std::uint32_t a, std::uint32_t b, + std::uint32_t& out) noexcept { + if (a == 0 || b == 0) { + out = 0; + return true; + } + if (a > std::numeric_limits::max() / b) { + return false; + } + out = a * b; + return true; +} + +AE_INTEGER_MATH_CONSTEXPR bool AddU32Checked(std::uint32_t a, std::uint32_t b, + std::uint32_t& out) noexcept { + if (a > std::numeric_limits::max() - b) { + return false; + } + out = a + b; + return true; +} + +// 32×32 → (hi, lo) via 16×16 products. No 64-bit arithmetic type. +AE_INTEGER_MATH_CONSTEXPR void MulU32Wide(std::uint32_t a, std::uint32_t b, + std::uint32_t& hi, + std::uint32_t& lo) noexcept { + std::uint32_t const a0 = a & 0xFFFFu; + std::uint32_t const a1 = a >> 16U; + std::uint32_t const b0 = b & 0xFFFFu; + std::uint32_t const b1 = b >> 16U; + std::uint32_t const p00 = a0 * b0; + std::uint32_t const p01 = a0 * b1; + std::uint32_t const p10 = a1 * b0; + std::uint32_t const p11 = a1 * b1; + lo = p00; + hi = p11; + std::uint32_t const p01_lo = p01 << 16U; + std::uint32_t const p01_hi = p01 >> 16U; + std::uint32_t s = lo + p01_lo; + std::uint32_t c = s < lo ? 1U : 0U; + lo = s; + hi += p01_hi + c; + std::uint32_t const p10_lo = p10 << 16U; + std::uint32_t const p10_hi = p10 >> 16U; + s = lo + p10_lo; + c = s < lo ? 1U : 0U; + lo = s; + hi += p10_hi + c; +} + +// (hi:lo) / d → 32-bit quotient. Requires d != 0 and hi < d. +AE_INTEGER_MATH_CONSTEXPR bool DivU32Wide(std::uint32_t hi, std::uint32_t lo, + std::uint32_t d, std::uint32_t& quot, + std::uint32_t& rem) noexcept { + if (d == 0 || hi >= d) { + return false; + } + std::uint32_t q = 0; + for (int i = 0; i < 32; ++i) { + std::uint32_t const hi_msb = hi >> 31U; + hi = (hi << 1U) | (lo >> 31U); + lo <<= 1U; + q <<= 1U; + if (hi_msb != 0U || hi >= d) { + hi -= d; + q |= 1U; + } + } + quot = q; + rem = hi; + return true; +} + +AE_INTEGER_MATH_CONSTEXPR bool RoundDivU32(std::uint32_t a, std::uint32_t b, + std::uint32_t& out) noexcept { + if (b == 0) { + return false; + } + std::uint32_t const q = a / b; + std::uint32_t const r = a % b; + if (r >= b - r) { + if (q == std::numeric_limits::max()) { + return false; + } + out = q + 1U; + } else { + out = q; + } + return true; +} + +// round_nearest((a * f) / d) with 32-bit operations only. +AE_INTEGER_MATH_CONSTEXPR bool MulDivU32Nearest( + std::uint32_t a, std::uint32_t f, std::uint32_t d, + std::uint32_t& out) noexcept { + if (d == 0) { + return false; + } + std::uint32_t hi = 0; + std::uint32_t lo = 0; + MulU32Wide(a, f, hi, lo); + std::uint32_t q = 0; + std::uint32_t r = 0; + if (!DivU32Wide(hi, lo, d, q, r)) { + return false; + } + if (r >= d - r) { + if (q == std::numeric_limits::max()) { + return false; + } + ++q; + } + out = q; + return true; +} + +AE_INTEGER_MATH_CONSTEXPR std::uint32_t SqrtU32(std::uint32_t n) noexcept { + if (n < 2U) { + return n; + } + std::uint32_t x0 = n >> 1U; + std::uint32_t x1 = (x0 + n / x0) >> 1U; + while (x1 < x0) { + x0 = x1; + x1 = (x0 + n / x0) >> 1U; + } + return x0; +} + +// Floor sqrt of a 64-bit magnitude stored as (hi, lo). 32-bit operations only: +// digit-by-digit search of the 32-bit root, comparing squares via MulU32Wide. +AE_INTEGER_MATH_CONSTEXPR std::uint32_t SqrtU32Wide(std::uint32_t hi, + std::uint32_t lo) noexcept { + if (hi == 0U) { + return SqrtU32(lo); + } + std::uint32_t res = 0; + for (unsigned i = 0; i < 32U; ++i) { + std::uint32_t const cand = res | (1U << (31U - i)); + std::uint32_t phi = 0; + std::uint32_t plo = 0; + MulU32Wide(cand, cand, phi, plo); + if (phi < hi || (phi == hi && plo <= lo)) { + res = cand; + } + } + return res; +} + +// Two's-complement negate of a 64-bit value stored as (hi, lo) uint32 halves. +AE_INTEGER_MATH_CONSTEXPR void NegU32Wide(std::uint32_t& hi, + std::uint32_t& lo) noexcept { + lo = ~lo + 1U; + hi = ~hi + (lo == 0U ? 1U : 0U); +} + +AE_INTEGER_MATH_CONSTEXPR void MulI32Wide(std::int32_t a, std::int32_t b, + std::uint32_t& hi, + std::uint32_t& lo) noexcept { + bool const neg = (a < 0) != (b < 0); + MulU32Wide(AbsI32ToU32(a), AbsI32ToU32(b), hi, lo); + if (neg) { + NegU32Wide(hi, lo); + } +} + +// Shift (hi:lo) left. Returns false if bits shift out of hi. +AE_INTEGER_MATH_CONSTEXPR bool ShlU32WideChecked(std::uint32_t& hi, + std::uint32_t& lo, + unsigned bits) noexcept { + if (bits == 0U) { + return true; + } + if (bits >= 64U) { + if (hi != 0U || lo != 0U) { + return false; + } + return true; + } + if (bits >= 32U) { + unsigned const rest = bits - 32U; + if (hi != 0U) { + return false; + } + if (rest != 0U && (lo >> (32U - rest)) != 0U) { + return false; + } + hi = lo << rest; + lo = 0U; + return true; + } + if ((hi >> (32U - bits)) != 0U) { + return false; + } + hi = (hi << bits) | (lo >> (32U - bits)); + lo <<= bits; + return true; +} + +// Arithmetic-free logical right shift of (hi:lo) with optional round-nearest +// (half away from zero on the unsigned magnitude). +AE_INTEGER_MATH_CONSTEXPR void ShrU32Wide(std::uint32_t& hi, std::uint32_t& lo, + unsigned bits, + bool round_nearest) noexcept { + if (bits == 0U) { + return; + } + if (bits >= 64U) { + std::uint32_t const round = + round_nearest && (hi != 0U || lo != 0U) ? 1U : 0U; + hi = 0U; + lo = round; + return; + } + std::uint32_t round_bit = 0U; + if (round_nearest) { + if (bits <= 32U) { + round_bit = (lo >> (bits - 1U)) & 1U; + } else { + round_bit = (hi >> (bits - 33U)) & 1U; + } + } + if (bits >= 32U) { + lo = hi >> (bits - 32U); + hi = 0U; + } else { + lo = (lo >> bits) | (hi << (32U - bits)); + hi >>= bits; + } + if (round_bit != 0U) { + lo += 1U; + if (lo == 0U) { + hi += 1U; + } + } +} + +// Compare unsigned products a*b vs c*d using 16×16 decomposition. +AE_INTEGER_MATH_CONSTEXPR int CmpMulU32(std::uint32_t a, std::uint32_t b, + std::uint32_t c, + std::uint32_t d) noexcept { + std::uint32_t ahi = 0; + std::uint32_t alo = 0; + std::uint32_t bhi = 0; + std::uint32_t blo = 0; + MulU32Wide(a, b, ahi, alo); + MulU32Wide(c, d, bhi, blo); + if (ahi != bhi) { + return ahi < bhi ? -1 : 1; + } + if (alo != blo) { + return alo < blo ? -1 : 1; + } + return 0; +} + } // namespace ae::integer_math #undef AE_INTEGER_MATH_CONSTEXPR diff --git a/ae-numeric/segmented_number.h b/ae-numeric/segmented_number.h index 236dd4e..fa251ed 100644 --- a/ae-numeric/segmented_number.h +++ b/ae-numeric/segmented_number.h @@ -17,9 +17,10 @@ #ifndef AE_NUMERIC_SEGMENTED_NUMBER_H_ #define AE_NUMERIC_SEGMENTED_NUMBER_H_ +#include #include #include -#include +#include #include #include @@ -27,7 +28,6 @@ #include "ae-numeric/details/segmented_curves.h" #include "ae-numeric/details/segmented_format.h" #include "ae-numeric/details/segmented_formula_backend.h" -#include "ae-numeric/details/segmented_lookup_backend.h" #include "ae-numeric/fixed_point.h" #include "ae-numeric/numeric_traits.h" #include "ae-numeric/runtime_numeric_traits.h" @@ -42,62 +42,25 @@ namespace segmented_number_internal { template struct RuntimeRawMap { - static constexpr std::int64_t ToRaw(RT const& v) { + using raw_type = typename Logical::rep_value_type; + + static constexpr raw_type ToRaw(RT const& v) { static_assert(numeric_traits::kIsFixedPoint, "include ae-numeric/segmented_number_floating_runtime.h " "to use floating runtime"); - return static_cast(v.RawValue()); + return v.RawValue(); } - static constexpr RT FromRaw(std::int64_t raw) { + static constexpr RT FromRaw(raw_type raw) { static_assert(numeric_traits::kIsFixedPoint, "include ae-numeric/segmented_number_floating_runtime.h " "to use floating runtime"); - auto const clamped = RT::ClampRaw(static_cast( - raw < static_cast(RT::kRawMin) - ? RT::kRawMin - : (raw > static_cast(RT::kRawMax) ? RT::kRawMax - : raw))); + auto const clamped = + raw < RT::kRawMin ? RT::kRawMin + : (raw > RT::kRawMax ? RT::kRawMax : raw); return RT::FromRaw( - fixed_point_internal::RepFromRawValue(clamped)); - } -}; - -template -struct Codec { - static constexpr std::uint32_t Encode( - segmented_compiler_internal::CompiledSegment const* segs, int nseg, - std::uint32_t code_count, std::int64_t raw) { - return segmented_formula_internal::EncodeRaw(segs, nseg, code_count, - raw); - } - - static constexpr std::int64_t Decode( - segmented_compiler_internal::CompiledSegment const* segs, int nseg, - std::uint32_t rank) { - return segmented_formula_internal::DecodeRankRaw(segs, nseg, rank); - } -}; - -template -struct Codec { - static constexpr auto kTables = - segmented_lookup_internal::MakeLookupTables(); - - static constexpr std::uint32_t Encode( - segmented_compiler_internal::CompiledSegment const*, int, std::uint32_t, - std::int64_t raw) { - return segmented_lookup_internal::LookupEncode(kTables, - raw); - } - - static constexpr std::int64_t Decode( - segmented_compiler_internal::CompiledSegment const*, int, - std::uint32_t rank) { - if (rank >= N) { - return kTables.decoded[0]; - } - return kTables.decoded[rank]; + fixed_point_internal::RepFromRawValue( + RT::ClampRaw(clamped))); } }; @@ -107,10 +70,6 @@ using RuntimeType = std::conditional_t< typename Spec::runtime_policy::rep, segmented_compiler_internal::LogicalTypeOf>; -template -inline constexpr bool kUseLookup = - std::is_same_v; - template constexpr Wire RankToWire(std::uint32_t rank) { if constexpr (std::is_same_v) { @@ -138,8 +97,15 @@ class SegmentedNumber { using runtime_type = segmented_number_internal::RuntimeType; using logical_type = segmented_compiler_internal::LogicalTypeOf; using wire_type = segmented_compiler_internal::WireTypeOf; - using runtime_raw_type = std::int64_t; + using runtime_raw_type = typename logical_type::rep_value_type; + static_assert(sizeof(runtime_raw_type) <= 4, + "SegmentedNumber runtime raw must be at most 32 bits"); + static_assert(std::is_same_v, + "SegmentedNumber supports compute::Formula only"); + + static_assert(sizeof(runtime_raw_type) <= 4, + "SegmentedNumber runtime raw must be at most 32 bits"); static constexpr auto kPlan = segmented_compiler_internal::PlanHolder::kPlan; static constexpr std::size_t kSegmentCount = @@ -148,12 +114,13 @@ class SegmentedNumber { static_cast(kPlan.code_count); static constexpr std::size_t kMaxWireBytes = kPlan.n8 > 0 ? 8U : (kPlan.n4 > 0 ? 4U : (kPlan.n2 > 0 ? 2U : 1U)); + // Compile-time schema identity (FNV-1a). Not used by encode/decode math. static constexpr std::uint64_t kSchemaHash = kPlan.schema_hash; static constexpr std::uint32_t kOneByteCount = kPlan.n1; static constexpr std::uint32_t kTwoByteCount = kPlan.n2; static constexpr std::uint32_t kFourByteCount = kPlan.n4; static constexpr std::size_t kFormulaCoefficientBytes = - sizeof(segmented_compiler_internal::CompiledSegment) * kSegmentCount; + segmented_compiler_internal::FormulaCoefficientBytes(); static_assert(!kIsFloatingRuntimePolicy || kFloatingRuntimeEnabled, @@ -162,35 +129,39 @@ class SegmentedNumber { static_assert(kCodeCount >= 1U, "format without values"); static constexpr auto kSegments = - segmented_compiler_internal::MakeCompiledSegments(); - - static constexpr std::size_t kLookupTableBytes = - segmented_number_internal::kUseLookup - ? kCodeCount * (sizeof(std::int64_t) + sizeof(std::uint32_t)) - : 0; + segmented_compiler_internal::MakeExactCompiledSegments< + Spec, logical_type, kSegmentCount>(); - constexpr SegmentedNumber() : value_(Decode(wire_type{})) {} + constexpr SegmentedNumber() : value_(DecodeUnchecked(wire_type{})) {} runtime_type Value() const { return value_; } + static std::optional TryDecode(wire_type wire) { + std::uint32_t const rank = + segmented_number_internal::WireToRank(wire); + if (rank >= kCodeCount) { + return std::nullopt; + } + return DecodeUnchecked(wire); + } + static constexpr runtime_type Decode(wire_type wire) { - std::uint32_t rank = segmented_number_internal::WireToRank(wire); + std::uint32_t const rank = + segmented_number_internal::WireToRank(wire); if (rank >= kCodeCount) { - rank = 0; + assert(rank < kCodeCount); + if (!std::is_constant_evaluated()) { + std::abort(); + } } - std::int64_t const raw = segmented_number_internal::Codec< - Spec, logical_type, kCodeCount, - segmented_number_internal::kUseLookup>::Decode( - kSegments.data(), kPlan.count, rank); - return segmented_number_internal::RuntimeRawMap::FromRaw(raw); + return DecodeUnchecked(wire); } static std::optional TryEncode(runtime_type value) { - std::int64_t const raw = + runtime_raw_type const raw = segmented_number_internal::RuntimeRawMap::ToRaw(value); - if (raw < kRawMin || raw > kRawMax) { + if (raw < kDeclaredRawMin || raw > kDeclaredRawMax) { return std::nullopt; } return segmented_number_internal::RankToWire(EncodeRaw(raw)); @@ -205,14 +176,14 @@ class SegmentedNumber { } static SegmentedNumber Saturating(runtime_type value) { - std::int64_t raw = + runtime_raw_type raw = segmented_number_internal::RuntimeRawMap::ToRaw(value); - if (raw < kRawMin) { - raw = kRawMin; + if (raw < kDeclaredRawMin) { + raw = kDeclaredRawMin; } - if (raw > kRawMax) { - raw = kRawMax; + if (raw > kDeclaredRawMax) { + raw = kDeclaredRawMax; } return FromWire(segmented_number_internal::RankToWire( EncodeRaw(raw))); @@ -226,7 +197,7 @@ class SegmentedNumber { static SegmentedNumber FromWire(wire_type wire) { SegmentedNumber n; - n.value_ = Decode(wire); + n.value_ = DecodeUnchecked(wire); return n; } @@ -285,40 +256,31 @@ class SegmentedNumber { static constexpr auto const& Logical() { return kPlan; } + static constexpr runtime_raw_type kDeclaredRawMin = + logical_type::FromRatio(kPlan.declared_min.num, kPlan.declared_min.den) + .RawValue(); + static constexpr runtime_raw_type kDeclaredRawMax = + logical_type::FromRatio(kPlan.declared_max.num, kPlan.declared_max.den) + .RawValue(); + static constexpr runtime_raw_type kRepresentableRawMin = + segmented_formula_internal::ScanRepresentable(true); + static constexpr runtime_raw_type kRepresentableRawMax = + segmented_formula_internal::ScanRepresentable( + false); + private: - static constexpr std::int64_t kRawMin = std::invoke([]() { - std::int64_t m = segmented_formula_internal::DecodeRankRaw( - kSegments.data(), kPlan.count, 0); - for (std::uint32_t r = 1; r < static_cast(kCodeCount); ++r) { - std::int64_t const v = - segmented_formula_internal::DecodeRankRaw( - kSegments.data(), kPlan.count, r); - if (v < m) { - m = v; - } - } - return m; - }); - static constexpr std::int64_t kRawMax = std::invoke([]() { - std::int64_t m = segmented_formula_internal::DecodeRankRaw( - kSegments.data(), kPlan.count, 0); - for (std::uint32_t r = 1; r < static_cast(kCodeCount); ++r) { - std::int64_t const v = - segmented_formula_internal::DecodeRankRaw( - kSegments.data(), kPlan.count, r); - if (v > m) { - m = v; - } - } - return m; - }); - - static constexpr std::uint32_t EncodeRaw(std::int64_t raw) { - return segmented_number_internal::Codec< - Spec, logical_type, kCodeCount, - segmented_number_internal::kUseLookup>::Encode( - kSegments.data(), kPlan.count, static_cast(kCodeCount), - raw); + static constexpr runtime_type DecodeUnchecked(wire_type wire) { + std::uint32_t const rank = + segmented_number_internal::WireToRank(wire); + runtime_raw_type const raw = + segmented_formula_internal::DecodeRankRaw(rank); + return segmented_number_internal::RuntimeRawMap::FromRaw(raw); + } + + static constexpr std::uint32_t EncodeRaw(runtime_raw_type raw) { + return segmented_formula_internal::EncodeRaw( + static_cast(kCodeCount), raw); } runtime_type value_{}; @@ -368,10 +330,6 @@ struct runtime_numeric_traits> { static constexpr value_type FromInteger(std::int64_t value) { return value_type::Saturating(runtime_numeric_traits::FromInteger(value)); } - - static consteval value_type FromDouble(double value) { - return value_type::Saturating(runtime_numeric_traits::FromDouble(value)); - } }; } // namespace ae diff --git a/ae-numeric/segmented_number_floating_runtime.h b/ae-numeric/segmented_number_floating_runtime.h index d70f812..2e53bc5 100644 --- a/ae-numeric/segmented_number_floating_runtime.h +++ b/ae-numeric/segmented_number_floating_runtime.h @@ -21,6 +21,8 @@ #include "ae-numeric/exponential_floating_runtime.h" #include "ae-numeric/segmented_number.h" +#include + namespace ae::seg { template <> @@ -31,28 +33,34 @@ inline constexpr bool kFloatingRuntimeEnabled = true; namespace segmented_number_internal { template -constexpr std::int64_t FloatingToLogicalRaw(FloatingT value) { +constexpr typename Logical::rep_value_type FloatingToLogicalRaw( + FloatingT value) { if (value == FloatingT{0}) { - return 0; + return static_cast(0); } bool const neg = value < FloatingT{0}; FloatingT const mag = neg ? -value : value; Logical const work = exponential_internal::FloatingToWork(mag); - std::int64_t const raw = static_cast(work.RawValue()); - return neg ? -raw : raw; + auto const raw = work.RawValue(); + if constexpr (std::is_signed_v) { + return neg ? static_cast(-raw) : raw; + } else { + (void)neg; + return raw; + } } template -constexpr FloatingT LogicalRawToFloating(std::int64_t raw) { - bool const neg = raw < 0; - std::int64_t ar = neg ? -raw : raw; - if (ar > static_cast(Logical::kRawMax)) { - ar = static_cast(Logical::kRawMax); +constexpr FloatingT LogicalRawToFloating(typename Logical::rep_value_type raw) { + bool const neg = + std::is_signed_v && raw < 0; + auto ar = neg ? static_cast(-raw) : raw; + if (ar > Logical::kRawMax) { + ar = Logical::kRawMax; } Logical const work = Logical::FromRaw( - fixed_point_internal::RepFromRawValue( - static_cast(ar))); + fixed_point_internal::RepFromRawValue(ar)); FloatingT const mag = exponential_internal::WorkToFloating(work); return neg ? -mag : mag; @@ -60,20 +68,20 @@ constexpr FloatingT LogicalRawToFloating(std::int64_t raw) { template struct RuntimeRawMap { - static constexpr std::int64_t ToRaw(float v) { + static constexpr typename Logical::rep_value_type ToRaw(float v) { return FloatingToLogicalRaw(v); } - static constexpr float FromRaw(std::int64_t raw) { + static constexpr float FromRaw(typename Logical::rep_value_type raw) { return LogicalRawToFloating(raw); } }; template struct RuntimeRawMap { - static constexpr std::int64_t ToRaw(double v) { + static constexpr typename Logical::rep_value_type ToRaw(double v) { return FloatingToLogicalRaw(v); } - static constexpr double FromRaw(std::int64_t raw) { + static constexpr double FromRaw(typename Logical::rep_value_type raw) { return LogicalRawToFloating(raw); } }; diff --git a/docs/benchmark_results.txt b/docs/benchmark_results.txt new file mode 100644 index 0000000..4811024 --- /dev/null +++ b/docs/benchmark_results.txt @@ -0,0 +1,15 @@ +BENCH_HOST arch=desktop note=nanoseconds_not_esp32_cycles iters=200000 +BENCH_SEG name=Rssi logical=-40 wire_bytes=1 encode_ns=24.170 decode_ns=0.238 serialize_ns=474.880 deserialize_ns=0.268 roundtrip_ns=440.944 sink=1 +BENCH_SEG name=Temperature_center logical=25 wire_bytes=1 encode_ns=20.201 decode_ns=0.234 serialize_ns=5676.991 deserialize_ns=33.132 roundtrip_ns=5848.599 sink=1 +BENCH_SEG name=Temperature_low logical=-35 wire_bytes=2 encode_ns=21.945 decode_ns=0.279 serialize_ns=7578.979 deserialize_ns=226.782 roundtrip_ns=7485.159 sink=2 +BENCH_SEG name=Temperature_high logical=100 wire_bytes=2 encode_ns=22.858 decode_ns=0.258 serialize_ns=7486.931 deserialize_ns=219.238 roundtrip_ns=7515.242 sink=2 +BENCH_SEG name=Humidity logical=55 wire_bytes=1 encode_ns=23.458 decode_ns=0.259 serialize_ns=636.130 deserialize_ns=0.259 roundtrip_ns=633.419 sink=1 +BENCH_SEG name=Co2_1B logical=600 wire_bytes=1 encode_ns=23.430 decode_ns=0.224 serialize_ns=6986.739 deserialize_ns=76.366 roundtrip_ns=7125.114 sink=1 +BENCH_SEG name=Co2_2B logical=2500 wire_bytes=2 encode_ns=26.120 decode_ns=0.252 serialize_ns=6709.723 deserialize_ns=83.351 roundtrip_ns=6619.372 sink=2 +BENCH_SEG name=Co2_4B logical=18000 wire_bytes=4 encode_ns=24.526 decode_ns=0.252 serialize_ns=7105.739 deserialize_ns=78.898 roundtrip_ns=7344.620 sink=4 +BENCH_SEG name=Rx_1B logical=1 wire_bytes=1 encode_ns=23.081 decode_ns=0.252 serialize_ns=5031.953 deserialize_ns=1.951 roundtrip_ns=5078.962 sink=1 +BENCH_SEG name=Rx_2B logical=120 wire_bytes=2 encode_ns=23.729 decode_ns=0.239 serialize_ns=9176.965 deserialize_ns=208.262 roundtrip_ns=10699.008 sink=2 +BENCH_SEG name=Rx_4B logical=7200 wire_bytes=4 encode_ns=30.999 decode_ns=0.218 serialize_ns=10707.724 deserialize_ns=54.874 roundtrip_ns=10635.228 sink=4 +BENCH_SEG name=Battery logical=3 wire_bytes=1 encode_ns=53.407 decode_ns=0.418 serialize_ns=1362.323 deserialize_ns=0.219 roundtrip_ns=1267.943 sink=1 +BENCH_SEG name=ConnectDuration logical=5 wire_bytes=1 encode_ns=44.609 decode_ns=0.382 serialize_ns=2543.144 deserialize_ns=1.254 roundtrip_ns=2137.385 sink=1 +BENCH_CYC wire_ns=0.627 restore_fwd_ns=0.627 restore_back_ns=0.627 advance_ns=0.598 ctx_deser_ns=0.598 sink=239 diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..3a6096d --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,73 @@ +# Numeric micro-benchmarks + +Desktop host timings for `SegmentedNumber` and `CyclicCounter`. + +**These results are desktop nanoseconds, not ESP32-C6 cycle counts.** Do not rescale them to MCU cycles. + +## Method + +| Item | Value | +|---|---| +| Architecture | Host desktop (Windows x86_64 when measured here) | +| Toolchain | Host CMake compiler for `numeric-bench` | +| Optimization | Release (`-O2` / MSVC `/O2` as configured) | +| CPU frequency | Host turbo frequency (not fixed MCU MHz) | +| Iterations | 200 000 timed ops after warm-up | +| Warm-up | ~5% of iterations before the timed loop | +| Anti-DCE | `volatile` sinks on encode/decode/wire results | +| Measured ops | Encode, Decode, Serialize, Deserialize, round-trip; CyclicCounter WireValue / TryRestore / TryAdvance / contextual deserialize | + +Build and run: + +```bash +cmake -S . -B build-bench -DCMAKE_BUILD_TYPE=Release -DAE_BUILD_TESTS=ON +cmake --build build-bench --target numeric-bench +./build-bench/tests/numeric-bench # or build-bench\tests\Release\numeric-bench.exe +``` + +Machine-readable lines are prefixed `BENCH_SEG` / `BENCH_CYC`. Refresh the table below after a run (paste or feed into the doc generator when extended). + +--- + +## Results + + +`BENCH_HOST arch=desktop note=nanoseconds_not_esp32_cycles iters=200000` + +| Name | Encode ns | Decode ns | Serialize ns | Deserialize ns | Round-trip ns | Notes | +|---|---:|---:|---:|---:|---:|---| +| Rssi | 24.170 | 0.238 | 474.880 | 0.268 | 440.944 | logical=-40 wire_bytes=1 | +| Temperature_center | 20.201 | 0.234 | 5676.991 | 33.132 | 5848.599 | logical=25 wire_bytes=1 | +| Temperature_low | 21.945 | 0.279 | 7578.979 | 226.782 | 7485.159 | logical=-35 wire_bytes=2 | +| Temperature_high | 22.858 | 0.258 | 7486.931 | 219.238 | 7515.242 | logical=100 wire_bytes=2 | +| Humidity | 23.458 | 0.259 | 636.130 | 0.259 | 633.419 | logical=55 wire_bytes=1 | +| Co2_1B | 23.430 | 0.224 | 6986.739 | 76.366 | 7125.114 | logical=600 wire_bytes=1 | +| Co2_2B | 26.120 | 0.252 | 6709.723 | 83.351 | 6619.372 | logical=2500 wire_bytes=2 | +| Co2_4B | 24.526 | 0.252 | 7105.739 | 78.898 | 7344.620 | logical=18000 wire_bytes=4 | +| Rx_1B | 23.081 | 0.252 | 5031.953 | 1.951 | 5078.962 | logical=1 wire_bytes=1 | +| Rx_2B | 23.729 | 0.239 | 9176.965 | 208.262 | 10699.008 | logical=120 wire_bytes=2 | +| Rx_4B | 30.999 | 0.218 | 10707.724 | 54.874 | 10635.228 | logical=7200 wire_bytes=4 | +| Battery | 53.407 | 0.418 | 1362.323 | 0.219 | 1267.943 | logical=3 wire_bytes=1 | +| ConnectDuration | 44.609 | 0.382 | 2543.144 | 1.254 | 2137.385 | logical=5 wire_bytes=1 | +| CyclicCounter WireValue | 0.627 | - | - | - | - | desktop ns | +| CyclicCounter TryRestore forward | 0.627 | - | - | - | - | desktop ns | +| CyclicCounter TryRestore backward | 0.627 | - | - | - | - | desktop ns | +| CyclicCounter TryAdvance | 0.598 | - | - | - | - | desktop ns | +| CyclicCounter contextual deserialize | 0.598 | - | - | - | - | desktop ns | + + +### How to read SegmentedNumber rows + +* **CO₂** and **RX** are split by approximate wire tier via sample selection (1 / 2 / 4-byte regimes where the format supports them). +* **Temperature** is split into central one-byte, lower tail, and upper tail samples. +* Encode/Decode are pure rank codecs; Serialize/Deserialize include `wire_traits` packing. + +### CyclicCounter rows + +| Operation | Meaning | +|---|---| +| WireValue | Truncate full value to wire bits | +| TryRestore forward | Restore a newer truncated sample without mutating | +| TryRestore backward | Restore an older truncated sample without mutating | +| TryAdvance | Restore and update base only when newer | +| Contextual deserialize | `TryDeserializeAndAdvance` from a wire buffer | diff --git a/docs/footprint.md b/docs/footprint.md new file mode 100644 index 0000000..80030ad --- /dev/null +++ b/docs/footprint.md @@ -0,0 +1,217 @@ +# Numeric code footprint + +This document records **object-only** flash and RAM cost for `SegmentedNumber` formats and `CyclicCounter` on **ESP32-C6** (RISC-V `rv32imac` / `ilp32`), plus instance sizes and stack usage. + +Regenerate numbers after changing the library: + +```bash +cmake -S . -B build-dev -DCMAKE_BUILD_TYPE=Release -DAE_BUILD_TESTS=ON +cmake --build build-dev --target segmented-footprint-obj +python tools/measure_esp32c6_footprint.py --repo . +python tools/generate_footprint_docs.py --repo . +``` + +`measure_esp32c6_footprint.py` writes `docs/footprint_results.json`. +`generate_footprint_docs.py` refreshes only the marked generated tables below. + +--- + +## Metrics + +| Section | Meaning | +|---|---| +| `.text` | Machine code | +| `.rodata` | Read-only constants | +| `.data` | Initialized static RAM (and its flash image) | +| `.bss` | Zero-initialized static RAM | + +Formulas: + +```text +Total flash = .text + .rodata + .data +Static RAM = .data + .bss +``` + +Notes: + +* Sizes come from **minimal object-only** targets (`tests/footprint/minimal_*.cpp`) compiled with Espressif `riscv32-esp-elf-g++`, `-ffunction-sections` / `-fdata-sections`, no exceptions/RTTI. +* CRT, Unity, `printf`, iostream, and application frameworks are **not** included. +* Harnesses keep encode/decode/serialize reachable (anti-DCE) via `volatile` sinks. +* Reported values are the **incremental footprint of the numeric implementation** pulled into that translation unit. +* A full application size also depends on linker deduplication and whether `Log2`/`Exp2`/FixedPoint helpers are already linked from elsewhere. + +--- + +## `sizeof(CompiledSegment)` is not flash + +```text +sizeof(CompiledSegment) == 68 bytes # host / ESP ILP32 measurement via fp-dump-meta +``` + +That is the size of the **universal C++ compile-time descriptor type**, not bytes of flash per segment. + +* The descriptor array is used at compile time and does **not** materialize as a per-segment `.rodata` table of `68 * segment_count`. +* Encode/decode paths are specialized per format and segment index. +* Real constant flash data are the shared FixedPoint `Log2` / `Exp2` tables when those curves are used; their size does **not** scale with code count or segment count. +* Do **not** treat `sizeof(CompiledSegment) * segments` as flash consumption. + +--- + +## SegmentedNumber ESP32-C6 footprint + + +Toolchain: `riscv32-esp-elf-g++ (esp-14.2.0_20241119)` (Espressif riscv32-esp-elf) +Target: ESP32-C6 / riscv32 ilp32 + +### ESP32-C6 `-Os` + +| Format | Opt | .text | .rodata | .data | .bss | Flash | RAM | sizeof(runtime) | sizeof(number) | sizeof(wire) | Codes | Segments | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| RSSI | -Os | 964 | 0 | 0 | 0 | 964 | 0 | 1 | 1 | 1 | 128 | 1 | +| Temperature | -Os | 5126 | 208 | 0 | 0 | 5334 | 0 | 2 | 2 | 2 | 1021 | 3 | +| Humidity | -Os | 2962 | 0 | 0 | 0 | 2962 | 0 | 2 | 2 | 1 | 256 | 3 | +| CO2 | -Os | 3658 | 208 | 0 | 0 | 3866 | 0 | 2 | 2 | 4 | 822 | 3 | +| RX window | -Os | 6596 | 208 | 0 | 0 | 6804 | 0 | 4 | 4 | 4 | 3046 | 4 | +| Battery | -Os | 4398 | 208 | 0 | 0 | 4606 | 0 | 2 | 2 | 1 | 256 | 2 | +| ConnectDuration | -Os | 3068 | 208 | 0 | 0 | 3276 | 0 | 4 | 4 | 1 | 256 | 2 | +| Thermometer | -Os | 9124 | 208 | 0 | 0 | 9332 | 0 | - | - | - | - | - | +| All seven | -Os | 12080 | 208 | 0 | 0 | 12288 | 0 | - | - | - | - | - | + +### ESP32-C6 `-O2` + +| Format | Opt | .text | .rodata | .data | .bss | Flash | RAM | sizeof(runtime) | sizeof(number) | sizeof(wire) | Codes | Segments | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| RSSI | -O2 | 1136 | 0 | 0 | 0 | 1136 | 0 | 1 | 1 | 1 | 128 | 1 | +| Temperature | -O2 | 6050 | 208 | 0 | 0 | 6258 | 0 | 2 | 2 | 2 | 1021 | 3 | +| Humidity | -O2 | 5800 | 0 | 0 | 0 | 5800 | 0 | 2 | 2 | 1 | 256 | 3 | +| CO2 | -O2 | 3890 | 208 | 0 | 0 | 4098 | 0 | 2 | 2 | 4 | 822 | 3 | +| RX window | -O2 | 8536 | 208 | 0 | 0 | 8744 | 0 | 4 | 4 | 4 | 3046 | 4 | +| Battery | -O2 | 4252 | 208 | 0 | 0 | 4460 | 0 | 2 | 2 | 1 | 256 | 2 | +| ConnectDuration | -O2 | 2786 | 208 | 0 | 0 | 2994 | 0 | 4 | 4 | 1 | 256 | 2 | +| Thermometer | -O2 | 14868 | 208 | 0 | 0 | 15076 | 0 | - | - | - | - | - | +| All seven | -O2 | 19838 | 208 | 0 | 0 | 20046 | 0 | - | - | - | - | - | + + +Thermometer = Temperature + Humidity + CO2 + Battery in one TU. +All seven = RSSI + Temperature + Humidity + CO2 + RX + Battery + ConnectDuration. + +--- + +## CyclicCounter ESP32-C6 footprint + + +### ESP32-C6 `-Os` + +| Configuration | Wire B | Runtime B | .text | .rodata | .data | .bss | Flash | RAM | Half-range | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| `uint8_t` -> `uint16_t` | 1 | 2 | 224 | 0 | 0 | 0 | 224 | 0 | 127 | +| `uint8_t` -> `uint32_t` | 1 | 4 | 182 | 0 | 0 | 0 | 182 | 0 | 127 | +| `uint16_t` -> `uint32_t` | 2 | 4 | 136 | 0 | 0 | 0 | 136 | 0 | 32767 | + +### ESP32-C6 `-O2` + +| Configuration | Wire B | Runtime B | .text | .rodata | .data | .bss | Flash | RAM | Half-range | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| `uint8_t` -> `uint16_t` | 1 | 2 | 244 | 0 | 0 | 0 | 244 | 0 | 127 | +| `uint8_t` -> `uint32_t` | 1 | 4 | 188 | 0 | 0 | 0 | 188 | 0 | 127 | +| `uint16_t` -> `uint32_t` | 2 | 4 | 136 | 0 | 0 | 0 | 136 | 0 | 32767 | + +All three configurations: `.rodata = 0`, `.data = 0`, `.bss = 0`, heap = 0, tables = 0, 64-bit arithmetic helper undefs = 0. + + +--- + +## Code sharing + +Standalone footprints sum larger than a combined TU because the linker keeps one copy of shared helpers: + +* `Log2` / `Exp2` and their FixedPoint tables +* FixedPoint division / scale conversion +* common curve helpers (`GeomApprox`, ramp, …) +* `TieredInt` wire paths +* shared wire API glue + + +### ESP32-C6 `-Os` + +| Bundle | Standalone .text sum | Combined .text | Saved | +|---|---:|---:|---:| +| Temperature + CO2 (standalone sum) | 8784 | (linked separately) | - | +| Thermometer (T+H+CO2+Bat) | 16144 | 9124 | 7020 | +| All seven | 26772 | 12080 | 14692 | + +### ESP32-C6 `-O2` + +| Bundle | Standalone .text sum | Combined .text | Saved | +|---|---:|---:|---:| +| Temperature + CO2 (standalone sum) | 9940 | (linked separately) | - | +| Thermometer (T+H+CO2+Bat) | 19992 | 14868 | 5124 | +| All seven | 32450 | 19838 | 12612 | + + +--- + +## Constant data + +Shared tables used by logarithmic / exponential / geometric / ramp math (ESP32-C6, ILP32): + +| Symbol | Element type | Count | Bytes | Purpose | +|---|---|---:|---:|---| +| `InvPow2TableHolder,26>::kTable` | `FixedPoint` (Rep <= 32-bit) | 26 | 104 | `Log2` / fractional `Exp2` bit weights | +| `Exp2MantTableHolder,26>::kTable` | `FixedPoint` (Rep <= 32-bit) | 26 | 104 | `Exp2` mantissa factors | + +Together these account for the observed **208** bytes of `.rodata` on formats that pull in `Log2`/`Exp2`. + +* Elements are our `FixedPoint` values; Rep is at most 32 bits. +* There are no homemade raw Q30/Q31 tables. +* Tables are **common** across formats; linking several formats in one TU does **not** duplicate them (still 208 B `.rodata`). +* Purely linear formats (e.g. RSSI, Humidity in this suite) can show `.rodata = 0`. +* `CyclicCounter` has `.rodata = 0` (no tables, no 64-bit helper undefs). + +--- + +## Runtime object size + +```text +sizeof(SegmentedNumber) == sizeof(runtime_type) +sizeof(CyclicCounter) == sizeof(ValueType) +``` + +No instance stores segment descriptors, lookup tables, heap pointers, or decoder state. + +| Type | sizeof object | Wire size | Heap | Additional runtime state | +|---|---:|---:|---|---| +| RSSI `SegmentedNumber` | 1 | 1 | 0 | none | +| Temperature | 2 | <=2 | 0 | none | +| Humidity | 2 | 1 | 0 | none | +| CO2 | 2 | <=4 | 0 | none | +| RX window | 4 | <=4 | 0 | none | +| Battery | 2 | 1 | 0 | none | +| ConnectDuration | 4 | 1 | 0 | none | +| `CyclicCounter` | 2 | 1 | 0 | none | +| `CyclicCounter` | 4 | 1 | 0 | none | +| `CyclicCounter` | 4 | 2 | 0 | none | + +--- + +## Stack usage + +Measured with GCC `-fstack-usage` on the same ESP32-C6 `-Os` object builds used for footprint (per-function static frame size from `.su` files). These are **compiler-reported frames**, not hand estimates. Call-graph worst case for deep helpers (e.g. RX `LinearRampApproxRuntime`) is listed in the Method column. + + +| Type | Encode | Decode | Serialize | Deserialize | Worst case | Method | +|---|---:|---:|---:|---:|---:|---| +| Temperature | 32 | 32 | 32 | 32 | 48 | GCC `-fstack-usage` ESP32-C6 `-Os` (.su) | +| CO2 | 32 | 32 | 32 | 32 | 48 | GCC `-fstack-usage` ESP32-C6 `-Os` (.su) | +| RX | 32 | 48 | 32 | 48 | 112 | GCC `-fstack-usage` ESP32-C6 `-Os` (.su) | +| Battery | 32 | 32 | 32 | 32 | 48 | GCC `-fstack-usage` ESP32-C6 `-Os` (.su) | +| CyclicCounter u8->u32 | 32 | 16 | 32 | 32 | 32 | GCC `-fstack-usage` ESP32-C6 `-Os` (.su) | + + +Deserialize for `CyclicCounter` is contextual (`TryDeserializeAnd*`); the Advance probe covers that path’s stack class. + +--- + +## Desktop comparison (optional) + +Desktop GCC / Clang / MSVC host objects are useful for relative trends only. Do **not** mix them into the ESP32-C6 tables above, and do not convert desktop nanoseconds into ESP cycles. Prefer regenerating host sizes with the same `minimal_*.cpp` sources if a comparison table is needed for a PR. diff --git a/docs/footprint_results.json b/docs/footprint_results.json new file mode 100644 index 0000000..094084d --- /dev/null +++ b/docs/footprint_results.json @@ -0,0 +1,398 @@ +{ + "toolchain": "riscv32-esp-elf-g++ (esp-14.2.0_20241119)", + "target": "ESP32-C6 / riscv32 ilp32", + "segmented": { + "Os": { + "empty": { + "text": 60, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 60, + "ram": 0, + "helpers64": [] + }, + "rssi": { + "text": 964, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 964, + "ram": 0, + "helpers64": [] + }, + "temperature": { + "text": 5126, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 5334, + "ram": 0, + "helpers64": [] + }, + "humidity": { + "text": 2962, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 2962, + "ram": 0, + "helpers64": [] + }, + "co2": { + "text": 3658, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 3866, + "ram": 0, + "helpers64": [] + }, + "rx": { + "text": 6596, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 6804, + "ram": 0, + "helpers64": [] + }, + "battery": { + "text": 4398, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 4606, + "ram": 0, + "helpers64": [] + }, + "connect": { + "text": 3068, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 3276, + "ram": 0, + "helpers64": [] + }, + "thermometer": { + "text": 9124, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 9332, + "ram": 0, + "helpers64": [] + }, + "all": { + "text": 12080, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 12288, + "ram": 0, + "helpers64": [] + } + }, + "O2": { + "empty": { + "text": 60, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 60, + "ram": 0, + "helpers64": [] + }, + "rssi": { + "text": 1136, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 1136, + "ram": 0, + "helpers64": [] + }, + "temperature": { + "text": 6050, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 6258, + "ram": 0, + "helpers64": [] + }, + "humidity": { + "text": 5800, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 5800, + "ram": 0, + "helpers64": [] + }, + "co2": { + "text": 3890, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 4098, + "ram": 0, + "helpers64": [] + }, + "rx": { + "text": 8536, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 8744, + "ram": 0, + "helpers64": [] + }, + "battery": { + "text": 4252, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 4460, + "ram": 0, + "helpers64": [] + }, + "connect": { + "text": 2786, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 2994, + "ram": 0, + "helpers64": [] + }, + "thermometer": { + "text": 14868, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 15076, + "ram": 0, + "helpers64": [] + }, + "all": { + "text": 19838, + "rodata": 208, + "data": 0, + "bss": 0, + "flash": 20046, + "ram": 0, + "helpers64": [] + } + } + }, + "cyclic": { + "Os": { + "cyclic_u8_u16": { + "text": 224, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 224, + "ram": 0, + "helpers64": [] + }, + "cyclic_u8_u32": { + "text": 182, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 182, + "ram": 0, + "helpers64": [] + }, + "cyclic_u16_u32": { + "text": 136, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 136, + "ram": 0, + "helpers64": [] + } + }, + "O2": { + "cyclic_u8_u16": { + "text": 244, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 244, + "ram": 0, + "helpers64": [] + }, + "cyclic_u8_u32": { + "text": 188, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 188, + "ram": 0, + "helpers64": [] + }, + "cyclic_u16_u32": { + "text": 136, + "rodata": 0, + "data": 0, + "bss": 0, + "flash": 136, + "ram": 0, + "helpers64": [] + } + } + }, + "stack_usage": { + "temperature": { + "TieredInt]": 0, + "MulU32Wide(uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "DivU32Wide(uint32_t, uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "MulDivU32Nearest(uint32_t, uint32_t, uint32_t, uint32_t&)": 32, + "ShlU32WideChecked(uint32_t&, uint32_t&, unsigned int)": 0, + "ShrU32Wide(uint32_t&, uint32_t&, unsigned int, bool)": 0, + "CmpMulU32(uint32_t, uint32_t, uint32_t, uint32_t)": 32, + "FromRuntimeInteger(int64_t) [with Rep = long int; auto Max = 32]": 32, + "MakeRawFromLogicalRuntime32(int32_t, int32_t) [with Rep = long unsigned int; auto Max = 86400; bool kIsSigned = false]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long int]": 0, + "SaturatingSub32(RV, RV, RV, RV) [with RV = long int]": 0, + "FixedPoint]": 16, + "SaturatingAdd32(RV, RV, RV, RV) [with RV = long int]": 0, + "FromWideProduct32(uint32_t, uint32_t, bool, int) [with ResultRep = long int; auto ResultMax = 86400]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long unsigned int]": 0, + "FixedPoint]": 32, + "FixedPoint]": 16, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "Segmented32MathPolicy]": 48, + "Exp2PosMinusOne(SegLog)": 16, + "GeomUnitWeightPos(SegLog, int, int)": 32, + "uint32_t TestMeta()": 16, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "SegLog, int, int, bool)": 64, + "FixedPoint, 1>]": 32, + "FixedPoint, 0>]": 32, + "FixedPoint, 2>]": 32, + "FixedPoint]": 48, + "uint32_t TestDecode(uint32_t)": 32, + "FixedPoint; int I = 0]": 32, + "FixedPoint; int I = 1]": 48, + "FixedPoint; int I = 2]": 32, + "Bytes<2> > > > >]": 32, + "uint32_t TestEncode(uint32_t)": 32, + "size_t TestSerialize(uint32_t, uint8_t*)": 32 + }, + "co2": { + "TieredInt]": 0, + "MulU32Wide(uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "DivU32Wide(uint32_t, uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "ShrU32Wide(uint32_t&, uint32_t&, unsigned int, bool)": 0, + "FromRuntimeInteger(int64_t) [with Rep = long int; auto Max = 32]": 32, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long unsigned int]": 0, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long int]": 0, + "FixedPoint]": 16, + "FixedPoint]": 16, + "Segmented32MathPolicy]": 48, + "SegLog)": 32, + "uint32_t TestMeta()": 16, + "FixedPoint]": 48, + "FixedPoint, 0>]": 32, + "FixedPoint, 1>]": 32, + "FixedPoint, 2>]": 32, + "uint32_t TestDecode(uint32_t)": 32, + "FixedPoint; int I = 0]": 32, + "FixedPoint; int I = 1]": 32, + "FixedPoint; int I = 2]": 32, + "OptimizeCuts> > >]": 32, + "uint32_t TestEncode(uint32_t)": 32, + "size_t TestSerialize(uint32_t, uint8_t*)": 32 + }, + "rx": { + "TieredInt]": 0, + "MulU32Checked(uint32_t, uint32_t, uint32_t&)": 0, + "MulU32Wide(uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "DivU32Wide(uint32_t, uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "MulDivU32Nearest(uint32_t, uint32_t, uint32_t, uint32_t&)": 32, + "ShlU32WideChecked(uint32_t&, uint32_t&, unsigned int)": 0, + "ShrU32Wide(uint32_t&, uint32_t&, unsigned int, bool)": 0, + "CmpMulU32(uint32_t, uint32_t, uint32_t, uint32_t)": 32, + "FromRuntimeInteger(int64_t) [with Rep = long int; auto Max = 32]": 32, + "MakeRawFromLogicalRuntime32(int32_t, int32_t) [with Rep = long unsigned int; auto Max = 86400; bool kIsSigned = false]": 32, + "SegLog)": 32, + "LinearShapeMag(int, int32_t, int32_t)": 32, + "DiscWide(uint32_t, uint32_t, uint32_t, bool, uint32_t&, uint32_t&)": 48, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long unsigned int]": 0, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long int]": 0, + "SaturatingAdd32(RV, RV, RV, RV) [with RV = long int]": 0, + "FixedPoint]": 32, + "SaturatingSub32(RV, RV, RV, RV) [with RV = long int]": 0, + "FixedPoint]": 16, + "FromWideProduct32(uint32_t, uint32_t, bool, int) [with ResultRep = long int; auto ResultMax = 86400]": 32, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "FixedPoint]": 32, + "Segmented32MathPolicy]": 48, + "Exp2PosMinusOne(SegLog)": 16, + "SegLog, int, int, bool)": 64, + "uint32_t TestMeta()": 16, + "AvoidEndpointCollision(StoredRaw, StoredRaw, StoredRaw, int, int) [with bool kSigned = false]": 0, + "FixedPoint, 0>]": 32, + "FixedPoint; int I = 0]": 32, + "FixedPoint, 1>]": 32, + "FixedPoint; int I = 1]": 32, + "FixedPoint, 2>]": 32, + "LinearApproxAt(StoredRaw, StoredRaw, int, StoredRaw) [with bool kSigned = false]": 32, + "FixedPoint; int I = 2]": 32, + "FixedPoint, 3>]": 48, + "uint32_t TestDecode(uint32_t)": 48, + "LinearRampApproxRuntime(StoredRaw, StoredRaw, int32_t, int32_t, int, StoredRaw) [with bool kSigned = false]": 112, + "FixedPoint; int I = 3]": 32, + "Bytes<4> > > > >]": 32, + "uint32_t TestEncode(uint32_t)": 32, + "size_t TestSerialize(uint32_t, uint8_t*)": 32 + }, + "battery": { + "uint32_t AeFpWireToU32(const Wire&) [with Wire = unsigned char]": 0, + "MulU32Wide(uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "DivU32Wide(uint32_t, uint32_t, uint32_t, uint32_t&, uint32_t&)": 0, + "MulDivU32Nearest(uint32_t, uint32_t, uint32_t, uint32_t&)": 32, + "ShlU32WideChecked(uint32_t&, uint32_t&, unsigned int)": 0, + "ShrU32Wide(uint32_t&, uint32_t&, unsigned int, bool)": 0, + "CmpMulU32(uint32_t, uint32_t, uint32_t, uint32_t)": 32, + "FromRuntimeInteger(int64_t) [with Rep = long int; auto Max = 32]": 32, + "MakeRawFromLogicalRuntime32(int32_t, int32_t) [with Rep = long unsigned int; auto Max = 86400; bool kIsSigned = false]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long int]": 0, + "SaturatingSub32(RV, RV, RV, RV) [with RV = long int]": 0, + "FixedPoint]": 16, + "SaturatingAdd32(RV, RV, RV, RV) [with RV = long int]": 0, + "FromWideProduct32(uint32_t, uint32_t, bool, int) [with ResultRep = long int; auto ResultMax = 86400]": 32, + "ConvertRawScale(RepValue, int, int, RepValue, RepValue) [with RepValue = long unsigned int]": 0, + "FixedPoint]": 32, + "FixedPoint]": 16, + "uint32_t TestMeta()": 16, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "Segmented32MathPolicy]": 48, + "Exp2PosMinusOne(SegLog)": 16, + "DivTo(L, R) [with Target = FixedPoint; L = FixedPoint; R = FixedPoint]": 32, + "Wire AeFpU32ToWire(uint32_t) [with Wire = unsigned char]": 0, + "SegLog, int, int, bool)": 64, + "FixedPoint, 0>]": 32, + "FixedPoint; int I = 0]": 32, + "FixedPoint, 1>]": 32, + "FixedPoint]": 48, + "uint32_t TestDecode(uint32_t)": 32, + "FixedPoint; int I = 1]": 48, + "Bytes<1> > > > >]": 32, + "uint32_t TestEncode(uint32_t)": 32, + "size_t TestSerialize(uint32_t, uint8_t*)": 32 + }, + "cyclic_u8_u32": { + "uint32_t TestRestore(uint32_t, uint32_t)": 16, + "uint32_t TestAdvance(uint32_t, uint32_t)": 32 + } + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b3a0e8d..1583427 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,7 +17,7 @@ cmake_minimum_required(VERSION 3.16) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -project(test-ae-numeric LANGUAGES CXX) +project(test-ae-numeric LANGUAGES C CXX) include(../cmake/CPM.cmake) @@ -27,12 +27,70 @@ if (CMAKE_COLOR_DIAGNOSTICS) add_compile_definitions(UNITY_OUTPUT_COLOR=1) endif() CPMAddPackage("https://github.com/ThrowTheSwitch/Unity.git#master") +if (TARGET unity) + set_target_properties(unity PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON) + target_compile_options(unity PRIVATE + $<$:-w> + $<$:-w> + $<$:/w> + ) +endif() option(AE_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer for tests" OFF) option(AE_ENABLE_COVERAGE "Build a separate gcov target for integer_math (AE_COVERAGE_BUILD)" OFF) +# clang-cl reports CMAKE_CXX_COMPILER_ID=Clang but MSVC=1. Do not pass GNU +# -fconstexpr-* to clang-cl; it rejects them as unknown arguments. +# Plain clang++ (including the VS LLVM clang++ driver) is GNU-style for +# -Wall/-Werror but still rejects GCC-only -fconstexpr-ops-limit / +# -fconstexpr-loop-limit; use -fconstexpr-steps there. +set(AE_GNU_STYLE_CXX FALSE) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR + (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT MSVC)) + set(AE_GNU_STYLE_CXX TRUE) +endif() + +function(ae_numeric_cxx_warnings_and_constexpr tgt) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + target_compile_options(${tgt} PRIVATE + -Wall -Wextra -Werror + -fconstexpr-ops-limit=268435456 + -fconstexpr-loop-limit=1048576) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT MSVC) + target_compile_options(${tgt} PRIVATE + -Wall -Wextra -Werror + -fconstexpr-steps=268435456) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # clang-cl rejects GNU -fconstexpr-* and does not honor MSVC + # /constexpr:steps. Raise the budget via -Xclang. + target_compile_options(${tgt} PRIVATE + /W4 /WX /wd4530 /wd4702 /wd4127 + "SHELL:-Xclang -fconstexpr-steps=268435456") + elseif(MSVC) + target_compile_options(${tgt} PRIVATE + /W4 /WX /FS /Zm200 /constexpr:steps2147483647 /wd4530 /wd4702 /wd4127) + endif() +endfunction() + +function(ae_numeric_constexpr_only tgt) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + target_compile_options(${tgt} PRIVATE + -fconstexpr-ops-limit=268435456 + -fconstexpr-loop-limit=1048576) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT MSVC) + target_compile_options(${tgt} PRIVATE -fconstexpr-steps=268435456) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(${tgt} PRIVATE "SHELL:-Xclang -fconstexpr-steps=268435456") + elseif(MSVC) + target_compile_options(${tgt} PRIVATE /constexpr:steps2147483647) + endif() +endfunction() + add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE main.cpp @@ -54,7 +112,7 @@ target_sources(${PROJECT_NAME} PRIVATE test-segmented-number-core.cpp test-segmented-number-formats.cpp test-segmented-number-wire.cpp - test-segmented-number-formula-lookup.cpp + test-segmented-number-schema.cpp test-segmented-number-floating-runtime.cpp test-segmented-number-size.cpp test-fixed-math.cpp @@ -64,17 +122,11 @@ target_sources(${PROJECT_NAME} PRIVATE ) target_link_libraries(${PROJECT_NAME} PRIVATE ae-numeric unity) - -# enable warnings and werror -target_compile_options(${PROJECT_NAME} PRIVATE - $<$: -Wall -Wextra -Werror> - $<$: -Wall -Wextra -Werror -fconstexpr-ops-limit=268435456 -fconstexpr-loop-limit=1048576> - $<$:/W4 /WX /MP /constexpr:steps100000000 /wd4530 /wd4702 /wd4127> -) +ae_numeric_cxx_warnings_and_constexpr(${PROJECT_NAME}) if(AE_ENABLE_UBSAN) - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + if(AE_GNU_STYLE_CXX) # Prefer trap-on-error so MinGW builds do not require libubsan. target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=undefined -fsanitize-undefined-trap-on-error) @@ -86,7 +138,7 @@ if(AE_ENABLE_UBSAN) endif() if(AE_ENABLE_COVERAGE) - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + if(AE_GNU_STYLE_CXX) # Full suite gets gcov arcs (constexpr helpers stay constexpr here). target_compile_options(${PROJECT_NAME} PRIVATE --coverage -O0 -g) target_link_options(${PROJECT_NAME} PRIVATE --coverage) @@ -99,9 +151,7 @@ if(AE_ENABLE_COVERAGE) target_link_libraries(test-integer-math-coverage PRIVATE ae-numeric unity) target_compile_definitions(test-integer-math-coverage PRIVATE AE_COVERAGE_BUILD=1) target_compile_options(test-integer-math-coverage PRIVATE - --coverage -O0 -g - $<$: -Wall -Wextra -Werror> - $<$: -Wall -Wextra -Werror>) + --coverage -O0 -g -Wall -Wextra -Werror) target_link_options(test-integer-math-coverage PRIVATE --coverage) add_test(NAME test-integer-math-coverage COMMAND $) @@ -137,6 +187,7 @@ foreach(fail_case add_executable(fail-${fail_case} EXCLUDE_FROM_ALL compile-fail/${fail_case}.cpp) target_link_libraries(fail-${fail_case} PRIVATE ae-numeric) + ae_numeric_constexpr_only(fail-${fail_case}) add_test(NAME fail-${fail_case} COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target fail-${fail_case} --config $) @@ -147,16 +198,14 @@ function(ae_numeric_add_footprint tgt) add_executable(${tgt} EXCLUDE_FROM_ALL footprint/segmented_footprint.cpp) target_link_libraries(${tgt} PRIVATE ae-numeric) target_compile_definitions(${tgt} PRIVATE ${ARGN}) - target_compile_options(${tgt} PRIVATE - $<$: -Wall -Wextra -Werror> - $<$: -Wall -Wextra -Werror -fconstexpr-ops-limit=268435456 -fconstexpr-loop-limit=1048576 -ffunction-sections -fdata-sections> - $<$:/W4 /WX /MP /constexpr:steps100000000 /Gy /wd4530 /wd4702 /wd4127> - ) - target_link_options(${tgt} PRIVATE - $<$:-Wl,--gc-sections> - $<$:-Wl,--gc-sections> - $<$:/OPT:REF> - ) + ae_numeric_cxx_warnings_and_constexpr(${tgt}) + if(AE_GNU_STYLE_CXX) + target_compile_options(${tgt} PRIVATE -ffunction-sections -fdata-sections) + target_link_options(${tgt} PRIVATE -Wl,--gc-sections) + elseif(MSVC) + target_compile_options(${tgt} PRIVATE /Gy) + target_link_options(${tgt} PRIVATE /OPT:REF) + endif() endfunction() ae_numeric_add_footprint(footprint-rssi AE_SEG_HAS_RSSI=1) @@ -167,9 +216,6 @@ ae_numeric_add_footprint(footprint-rx-window AE_SEG_HAS_RX=1) ae_numeric_add_footprint(footprint-battery AE_SEG_HAS_BAT=1) ae_numeric_add_footprint(footprint-connect-duration AE_SEG_HAS_CONN=1) ae_numeric_add_footprint(footprint-all-formula AE_SEG_FOOTPRINT_ALL=1) -ae_numeric_add_footprint(footprint-rssi-lookup AE_SEG_HAS_RSSI_LOOKUP=1) -ae_numeric_add_footprint(footprint-temperature-lookup AE_SEG_HAS_TEMP_LOOKUP=1) -ae_numeric_add_footprint(footprint-all-lookup AE_SEG_FOOTPRINT_ALL_LOOKUP=1) add_custom_target(segmented-footprint DEPENDS footprint-rssi @@ -180,29 +226,84 @@ add_custom_target(segmented-footprint DEPENDS footprint-battery footprint-connect-duration footprint-all-formula - footprint-rssi-lookup - footprint-temperature-lookup - footprint-all-lookup ) -# Minimal object-only CyclicCounter footprint probes (no CRT link). -function(ae_numeric_add_cyclic_fp_obj tgt src) +# Object-only library incremental measurement: no CRT, no Unity, no iostream. +function(ae_numeric_add_footprint_obj tgt src) add_library(${tgt} OBJECT EXCLUDE_FROM_ALL ${src}) target_link_libraries(${tgt} PRIVATE ae-numeric) - target_include_directories(${tgt} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/footprint) - target_compile_options(${tgt} PRIVATE - $<$: -Wall -Wextra -Werror -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti> - $<$: -Wall -Wextra -Werror -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti> - $<$:/W4 /WX /Gy /GR- /EHs-c- /wd4530> - ) + ae_numeric_cxx_warnings_and_constexpr(${tgt}) + if(AE_GNU_STYLE_CXX) + target_compile_options(${tgt} PRIVATE + -ffunction-sections -fdata-sections + -fno-exceptions -fno-rtti) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # clang-cl + target_compile_options(${tgt} PRIVATE /Gy /GR- /EHs-c-) + elseif(MSVC) + target_compile_options(${tgt} PRIVATE /Gy /GR- /EHs-c-) + endif() endfunction() -ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u8-u16 footprint/minimal_cyclic_u8_u16.cpp) -ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u8-u32 footprint/minimal_cyclic_u8_u32.cpp) -ae_numeric_add_cyclic_fp_obj(fp-obj-cyclic-u16-u32 footprint/minimal_cyclic_u16_u32.cpp) +ae_numeric_add_footprint_obj(fp-obj-rssi footprint/minimal_rssi.cpp) +ae_numeric_add_footprint_obj(fp-obj-temperature footprint/minimal_temperature.cpp) +ae_numeric_add_footprint_obj(fp-obj-humidity footprint/minimal_humidity.cpp) +ae_numeric_add_footprint_obj(fp-obj-co2 footprint/minimal_co2.cpp) +ae_numeric_add_footprint_obj(fp-obj-rx footprint/minimal_rx.cpp) +ae_numeric_add_footprint_obj(fp-obj-battery footprint/minimal_battery.cpp) +ae_numeric_add_footprint_obj(fp-obj-connect footprint/minimal_connect.cpp) +ae_numeric_add_footprint_obj(fp-obj-all footprint/minimal_all.cpp) +ae_numeric_add_footprint_obj(fp-obj-empty footprint/minimal_empty.cpp) +ae_numeric_add_footprint_obj(fp-obj-uniform footprint/minimal_uniform.cpp) +ae_numeric_add_footprint_obj(fp-obj-ramp footprint/minimal_ramp.cpp) +ae_numeric_add_footprint_obj(fp-obj-exponential footprint/minimal_exponential.cpp) +ae_numeric_add_footprint_obj(fp-obj-geometric footprint/minimal_geometric.cpp) +ae_numeric_add_footprint_obj(fp-obj-tiered footprint/minimal_tiered.cpp) +ae_numeric_add_footprint_obj(fp-obj-combined footprint/minimal_combined.cpp) +ae_numeric_add_footprint_obj(fp-obj-cyclic-u8-u16 footprint/minimal_cyclic_u8_u16.cpp) +ae_numeric_add_footprint_obj(fp-obj-cyclic-u8-u32 footprint/minimal_cyclic_u8_u32.cpp) +ae_numeric_add_footprint_obj(fp-obj-cyclic-u16-u32 footprint/minimal_cyclic_u16_u32.cpp) +ae_numeric_add_footprint_obj(fp-obj-thermometer footprint/minimal_thermometer.cpp) + +add_executable(fp-dump-layout EXCLUDE_FROM_ALL footprint/dump_layout.cpp) +target_link_libraries(fp-dump-layout PRIVATE ae-numeric) +ae_numeric_cxx_warnings_and_constexpr(fp-dump-layout) + +add_executable(fp-dump-meta EXCLUDE_FROM_ALL footprint/dump_meta.cpp) +target_link_libraries(fp-dump-meta PRIVATE ae-numeric) +ae_numeric_cxx_warnings_and_constexpr(fp-dump-meta) + +add_custom_target(segmented-footprint-obj DEPENDS + fp-obj-rssi + fp-obj-temperature + fp-obj-humidity + fp-obj-co2 + fp-obj-rx + fp-obj-battery + fp-obj-connect + fp-obj-all + fp-obj-thermometer + fp-obj-empty + fp-obj-uniform + fp-obj-ramp + fp-obj-exponential + fp-obj-geometric + fp-obj-tiered + fp-obj-combined + fp-obj-cyclic-u8-u16 + fp-obj-cyclic-u8-u32 + fp-obj-cyclic-u16-u32 + fp-dump-layout + fp-dump-meta +) add_custom_target(cyclic-counter-footprint-obj DEPENDS fp-obj-cyclic-u8-u16 fp-obj-cyclic-u8-u32 fp-obj-cyclic-u16-u32 ) + +add_executable(numeric-bench EXCLUDE_FROM_ALL benchmark/numeric_bench.cpp) +target_link_libraries(numeric-bench PRIVATE ae-numeric) +target_include_directories(numeric-bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +ae_numeric_cxx_warnings_and_constexpr(numeric-bench) diff --git a/tests/benchmark/numeric_bench.cpp b/tests/benchmark/numeric_bench.cpp new file mode 100644 index 0000000..2c37807 --- /dev/null +++ b/tests/benchmark/numeric_bench.cpp @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Host micro-benchmark for SegmentedNumber / CyclicCounter (desktop cycles). + * Prints machine-readable lines; not ESP32-C6 cycle counts. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../segmented_test_formats.h" + +namespace { + +using Clock = std::chrono::steady_clock; + +template +double NsPerOp(F&& fn, std::uint64_t iters) { + for (std::uint64_t i = 0; i < iters / 20 + 1; ++i) { + fn(); + } + auto const t0 = Clock::now(); + for (std::uint64_t i = 0; i < iters; ++i) { + fn(); + } + auto const t1 = Clock::now(); + double const ns = + std::chrono::duration(t1 - t0).count(); + return ns / static_cast(iters); +} + +template +typename Num::runtime_type MakeRuntimeFromInt(std::int64_t logical) { + using RT = typename Num::runtime_type; + return ae::runtime_numeric_traits::FromInteger(logical); +} + +template +void BenchSeg(char const* name, std::int64_t logical_sample, std::uint64_t iters) { + using namespace ae; + volatile std::uint32_t sink = 0; + volatile std::int64_t logical_v = logical_sample; + std::uint8_t buf[16] = {}; + + auto const rt0 = MakeRuntimeFromInt(logical_sample); + auto const wire_opt = Num::TryEncode(rt0); + if (!wire_opt) { + std::printf("BENCH %s SKIP no_encode logical=%lld\n", name, + static_cast(logical_sample)); + return; + } + auto const wire = *wire_opt; + auto const n0 = Num::Saturating(rt0); + auto const packed = wire_traits::Serialize(n0, buf); + + double const enc = NsPerOp( + [&] { + auto const rt = MakeRuntimeFromInt(logical_v); + auto w = Num::TryEncode(rt); + sink ^= w ? 1u : 0u; + }, + iters); + double const dec = NsPerOp( + [&] { + auto v = Num::TryDecode(wire); + sink ^= v ? 1u : 0u; + }, + iters); + double const ser = NsPerOp( + [&] { + auto const rt = MakeRuntimeFromInt(logical_v); + Num n = Num::Saturating(rt); + sink ^= static_cast( + wire_traits::Serialize(n, buf)); + }, + iters); + double const deser = NsPerOp( + [&] { + auto r = wire_traits::Deserialize(buf, sizeof(buf)); + sink ^= static_cast(r.bytes_read); + }, + iters); + double const rt_ns = NsPerOp( + [&] { + auto const rt = MakeRuntimeFromInt(logical_v); + Num n = Num::Saturating(rt); + auto const nw = wire_traits::Serialize(n, buf); + auto r = wire_traits::Deserialize(buf, nw); + sink ^= static_cast(r.bytes_read); + }, + iters); + std::printf( + "BENCH_SEG name=%s logical=%lld wire_bytes=%zu encode_ns=%.3f decode_ns=%.3f " + "serialize_ns=%.3f deserialize_ns=%.3f roundtrip_ns=%.3f sink=%u\n", + name, static_cast(logical_sample), packed, enc, dec, ser, deser, + rt_ns, sink); +} + +void BenchCyclic(std::uint64_t iters) { + using C = ae::CyclicCounter; + C c{1001u}; + volatile std::uint32_t sink = 0; + std::uint8_t buf[1] = {237u}; + + double const wire = NsPerOp( + [&] { sink ^= c.WireValue(); }, iters); + double const fwd = NsPerOp( + [&] { + auto r = c.TryRestore(237u); + sink ^= r ? *r : 0u; + }, + iters); + C c2{1008u}; + double const back = NsPerOp( + [&] { + auto r = c2.TryRestore(235u); + sink ^= r ? *r : 0u; + }, + iters); + C c3{1001u}; + double const adv = NsPerOp( + [&] { + C tmp{1001u}; + auto r = tmp.TryAdvance(237u); + sink ^= r ? *r : 0u; + }, + iters); + C c4{1001u}; + double const ctx = NsPerOp( + [&] { + C tmp{1001u}; + auto r = tmp.TryDeserializeAndAdvance(buf, 1); + sink ^= r.ok() ? r.value : 0u; + }, + iters); + std::printf( + "BENCH_CYC wire_ns=%.3f restore_fwd_ns=%.3f restore_back_ns=%.3f " + "advance_ns=%.3f ctx_deser_ns=%.3f sink=%u\n", + wire, fwd, back, adv, ctx, sink); +} + +} // namespace + +int main() { + using namespace ae::test_segmented_formats; + constexpr std::uint64_t kIters = 200000; + + std::printf("BENCH_HOST arch=desktop note=nanoseconds_not_esp32_cycles " + "iters=%llu\n", + static_cast(kIters)); + + // logical samples chosen to exercise central / tail / tier regimes + BenchSeg("Rssi", -40, kIters); + BenchSeg("Temperature_center", 25, kIters); + BenchSeg("Temperature_low", -35, kIters); + BenchSeg("Temperature_high", 100, kIters); + BenchSeg("Humidity", 55, kIters); + BenchSeg("Co2_1B", 600, kIters); + BenchSeg("Co2_2B", 2500, kIters); + BenchSeg("Co2_4B", 18000, kIters); + BenchSeg("Rx_1B", 1, kIters); + BenchSeg("Rx_2B", 120, kIters); + BenchSeg("Rx_4B", 7200, kIters); + BenchSeg("Battery", 3, kIters); + BenchSeg("ConnectDuration", 5, kIters); + BenchCyclic(kIters); + return 0; +} diff --git a/tests/footprint/component_formats.h b/tests/footprint/component_formats.h new file mode 100644 index 0000000..2335461 --- /dev/null +++ b/tests/footprint/component_formats.h @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_TESTS_FOOTPRINT_COMPONENT_FORMATS_H_ +#define AE_NUMERIC_TESTS_FOOTPRINT_COMPONENT_FORMATS_H_ + +#include + +#include +#include + +namespace ae::fp_component { + +template +using D = Decimal; + +using Uniform = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<0>>, seg::Step>, + seg::Place>>>>>; + +using Ramp = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<20>>, seg::Intervals<45>, + seg::StepAtUpper, + seg::Place>>>>>; + +using Exponential = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<2>>, seg::Intervals<102>, + seg::Place>>>>>; + +using Geometric = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<275, -2>>, seg::Intervals<130>, + seg::StepAtUpper>, seg::Place>>>>>; + +using Tiered = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::UniformStep, D<200>>, seg::Step>, + seg::Place>>, + seg::UniformStep, D<400>>, seg::Step>, + seg::Place>>>>>; + +using Combined = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout< + seg::LinearStepRamp, D<20>>, seg::Intervals<45>, + seg::StepAtUpper, + seg::Place>>, + seg::UniformValues, D<80>>, seg::Intervals<168>, + seg::Place>>, + seg::LinearStepRamp, D<100>>, seg::Intervals<42>, + seg::StepAtLower, + seg::Place>>>>>; + +} // namespace ae::fp_component + +#endif // AE_NUMERIC_TESTS_FOOTPRINT_COMPONENT_FORMATS_H_ diff --git a/tests/footprint/dump_layout.cpp b/tests/footprint/dump_layout.cpp new file mode 100644 index 0000000..2d4807a --- /dev/null +++ b/tests/footprint/dump_layout.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include "../segmented_test_formats.h" + +using CS = ae::seg::segmented_compiler_internal::CompiledSegment; + +#define AE_FP_FIELD(name) \ + std::printf(" %-22s sizeof=%zu offset=%zu\n", #name, sizeof(CS::name), \ + offsetof(CS, name)) + +int main() { + std::printf("sizeof(CompiledSegment)=%zu alignof=%zu\n", sizeof(CS), + alignof(CS)); + AE_FP_FIELD(physical_begin_raw); + AE_FP_FIELD(physical_end_raw); + AE_FP_FIELD(wire_code_begin); + AE_FP_FIELD(code_count); + AE_FP_FIELD(curve_kind); + AE_FP_FIELD(wire_bytes); + AE_FP_FIELD(intervals); + AE_FP_FIELD(math_first); + AE_FP_FIELD(curve_begin_raw); + AE_FP_FIELD(curve_end_raw); + AE_FP_FIELD(step0_raw); + AE_FP_FIELD(last_step_raw); + AE_FP_FIELD(delta_raw); + AE_FP_FIELD(log2_r); + AE_FP_FIELD(log2_q); + AE_FP_FIELD(log2_begin); + AE_FP_FIELD(log2_end); + AE_FP_FIELD(from_upper); + + using namespace ae::test_segmented_formats; + std::printf("\n"); + std::printf("Rssi sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(Rssi::kSegments), Rssi::kSegmentCount, + Rssi::kFormulaCoefficientBytes); + std::printf("Temperature sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(Temperature::kSegments), Temperature::kSegmentCount, + Temperature::kFormulaCoefficientBytes); + std::printf("Humidity sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(Humidity::kSegments), Humidity::kSegmentCount, + Humidity::kFormulaCoefficientBytes); + std::printf("Co2 sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(Co2::kSegments), Co2::kSegmentCount, + Co2::kFormulaCoefficientBytes); + std::printf("RxWindow sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(RxWindow::kSegments), RxWindow::kSegmentCount, + RxWindow::kFormulaCoefficientBytes); + std::printf("Battery sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(Battery::kSegments), Battery::kSegmentCount, + Battery::kFormulaCoefficientBytes); + std::printf("Connect sizeof(kSegments)=%zu count=%zu formula_bytes=%zu\n", + sizeof(ConnectDuration::kSegments), + ConnectDuration::kSegmentCount, + ConnectDuration::kFormulaCoefficientBytes); + return 0; +} diff --git a/tests/footprint/dump_meta.cpp b/tests/footprint/dump_meta.cpp new file mode 100644 index 0000000..a5bbf96 --- /dev/null +++ b/tests/footprint/dump_meta.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "../segmented_test_formats.h" + +namespace { + +template +void PrintSeg(char const* name) { + using RT = typename Num::runtime_type; + using WT = typename Num::wire_type; + std::printf( + "SEG %s code_count=%zu segments=%zu sizeof_runtime=%zu sizeof_number=%zu " + "sizeof_wire=%zu max_wire_bytes=%zu formula_coeff_bytes=%zu\n", + name, Num::kCodeCount, Num::kSegmentCount, sizeof(RT), sizeof(Num), + sizeof(WT), Num::kMaxWireBytes, Num::kFormulaCoefficientBytes); +} + +template +void PrintCyclic(char const* name) { + using C = ae::CyclicCounter; + std::printf( + "CYC %s sizeof=%zu wire_bytes=%zu value_bytes=%zu half_range=%u " + "wire_space=%u\n", + name, sizeof(C), sizeof(Wire), sizeof(Value), + static_cast(C::kHalfRange), + static_cast(C::kWireSpace)); +} + +} // namespace + +int main() { + using CS = ae::seg::segmented_compiler_internal::CompiledSegment; + std::printf("META sizeof_CompiledSegment=%zu alignof_CompiledSegment=%zu\n", + sizeof(CS), alignof(CS)); + + using namespace ae::test_segmented_formats; + PrintSeg("Rssi"); + PrintSeg("Temperature"); + PrintSeg("Humidity"); + PrintSeg("Co2"); + PrintSeg("RxWindow"); + PrintSeg("Battery"); + PrintSeg("ConnectDuration"); + + PrintCyclic("u8_u16"); + PrintCyclic("u8_u32"); + PrintCyclic("u16_u32"); + return 0; +} diff --git a/tests/footprint/minimal_all.cpp b/tests/footprint/minimal_all.cpp new file mode 100644 index 0000000..f057012 --- /dev/null +++ b/tests/footprint/minimal_all.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +#include "../segmented_test_formats.h" + +namespace { + +template +AE_FP_NOINLINE std::uint32_t WireToU32(Wire const& w) { + if constexpr (std::is_same_v) { + return static_cast(w); + } else { + return static_cast(static_cast(w)); + } +} + +template +AE_FP_NOINLINE std::uint32_t EncodeOne(std::uint32_t raw) { + using RT = typename Num::runtime_type; + volatile std::uint32_t in = raw; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + auto const w = Num::TryEncode(v); + if (!w.has_value()) { + return 0xFFFFFFFFu; + } + return WireToU32(*w); +} + +} // namespace + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestEncodeAll( + std::uint32_t raw) { + std::uint32_t s = 0; + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + return s; +} diff --git a/tests/footprint/minimal_battery.cpp b/tests/footprint/minimal_battery.cpp new file mode 100644 index 0000000..56ff9eb --- /dev/null +++ b/tests/footprint/minimal_battery.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::Battery +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_body.inc b/tests/footprint/minimal_body.inc new file mode 100644 index 0000000..805ff20 --- /dev/null +++ b/tests/footprint/minimal_body.inc @@ -0,0 +1,70 @@ +// Instantiated by each minimal_*.cpp after defining AE_FP_NUM. +// Object-only: no main, no printf, no Unity. + +#include "ae_fp_common.h" + +#include + +using Num = AE_FP_NUM; +using RT = typename Num::runtime_type; + +template +AE_FP_NOINLINE std::uint32_t AeFpWireToU32(Wire const& w) { + if constexpr (std::is_same_v) { + return static_cast(w); + } else { + return static_cast(static_cast(w)); + } +} + +template +AE_FP_NOINLINE Wire AeFpU32ToWire(std::uint32_t code) { + if constexpr (std::is_same_v) { + return static_cast(code); + } else { + return Wire{code}; + } +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestEncode( + std::uint32_t raw) { + volatile std::uint32_t in = raw; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + auto const w = Num::TryEncode(v); + if (!w.has_value()) { + return 0xFFFFFFFFu; + } + return AeFpWireToU32(*w); +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestDecode( + std::uint32_t code) { + volatile std::uint32_t in = code; + auto const w = AeFpU32ToWire(in); + auto const d = Num::TryDecode(w); + if (!d.has_value()) { + return 0xFFFFFFFFu; + } + return static_cast(d->RawValue()); +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::size_t TestSerialize( + std::uint32_t raw, std::uint8_t* out) { + volatile std::uint32_t in = raw; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + auto const n = Num::Saturating(v); + return Num::Serialize(n, out); +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestMeta() { + volatile std::size_t sink = 0; + sink += Num::kCodeCount; + sink += Num::kSegmentCount; + sink += Num::kFormulaCoefficientBytes; + sink += sizeof(Num::kSegments); + return static_cast(sink); +} diff --git a/tests/footprint/minimal_co2.cpp b/tests/footprint/minimal_co2.cpp new file mode 100644 index 0000000..f126665 --- /dev/null +++ b/tests/footprint/minimal_co2.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::Co2 +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_combined.cpp b/tests/footprint/minimal_combined.cpp new file mode 100644 index 0000000..628fae1 --- /dev/null +++ b/tests/footprint/minimal_combined.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Combined +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_connect.cpp b/tests/footprint/minimal_connect.cpp new file mode 100644 index 0000000..b0b7037 --- /dev/null +++ b/tests/footprint/minimal_connect.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::ConnectDuration +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_empty.cpp b/tests/footprint/minimal_empty.cpp new file mode 100644 index 0000000..0cbfdcf --- /dev/null +++ b/tests/footprint/minimal_empty.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +using RT = ae::FixedPoint; + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestEncode( + std::uint32_t raw) { + volatile std::uint32_t in = raw; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + return static_cast(v.RawValue()); +} + +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestDecode( + std::uint32_t code) { + volatile std::uint32_t in = code; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + return static_cast(v.RawValue()); +} diff --git a/tests/footprint/minimal_exponential.cpp b/tests/footprint/minimal_exponential.cpp new file mode 100644 index 0000000..1ff98c8 --- /dev/null +++ b/tests/footprint/minimal_exponential.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Exponential +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_geometric.cpp b/tests/footprint/minimal_geometric.cpp new file mode 100644 index 0000000..ae1af3c --- /dev/null +++ b/tests/footprint/minimal_geometric.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Geometric +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_humidity.cpp b/tests/footprint/minimal_humidity.cpp new file mode 100644 index 0000000..769b85b --- /dev/null +++ b/tests/footprint/minimal_humidity.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::Humidity +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_ramp.cpp b/tests/footprint/minimal_ramp.cpp new file mode 100644 index 0000000..2a83b29 --- /dev/null +++ b/tests/footprint/minimal_ramp.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Ramp +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_rssi.cpp b/tests/footprint/minimal_rssi.cpp new file mode 100644 index 0000000..837ec5d --- /dev/null +++ b/tests/footprint/minimal_rssi.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::Rssi +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_rx.cpp b/tests/footprint/minimal_rx.cpp new file mode 100644 index 0000000..4298b1d --- /dev/null +++ b/tests/footprint/minimal_rx.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::RxWindow +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_temperature.cpp b/tests/footprint/minimal_temperature.cpp new file mode 100644 index 0000000..6a7365a --- /dev/null +++ b/tests/footprint/minimal_temperature.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "../segmented_test_formats.h" +#define AE_FP_NUM ae::test_segmented_formats::Temperature +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_thermometer.cpp b/tests/footprint/minimal_thermometer.cpp new file mode 100644 index 0000000..646a1fa --- /dev/null +++ b/tests/footprint/minimal_thermometer.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ae_fp_common.h" + +#include + +#include + +#include "../segmented_test_formats.h" + +namespace { + +template +AE_FP_NOINLINE std::uint32_t WireToU32(Wire const& w) { + if constexpr (std::is_same_v) { + return static_cast(w); + } else { + return static_cast(static_cast(w)); + } +} + +template +AE_FP_NOINLINE std::uint32_t EncodeOne(std::uint32_t raw) { + using RT = typename Num::runtime_type; + volatile std::uint32_t in = raw; + auto const v = RT::FromRaw( + RT::ClampRaw(static_cast( + static_cast(in)))); + auto const w = Num::TryEncode(v); + if (!w.has_value()) { + return 0xFFFFFFFFu; + } + return WireToU32(*w); +} + +} // namespace + +// Thermometer = Temperature + Humidity + CO2 + Battery (shared Log2/Exp2). +extern "C" AE_FP_NOINLINE AE_FP_USED std::uint32_t TestEncodeThermometer( + std::uint32_t raw) { + std::uint32_t s = 0; + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + s ^= EncodeOne(raw); + return s; +} diff --git a/tests/footprint/minimal_tiered.cpp b/tests/footprint/minimal_tiered.cpp new file mode 100644 index 0000000..84ade97 --- /dev/null +++ b/tests/footprint/minimal_tiered.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Tiered +#include "minimal_body.inc" diff --git a/tests/footprint/minimal_uniform.cpp b/tests/footprint/minimal_uniform.cpp new file mode 100644 index 0000000..4883c8a --- /dev/null +++ b/tests/footprint/minimal_uniform.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "component_formats.h" +#define AE_FP_NUM ae::fp_component::Uniform +#include "minimal_body.inc" diff --git a/tests/footprint/segmented_footprint.cpp b/tests/footprint/segmented_footprint.cpp index e8fbe57..e152dba 100644 --- a/tests/footprint/segmented_footprint.cpp +++ b/tests/footprint/segmented_footprint.cpp @@ -44,7 +44,6 @@ void SinkOne(char const* name) { sink += Num::kMaxWireBytes; sink += Num::kSegmentCount; sink += Num::kFormulaCoefficientBytes; - sink += Num::kLookupTableBytes; auto const n = Num::Saturating(Num::runtime_type::FromInteger(1)); std::uint8_t buf[8] = {}; sink += Num::Serialize(n, buf); @@ -52,11 +51,10 @@ void SinkOne(char const* name) { sink += back.bytes_read; std::printf( "%s sizeof(runtime)=%zu sizeof(number)=%zu sizeof(wire)=%zu " - "codes=%zu max_bytes=%zu segs=%zu formula_bytes=%zu lookup_bytes=%zu " - "sink=%zu\n", + "codes=%zu max_bytes=%zu segs=%zu formula_bytes=%zu sink=%zu\n", name, sizeof(typename Num::runtime_type), sizeof(Num), sizeof(typename Num::wire_type), Num::kCodeCount, Num::kMaxWireBytes, - Num::kSegmentCount, Num::kFormulaCoefficientBytes, Num::kLookupTableBytes, + Num::kSegmentCount, Num::kFormulaCoefficientBytes, static_cast(sink)); } @@ -83,18 +81,6 @@ int main() { #endif #if defined(AE_SEG_HAS_CONN) SinkOne("connect-duration"); -#endif -#if defined(AE_SEG_HAS_RSSI_LOOKUP) || defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) - SinkOne("rssi-lookup"); -#endif -#if defined(AE_SEG_HAS_TEMP_LOOKUP) || defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) - SinkOne("temperature-lookup"); -#endif -#if defined(AE_SEG_FOOTPRINT_ALL_LOOKUP) - SinkOne("humidity-lookup"); - SinkOne("co2-lookup"); - SinkOne("battery-lookup"); - SinkOne("connect-lookup"); #endif return 0; } diff --git a/tests/main.cpp b/tests/main.cpp index 6c32ba1..0a191ce 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -37,7 +37,7 @@ extern int test_composed_exponential_tiered(); extern int test_segmented_number_core(); extern int test_segmented_number_formats(); extern int test_segmented_number_wire(); -extern int test_segmented_number_formula_lookup(); +extern int test_segmented_number_schema(); extern int test_segmented_number_floating_runtime(); extern int test_segmented_number_size(); extern int test_fixed_math(); @@ -65,7 +65,7 @@ int main() { res += test_segmented_number_core(); res += test_segmented_number_formats(); res += test_segmented_number_wire(); - res += test_segmented_number_formula_lookup(); + res += test_segmented_number_schema(); res += test_segmented_number_floating_runtime(); res += test_segmented_number_size(); res += test_fixed_math(); diff --git a/tests/segmented_reference_math.h b/tests/segmented_reference_math.h new file mode 100644 index 0000000..ec2921a --- /dev/null +++ b/tests/segmented_reference_math.h @@ -0,0 +1,143 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AE_NUMERIC_TESTS_SEGMENTED_REFERENCE_MATH_H_ +#define AE_NUMERIC_TESTS_SEGMENTED_REFERENCE_MATH_H_ + +#include +#include + +namespace ae::test_segmented_reference { + +inline long double RefExpRatio(long double begin, long double end, int n) { + if (begin <= 0.0L || end <= 0.0L || n <= 0) { + return 1.0L; + } + return std::pow(end / begin, 1.0L / static_cast(n)); +} + +inline long double RefGeomSum(long double q, int n) { + if (n <= 0) { + return 0.0L; + } + if (std::fabsl(q - 1.0L) < 1.0e-18L) { + return static_cast(n); + } + return (std::pow(q, static_cast(n)) - 1.0L) / (q - 1.0L); +} + +inline long double RefExpValue(long double begin, long double end, int n, + int i) { + if (i <= 0) { + return begin; + } + if (i >= n) { + return end; + } + long double const r = RefExpRatio(begin, end, n); + return begin * std::pow(r, static_cast(i)); +} + +inline long double RefGeomValue(long double begin, long double end, + long double step0, long double q, int n, + int i, bool from_upper, long double last_step) { + if (i <= 0) { + return begin; + } + if (i >= n) { + return end; + } + if (from_upper) { + return end - last_step * RefGeomSum(q, n - i); + } + return begin + step0 * RefGeomSum(q, i); +} + +inline long double RefLinearValue(long double begin, long double step0, + long double delta, int n, int i) { + if (i <= 0) { + return begin; + } + if (i >= n) { + return begin + static_cast(n) * step0 + + delta * static_cast(n) * + static_cast(n - 1) / 2.0L; + } + return begin + static_cast(i) * step0 + + delta * static_cast(i) * + static_cast(i - 1) / 2.0L; +} + +inline long double RefRelQuantError(long double r) { + return std::sqrt(r) - 1.0L; +} + +inline long double RefSolveGeomQ(int n, long double sum) { + if (n <= 0 || sum <= static_cast(n)) { + return 1.0L; + } + long double lo = 1.0L; + long double hi = 1.5L; + while (RefGeomSum(hi, n) < sum && hi < 8.0L) { + hi *= 2.0L; + } + for (int i = 0; i < 80; ++i) { + long double const mid = 0.5L * (lo + hi); + if (RefGeomSum(mid, n) < sum) { + lo = mid; + } else { + hi = mid; + } + } + return 0.5L * (lo + hi); +} + +inline int RefAutoSplitN1(long double begin, long double mid, long double end, + int total) { + int best_n1 = 1; + long double best_jump = 1.0e300L; + long double best_err = 1.0e300L; + for (int n1 = 1; n1 < total; ++n1) { + int const n2 = total - n1; + long double const r1 = RefExpRatio(begin, mid, n1); + long double const r2 = RefExpRatio(mid, end, n2); + long double const step_before = mid - mid / r1; + long double const step_after = mid * r2 - mid; + long double const smaller = + step_before < step_after ? step_before : step_after; + if (smaller <= 0.0L) { + continue; + } + long double const jump = + std::fabsl(step_after - step_before) / smaller; + long double const err1 = RefRelQuantError(r1); + long double const err2 = RefRelQuantError(r2); + long double const err = err1 > err2 ? err1 : err2; + bool const better = jump < best_jump || + (jump == best_jump && err < best_err) || + (jump == best_jump && err == best_err && n1 < best_n1); + if (better) { + best_jump = jump; + best_err = err; + best_n1 = n1; + } + } + return best_n1; +} + +} // namespace ae::test_segmented_reference + +#endif // AE_NUMERIC_TESTS_SEGMENTED_REFERENCE_MATH_H_ diff --git a/tests/segmented_test_formats.h b/tests/segmented_test_formats.h index da6bc40..81e002d 100644 --- a/tests/segmented_test_formats.h +++ b/tests/segmented_test_formats.h @@ -18,6 +18,7 @@ #define AE_NUMERIC_TESTS_SEGMENTED_TEST_FORMATS_H_ #include +#include #include #include @@ -121,49 +122,6 @@ using ConnectDurationSpec = seg::Format< seg::ExponentialValues, D<60>>>>>>; using ConnectDuration = seg::Compile; -using TemperatureLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, - typename TemperatureSpec::layout_type>; -using TemperatureLookup = seg::Compile; - -using RssiLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename RssiSpec::layout_type>; -using RssiLookup = seg::Compile; - -using HumidityLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename HumiditySpec::layout_type>; -using HumidityLookup = seg::Compile; - -using Co2LookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename Co2Spec::layout_type>; -using Co2Lookup = seg::Compile; - -using BatteryLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename BatteryVoltageSpec::layout_type>; -using BatteryLookup = seg::Compile; - -using ConnectLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename ConnectDurationSpec::layout_type>; -using ConnectLookup = seg::Compile; - -using RxWindowLookupSpec = seg::Format< - seg::runtime::Fixed, - seg::wire::AutoTiered>, - seg::compute::Lookup, typename RxWindowSpec::layout_type>; -using RxWindowLookup = seg::Compile; - template typename Num::wire_type WireFromRank(std::uint32_t rank) { if constexpr (std::is_same_v) { diff --git a/tests/test-fixed-math.cpp b/tests/test-fixed-math.cpp index a3e828f..e71647f 100644 --- a/tests/test-fixed-math.cpp +++ b/tests/test-fixed-math.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -83,6 +84,8 @@ void test_Exp2FractionalValues() { } void test_RoundTrip() { + using HP = fixed_math::HighPrecisionFixedMathPolicy; + using HPLog = HP::log_type; constexpr Runtime values[] = {Runtime{1}, Runtime{2}, Runtime{3}, Runtime{5}, Runtime{10}, Runtime{30}, Runtime{60}}; @@ -90,8 +93,8 @@ void test_RoundTrip() { 200000, 2500000, 5000000}; for (std::size_t i = 0; i < sizeof(values) / sizeof(values[0]); ++i) { const Runtime x = values[i]; - const Log lx = fixed_math::Log2To(x); - const Runtime y = fixed_math::Exp2To(lx); + const HPLog lx = fixed_math::Log2To(x); + const Runtime y = fixed_math::Exp2To(lx); TEST_ASSERT(NearRaw(y, x, tolerances[i])); } } @@ -104,12 +107,59 @@ void test_SmallRuntimeRoundTrip() { void test_FromClampedRawClampsLargeValue() { using Target = FixedPoint; - constexpr auto clamped = - fixed_math::internal::from_clamped_raw(std::int64_t{1} << 40); + constexpr auto clamped = fixed_math::internal::from_clamped_i32( + std::numeric_limits::max()); TEST_ASSERT_EQUAL(static_cast(Target::kRawMax), static_cast(clamped.RawValue())); } +using HP = fixed_math::HighPrecisionFixedMathPolicy; +using Near = FixedPoint; +using HPLog = HP::log_type; + +void test_SqrtOfFour() { + TEST_ASSERT(NearRaw(fixed_math::Sqrt(Runtime{4}), Runtime{2}, 8)); +} + +void test_ValuesNearOne() { + Near const samples[] = {Near::FromRatio(1001, 1000), + Near::FromRatio(1005, 1000), + Near::FromRatio(101, 100), + Near::FromRatio(103, 100)}; + for (Near const x : samples) { + HPLog const lx = fixed_math::Log2To(x); + Near const y = fixed_math::Exp2To(lx); + TEST_ASSERT(NearRaw(y, x, 64)); + Near const sq = fixed_math::PowIntTo(x, 2); + Near const back = fixed_math::NthRoot(sq, 2); + TEST_ASSERT(NearRaw(back, x, 256)); + } +} + +void test_PowIntNearOne() { + Near const q = Near::FromRatio(1001957, 1000000); + Near const p = fixed_math::PowIntTo(q, 349); + TEST_ASSERT(p > Near::FromRuntimeInteger(1)); + TEST_ASSERT(p < Near::FromRuntimeInteger(3)); +} + +void test_PowIntExp2Agreement() { + Near const base = Near::FromRatio(103, 100); + Near const p4 = fixed_math::PowIntTo(base, 4); + HPLog const l = fixed_math::ScaleLogByInt( + fixed_math::Log2To(base), 4); + Near const e = fixed_math::Exp2To(l); + TEST_ASSERT(NearRaw(p4, e, 64)); +} + +void test_HpRoundTrip380() { + using W = FixedPoint; + W const x = W::FromInteger(380); + HPLog const lx = fixed_math::Log2To(x); + W const y = fixed_math::Exp2To(lx); + TEST_ASSERT(NearRaw(y, x, 1024)); +} + } // namespace ae::test_fixed_math int test_fixed_math() { @@ -121,5 +171,10 @@ int test_fixed_math() { RUN_TEST(ae::test_fixed_math::test_RoundTrip); RUN_TEST(ae::test_fixed_math::test_SmallRuntimeRoundTrip); RUN_TEST(ae::test_fixed_math::test_FromClampedRawClampsLargeValue); + RUN_TEST(ae::test_fixed_math::test_SqrtOfFour); + RUN_TEST(ae::test_fixed_math::test_ValuesNearOne); + RUN_TEST(ae::test_fixed_math::test_PowIntNearOne); + RUN_TEST(ae::test_fixed_math::test_PowIntExp2Agreement); + RUN_TEST(ae::test_fixed_math::test_HpRoundTrip380); return UNITY_END(); } diff --git a/tests/test-integer-math.cpp b/tests/test-integer-math.cpp index 3a61a19..630708c 100644 --- a/tests/test-integer-math.cpp +++ b/tests/test-integer-math.cpp @@ -473,6 +473,74 @@ void test_SqrtU64() { TEST_ASSERT_EQUAL_UINT(4294967295ULL, f.sqrt_u64(~0ULL)); } +void test_MulU32WideMatchesU64() { + auto check = [](std::uint32_t a, std::uint32_t b) { + std::uint32_t hi = 0; + std::uint32_t lo = 0; + MulU32Wide(a, b, hi, lo); + std::uint64_t const prod = + static_cast(a) * static_cast(b); + TEST_ASSERT_EQUAL_UINT32(static_cast(prod), lo); + TEST_ASSERT_EQUAL_UINT32(static_cast(prod >> 32), hi); + }; + check(0, 0); + check(1, 1); + check(65535, 65535); + check(65536, 65536); + check(0xFFFFFFFFu, 0xFFFFFFFFu); + check(123456789u, 987654321u); +} + +void test_MulDivU32NearestVsU64() { + auto check = [](std::uint32_t a, std::uint32_t f, std::uint32_t d) { + std::uint32_t out32 = 0; + std::uint64_t out64 = 0; + bool const ok32 = MulDivU32Nearest(a, f, d, out32); + bool const ok64 = MulDivU64Nearest(a, f, d, out64); + TEST_ASSERT_EQUAL(ok64, ok32); + if (ok32 && out64 <= std::numeric_limits::max()) { + TEST_ASSERT_EQUAL_UINT32(static_cast(out64), out32); + } + }; + check(0, 5, 3); + check(7, 9, 2); + check(1000, 1000, 7); + check(0x80000000u, 3, 2); + check(12345, 67890, 111); +} + +void test_SqrtU32MatchesU64() { + TEST_ASSERT_EQUAL_UINT32(0U, SqrtU32(0)); + TEST_ASSERT_EQUAL_UINT32(1U, SqrtU32(1)); + TEST_ASSERT_EQUAL_UINT32(10U, SqrtU32(100)); + TEST_ASSERT_EQUAL_UINT32(65535U, SqrtU32(4294836225u)); + for (std::uint32_t n = 0; n < 4096; ++n) { + TEST_ASSERT_EQUAL_UINT32(static_cast(SqrtU64(n)), SqrtU32(n)); + } + TEST_ASSERT_EQUAL_UINT32(static_cast(SqrtU64(0xFFFFFFFFu)), + SqrtU32(0xFFFFFFFFu)); +} + +void test_SqrtU32WideMatchesU64() { + auto check = [](std::uint64_t n) { + std::uint32_t const hi = static_cast(n >> 32U); + std::uint32_t const lo = static_cast(n); + TEST_ASSERT_EQUAL_UINT32(static_cast(SqrtU64(n)), + SqrtU32Wide(hi, lo)); + }; + check(0); + check(1); + check(100); + check(0xFFFFFFFFull); + check(0x100000000ull); + check(0x123456789Aull); + check(0xFFFFFFFF00000000ull); + check(0xFFFFFFFFFFFFFFFFull); + for (std::uint32_t i = 0; i < 256; ++i) { + check((static_cast(i) << 32U) | (i * 65537U)); + } +} + } // namespace ae::test_integer_math int test_integer_math() { @@ -498,5 +566,9 @@ int test_integer_math() { RUN_TEST(ae::test_integer_math::test_RawFromRatioOverflowButFitsAndBounds); RUN_TEST(ae::test_integer_math::test_RawFromRatioExhaustiveSmall); RUN_TEST(ae::test_integer_math::test_SqrtU64); + RUN_TEST(ae::test_integer_math::test_MulU32WideMatchesU64); + RUN_TEST(ae::test_integer_math::test_MulDivU32NearestVsU64); + RUN_TEST(ae::test_integer_math::test_SqrtU32MatchesU64); + RUN_TEST(ae::test_integer_math::test_SqrtU32WideMatchesU64); return UNITY_END(); } diff --git a/tests/test-segmented-number-core.cpp b/tests/test-segmented-number-core.cpp index 834de04..6c994b3 100644 --- a/tests/test-segmented-number-core.cpp +++ b/tests/test-segmented-number-core.cpp @@ -61,7 +61,6 @@ static_assert(sizeof(Rssi) == sizeof(Rssi::runtime_type)); static_assert(Rssi::kCodeCount == 128); static_assert(std::is_same_v); static_assert(Rssi::kMaxWireBytes == 1); -static_assert(Rssi::kLookupTableBytes == 0); static_assert(sizeof(Temperature) == sizeof(Temperature::runtime_type)); static_assert(Temperature::kCodeCount == 1021); static_assert(Temperature::kOneByteCount == 253); @@ -120,10 +119,10 @@ void test_TemperatureGolden() { TEST_ASSERT_EQUAL_UINT(2U, Temperature::kMaxWireBytes); auto const& p = Temperature::Logical(); TEST_ASSERT_EQUAL(3, p.count); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0019571403660685, p.segs[0].q); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0032825748092233, p.segs[2].q); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.1974705407, p.segs[0].step0); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.3934835786, p.segs[2].last_step); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-6, 1.0019571403660685, AsDouble(p.segs[0].q)); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-6, 1.0032825748092233, AsDouble(p.segs[2].q)); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-4, 0.1974705407, AsDouble(p.segs[0].step0)); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-4, 0.3934835786, AsDouble(p.segs[2].last_step)); } void test_TemperatureEndpointsAndWire() { @@ -164,8 +163,8 @@ void test_TemperatureQuantization() { }; TEST_ASSERT(max_err(9.9, 10.1, 40) <= 0.06); TEST_ASSERT(max_err(35.1, 35.3, 40) <= 0.06); - TEST_ASSERT(max_err(-40.0, -39.7, 40) <= 0.11); - TEST_ASSERT(max_err(124.6, 125.0, 40) <= 0.22); + TEST_ASSERT(max_err(-40.0, -39.7, 40) <= 0.105); + TEST_ASSERT(max_err(124.6, 125.0, 40) <= 0.20); } void test_TemperatureRoundTrip() { CheckRankRoundTrip(); } diff --git a/tests/test-segmented-number-formats.cpp b/tests/test-segmented-number-formats.cpp index de48eb3..555831b 100644 --- a/tests/test-segmented-number-formats.cpp +++ b/tests/test-segmented-number-formats.cpp @@ -21,9 +21,11 @@ #include #include +#include #include #include +#include "segmented_reference_math.h" #include "segmented_test_formats.h" namespace ae::test_segmented_number_formats { @@ -99,9 +101,9 @@ void test_HumidityGolden() { TEST_ASSERT_EQUAL(45, p.segs[0].intervals); TEST_ASSERT_EQUAL(168, p.segs[1].intervals); TEST_ASSERT_EQUAL(42, p.segs[2].intervals); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-8, 60.0 / 168.0, p.segs[1].step0); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.5317460317, p.segs[0].step0); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 0.5952380952, p.segs[2].last_step); + TEST_ASSERT_DOUBLE_WITHIN(2.0e-4, 60.0 / 168.0, AsDouble(p.segs[1].step0)); + TEST_ASSERT_DOUBLE_WITHIN(2.0e-4, 0.5317460317, AsDouble(p.segs[0].step0)); + TEST_ASSERT_DOUBLE_WITHIN(2.0e-4, 0.5952380952, AsDouble(p.segs[2].last_step)); } void test_HumidityErrors() { @@ -135,7 +137,7 @@ void test_Co2Golden() { TEST_ASSERT_EQUAL(821, p.segs[0].intervals); TEST_ASSERT_EQUAL(254, p.segs[0].last_1); TEST_ASSERT_EQUAL(477, p.segs[0].last_2); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.005414508222845, p.segs[0].r); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.005414508222845, AsDouble(p.segs[0].r)); } void test_Co2DecodedCuts() { @@ -160,9 +162,15 @@ void test_RxWindowGolden() { TEST_ASSERT_EQUAL_UINT(255U, RxWindow::kTwoByteCount); TEST_ASSERT_EQUAL_UINT(3046U, RxWindow::kCodeCount); TEST_ASSERT_EQUAL_UINT(4U, RxWindow::kMaxWireBytes); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0355033664891309, p.segs[0].r); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0341296978352505, p.segs[1].r); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.012401106168161, p.segs[2].q); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.0355033664891309, AsDouble(p.segs[0].r)); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.0341296978352505, AsDouble(p.segs[1].r)); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-5, 1.012401106168161, AsDouble(p.segs[2].q)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 2.0, AsDouble(p.segs[2].step0)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 45.0, AsDouble(p.segs[2].last_step)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 45.0, AsDouble(p.segs[3].step0)); + TEST_ASSERT_DOUBLE_WITHIN(2.0, 20.0, AsDouble(p.segs[3].last_step)); + TEST_ASSERT_EQUAL( + 132, test_segmented_reference::RefAutoSplitN1(0.01L, 1.0L, 60.0L, 254)); } void test_BatteryGolden() { @@ -170,8 +178,8 @@ void test_BatteryGolden() { auto const& p = Battery::Logical(); TEST_ASSERT_EQUAL(130, p.segs[0].intervals); TEST_ASSERT_EQUAL(125, p.segs[1].intervals); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0116049124714404, p.segs[0].q); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-7, 0.0088601265, p.segs[0].step0); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-6, 1.0116049124714404, AsDouble(p.segs[0].q)); + TEST_ASSERT_DOUBLE_WITHIN(5.0e-4, 0.0088601265, AsDouble(p.segs[0].step0)); } void test_ConnectDurationGolden() { @@ -179,36 +187,13 @@ void test_ConnectDurationGolden() { auto const& p = ConnectDuration::Logical(); TEST_ASSERT_EQUAL(102, p.segs[0].intervals); TEST_ASSERT_EQUAL(153, p.segs[1].intervals); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0228310927967654, p.segs[0].r); - TEST_ASSERT_DOUBLE_WITHIN(1.0e-9, 1.0224789769119687, p.segs[1].r); -} - -void test_RoundTripAllFormats() { - CheckRankRoundTrip(); - CheckRankRoundTrip(); - CheckRankRoundTrip(); - CheckRankRoundTrip(); - CheckRankRoundTrip(); -} - -void test_UniqueAndSerializeSmall() { - CheckUniqueRaws(); - CheckUniqueRaws(); - CheckUniqueRaws(); - CheckUniqueRaws(); - CheckSerializeRoundTrip(); - CheckSerializeRoundTrip(); - CheckSerializeRoundTrip(); -} - -void test_Co2UniqueAndSerialize() { - CheckUniqueRaws(); - CheckSerializeRoundTrip(); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.0228310927967654, AsDouble(p.segs[0].r)); + TEST_ASSERT_DOUBLE_WITHIN(1.0e-6, 1.0224789769119687, AsDouble(p.segs[1].r)); } template double MaxAbsErrorRatio(std::int64_t den, std::int64_t n0, std::int64_t n1, - std::int64_t step) { + std::int64_t step) { double m = 0.0; for (std::int64_t n = n0; n <= n1; n += step) { auto const v = Num::runtime_type::FromRatio(n, den); @@ -222,13 +207,41 @@ double MaxAbsErrorRatio(std::int64_t den, std::int64_t n0, std::int64_t n1, return m; } -void test_DenseSampling() { - TEST_ASSERT(MaxAbsErrorRatio(100, -4000, 12500, 1) <= 0.22); +void test_HumidityRoundTrip() { CheckRankRoundTrip(); } +void test_Co2RoundTrip() { CheckRankRoundTrip(); } +void test_BatteryRoundTrip() { CheckRankRoundTrip(); } +void test_ConnectRoundTrip() { CheckRankRoundTrip(); } +void test_RxWindowRoundTrip() { CheckRankRoundTrip(); } + +void test_HumidityUnique() { CheckUniqueRaws(); } +void test_BatteryUnique() { CheckUniqueRaws(); } +void test_ConnectUnique() { CheckUniqueRaws(); } +void test_RxWindowUnique() { CheckUniqueRaws(); } +void test_Co2Unique() { CheckUniqueRaws(); } + +void test_HumiditySerialize() { CheckSerializeRoundTrip(); } +void test_BatterySerialize() { CheckSerializeRoundTrip(); } +void test_ConnectSerialize() { CheckSerializeRoundTrip(); } +void test_Co2Serialize() { CheckSerializeRoundTrip(); } + +void test_DenseTemperature() { + TEST_ASSERT(MaxAbsErrorRatio(100, -4000, 12500, 1) <= 0.20); +} +void test_DenseHumidity() { TEST_ASSERT(MaxAbsErrorRatio(100, 0, 10000, 1) <= 0.32); +} +void test_DenseRssi() { TEST_ASSERT(MaxAbsErrorRatio(10, -1270, 0, 1) <= 0.51); +} +void test_DenseBattery() { TEST_ASSERT(MaxAbsErrorRatio(10000, 21500, 30000, 1) <= 0.005); +} +void test_DenseConnect() { TEST_ASSERT(MaxAbsErrorRatio(1000, 200, 60000, 1) <= 0.70); +} +void test_DenseCo2() { double co2_rel = 0.0; + double co2_rel_hi = 0.0; for (int ppm = 380; ppm <= 32000; ++ppm) { auto const v = Co2::runtime_type::FromInteger(ppm); auto const n = Co2::TryFromRuntime(v); @@ -237,10 +250,71 @@ void test_DenseSampling() { double const rel = std::fabs(got - static_cast(ppm)) / static_cast(ppm); co2_rel = std::max(co2_rel, rel); + if (ppm >= 2000) { + co2_rel_hi = std::max(co2_rel_hi, rel); + } } - TEST_ASSERT(co2_rel <= 0.0035); + TEST_ASSERT(co2_rel_hi <= 0.00280); + TEST_ASSERT(co2_rel <= 0.00320); +} +void test_DenseRxWindow() { TEST_ASSERT(MaxAbsErrorRatio(1, 1, 86400, 1) <= 23.0); - TEST_ASSERT(MaxAbsErrorRatio(1, 86000, 86400, 1) <= 12.0); +} +void test_DenseRxWindow24h() { + // Last linear step is 20 s; the 10.01 s bound applies at the 24 h edge, + // not 400 s earlier where the ramp step is still slightly larger. + TEST_ASSERT(MaxAbsErrorRatio(1, 86380, 86400, 1) <= 10.01); +} + +void test_ConnectJumpAndReference() { + auto const& p = ConnectDuration::Logical(); + double const a = AsDouble(p.segs[0].last_step); + double const b = AsDouble(p.segs[1].step0); + TEST_ASSERT(a > 0.0); + TEST_ASSERT(b > 0.0); + double const jump = std::fabs(a - b) / (a < b ? a : b); + TEST_ASSERT(jump < 0.01); + TEST_ASSERT_EQUAL( + 102, test_segmented_reference::RefAutoSplitN1(0.2L, 2.0L, 60.0L, 255)); +} + +void test_TemperatureMatchesReferenceQ() { + double const q0 = AsDouble(Temperature::Logical().segs[0].q); + TEST_ASSERT_DOUBLE_WITHIN( + 5.0e-6, static_cast(test_segmented_reference::RefSolveGeomQ(349, 500.0L)), + q0); +} + +void test_Co2MatchesReferenceR() { + double const r = AsDouble(Co2::Logical().segs[0].r); + TEST_ASSERT_DOUBLE_WITHIN( + 5.0e-6, + static_cast(test_segmented_reference::RefExpRatio(380.0L, 32000.0L, 821)), + r); +} + +void test_RampIndexError() { + int const hum_codes = + ae::seg::segmented_formula_internal::MaxRampIndexError< + Humidity::spec_type, Humidity::logical_type>(); + int const hum_dense = + ae::seg::segmented_formula_internal::MaxRampIndexErrorDenseInputs< + Humidity::spec_type, Humidity::logical_type>(); + int const rx_codes = ae::seg::segmented_formula_internal::MaxRampIndexError< + RxWindow::spec_type, RxWindow::logical_type>(); + int const rx_dense = + ae::seg::segmented_formula_internal::MaxRampIndexErrorDenseInputs< + RxWindow::spec_type, RxWindow::logical_type>(); + int const temp = ae::seg::segmented_formula_internal::MaxRampIndexError< + Temperature::spec_type, Temperature::logical_type>(); + int const co2 = ae::seg::segmented_formula_internal::MaxRampIndexError< + Co2::spec_type, Co2::logical_type>(); + TEST_ASSERT_LESS_OR_EQUAL_INT(3, hum_codes); + TEST_ASSERT_LESS_OR_EQUAL_INT(3, rx_codes); + TEST_ASSERT_LESS_OR_EQUAL_INT(3, hum_dense); + TEST_ASSERT_LESS_OR_EQUAL_INT(3, rx_dense); + TEST_ASSERT_EQUAL_INT(0, temp); + TEST_ASSERT_EQUAL_INT(0, co2); } } // namespace ae::test_segmented_number_formats @@ -254,9 +328,31 @@ int test_segmented_number_formats() { RUN_TEST(ae::test_segmented_number_formats::test_RxWindowGolden); RUN_TEST(ae::test_segmented_number_formats::test_BatteryGolden); RUN_TEST(ae::test_segmented_number_formats::test_ConnectDurationGolden); - RUN_TEST(ae::test_segmented_number_formats::test_RoundTripAllFormats); - RUN_TEST(ae::test_segmented_number_formats::test_UniqueAndSerializeSmall); - RUN_TEST(ae::test_segmented_number_formats::test_Co2UniqueAndSerialize); - RUN_TEST(ae::test_segmented_number_formats::test_DenseSampling); + RUN_TEST(ae::test_segmented_number_formats::test_HumidityRoundTrip); + RUN_TEST(ae::test_segmented_number_formats::test_Co2RoundTrip); + RUN_TEST(ae::test_segmented_number_formats::test_BatteryRoundTrip); + RUN_TEST(ae::test_segmented_number_formats::test_ConnectRoundTrip); + RUN_TEST(ae::test_segmented_number_formats::test_RxWindowRoundTrip); + RUN_TEST(ae::test_segmented_number_formats::test_HumidityUnique); + RUN_TEST(ae::test_segmented_number_formats::test_BatteryUnique); + RUN_TEST(ae::test_segmented_number_formats::test_ConnectUnique); + RUN_TEST(ae::test_segmented_number_formats::test_RxWindowUnique); + RUN_TEST(ae::test_segmented_number_formats::test_Co2Unique); + RUN_TEST(ae::test_segmented_number_formats::test_HumiditySerialize); + RUN_TEST(ae::test_segmented_number_formats::test_BatterySerialize); + RUN_TEST(ae::test_segmented_number_formats::test_ConnectSerialize); + RUN_TEST(ae::test_segmented_number_formats::test_Co2Serialize); + RUN_TEST(ae::test_segmented_number_formats::test_ConnectJumpAndReference); + RUN_TEST(ae::test_segmented_number_formats::test_TemperatureMatchesReferenceQ); + RUN_TEST(ae::test_segmented_number_formats::test_Co2MatchesReferenceR); + RUN_TEST(ae::test_segmented_number_formats::test_DenseTemperature); + RUN_TEST(ae::test_segmented_number_formats::test_DenseHumidity); + RUN_TEST(ae::test_segmented_number_formats::test_DenseRssi); + RUN_TEST(ae::test_segmented_number_formats::test_DenseBattery); + RUN_TEST(ae::test_segmented_number_formats::test_DenseConnect); + RUN_TEST(ae::test_segmented_number_formats::test_DenseCo2); + RUN_TEST(ae::test_segmented_number_formats::test_DenseRxWindow); + RUN_TEST(ae::test_segmented_number_formats::test_DenseRxWindow24h); + RUN_TEST(ae::test_segmented_number_formats::test_RampIndexError); return UNITY_END(); } diff --git a/tests/test-segmented-number-formula-lookup.cpp b/tests/test-segmented-number-formula-lookup.cpp deleted file mode 100644 index 57a211a..0000000 --- a/tests/test-segmented-number-formula-lookup.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2026 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include - -#include "segmented_test_formats.h" - -namespace ae::test_segmented_number_formula_lookup { - -using test_segmented_formats::Battery; -using test_segmented_formats::BatteryLookup; -using test_segmented_formats::Co2; -using test_segmented_formats::Co2Lookup; -using test_segmented_formats::ConnectDuration; -using test_segmented_formats::ConnectLookup; -using test_segmented_formats::Humidity; -using test_segmented_formats::HumidityLookup; -using test_segmented_formats::Rssi; -using test_segmented_formats::RssiLookup; -using test_segmented_formats::RxWindow; -using test_segmented_formats::RxWindowLookup; -using test_segmented_formats::Temperature; -using test_segmented_formats::TemperatureLookup; - -static_assert(Rssi::kLookupTableBytes == 0); -static_assert(RssiLookup::kLookupTableBytes > 0); -static_assert(Temperature::kLookupTableBytes == 0); -static_assert(TemperatureLookup::kLookupTableBytes > 0); - -template -void CompareAllRanks() { - TEST_ASSERT_EQUAL_UINT(Formula::kCodeCount, Lookup::kCodeCount); - TEST_ASSERT_EQUAL_UINT(Formula::kOneByteCount, Lookup::kOneByteCount); - TEST_ASSERT_EQUAL_UINT(Formula::kMaxWireBytes, Lookup::kMaxWireBytes); - for (std::uint32_t rank = 0; - rank < static_cast(Formula::kCodeCount); ++rank) { - auto const wf = test_segmented_formats::WireFromRank(rank); - auto const wl = test_segmented_formats::WireFromRank(rank); - auto const df = Formula::Decode(wf); - auto const dl = Lookup::Decode(wl); - TEST_ASSERT_EQUAL(df.RawValue(), dl.RawValue()); - auto const ef = Formula::TryEncode(df); - auto const el = Lookup::TryEncode(dl); - TEST_ASSERT(ef.has_value()); - TEST_ASSERT(el.has_value()); - TEST_ASSERT_EQUAL_UINT(static_cast(*ef), - static_cast(*el)); - } -} - -void test_RssiFormulaLookup() { CompareAllRanks(); } - -void test_HumidityFormulaLookup() { - CompareAllRanks(); -} - -void test_BatteryFormulaLookup() { CompareAllRanks(); } - -void test_ConnectFormulaLookup() { - CompareAllRanks(); -} - -void test_TemperatureFormulaLookup() { - CompareAllRanks(); -} - -void test_Co2FormulaLookup() { CompareAllRanks(); } - -void test_RxWindowFormulaLookup() { - CompareAllRanks(); -} - -} // namespace ae::test_segmented_number_formula_lookup - -int test_segmented_number_formula_lookup() { - UNITY_BEGIN(); - RUN_TEST(ae::test_segmented_number_formula_lookup::test_RssiFormulaLookup); - RUN_TEST( - ae::test_segmented_number_formula_lookup::test_HumidityFormulaLookup); - RUN_TEST(ae::test_segmented_number_formula_lookup::test_BatteryFormulaLookup); - RUN_TEST( - ae::test_segmented_number_formula_lookup::test_ConnectFormulaLookup); - RUN_TEST( - ae::test_segmented_number_formula_lookup::test_TemperatureFormulaLookup); - RUN_TEST(ae::test_segmented_number_formula_lookup::test_Co2FormulaLookup); - RUN_TEST( - ae::test_segmented_number_formula_lookup::test_RxWindowFormulaLookup); - return UNITY_END(); -} diff --git a/tests/test-segmented-number-schema.cpp b/tests/test-segmented-number-schema.cpp new file mode 100644 index 0000000..2a6a418 --- /dev/null +++ b/tests/test-segmented-number-schema.cpp @@ -0,0 +1,125 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include +#include +#include + +#include "segmented_test_formats.h" + +namespace ae::test_segmented_number_schema { + +using test_segmented_formats::Co2; +using test_segmented_formats::Rssi; +using test_segmented_formats::RssiSpec; +using test_segmented_formats::Temperature; +using test_segmented_formats::TemperatureSpec; + +template +using D = ae::Decimal; + +using RssiAgain = seg::Compile; + +using RssiFloatSpec = seg::Format< + seg::runtime::Floating, + seg::wire::AutoTiered>, + seg::compute::Formula, typename RssiSpec::layout_type>; +using RssiFloat = seg::Compile; + +using TemperatureFloatSpec = seg::Format< + seg::runtime::Floating, + seg::wire::AutoTiered>, + seg::compute::Formula, typename TemperatureSpec::layout_type>; +using TemperatureFloat = seg::Compile; + +using RssiFewerCodes = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<0>>, seg::Step>, + seg::Place>>>>>; + +using RssiWiderRange = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<0>>, seg::Step>, + seg::Place>>>>>; + +using Co2AltCuts = seg::Compile, + seg::wire::AutoTiered>, + seg::compute::Formula, + seg::Layout, D<32000>>, seg::ExactEndpoints, + seg::WireCuts, seg::Bytes<1>>, + seg::ApproximateCut, seg::Bytes<2>>, + seg::Rest>>, + seg::OptimizeCuts>>>>; + +static_assert(Rssi::kSchemaHash == RssiAgain::kSchemaHash); +static_assert(Rssi::kSchemaHash == RssiFloat::kSchemaHash); +static_assert(Temperature::kSchemaHash == TemperatureFloat::kSchemaHash); +static_assert(Rssi::kSchemaHash != RssiFewerCodes::kSchemaHash); +static_assert(Rssi::kSchemaHash != RssiWiderRange::kSchemaHash); +static_assert(Co2::kSchemaHash != Co2AltCuts::kSchemaHash); + +void test_SameFormatSameHash() { + TEST_ASSERT_EQUAL_UINT64(Rssi::kSchemaHash, RssiAgain::kSchemaHash); +} + +void test_FixedFloatingSameHash() { + TEST_ASSERT_EQUAL_UINT64(Rssi::kSchemaHash, RssiFloat::kSchemaHash); + TEST_ASSERT_EQUAL_UINT64(Temperature::kSchemaHash, + TemperatureFloat::kSchemaHash); +} + +void test_IntervalChangeChangesHash() { + TEST_ASSERT(Rssi::kSchemaHash != RssiFewerCodes::kSchemaHash); +} + +void test_RangeChangeChangesHash() { + TEST_ASSERT(Rssi::kSchemaHash != RssiWiderRange::kSchemaHash); +} + +void test_WireCutChangeChangesHash() { + TEST_ASSERT(Co2::kSchemaHash != Co2AltCuts::kSchemaHash); +} + +void test_TryDecodeRejectsInvalidRank() { + auto const bad = Rssi::TryDecode(static_cast(200)); + TEST_ASSERT_FALSE(bad.has_value()); + auto const ok = Rssi::TryDecode(static_cast(0)); + TEST_ASSERT(ok.has_value()); +} + +} // namespace ae::test_segmented_number_schema + +int test_segmented_number_schema() { + UNITY_BEGIN(); + RUN_TEST(ae::test_segmented_number_schema::test_SameFormatSameHash); + RUN_TEST(ae::test_segmented_number_schema::test_FixedFloatingSameHash); + RUN_TEST(ae::test_segmented_number_schema::test_IntervalChangeChangesHash); + RUN_TEST(ae::test_segmented_number_schema::test_RangeChangeChangesHash); + RUN_TEST(ae::test_segmented_number_schema::test_WireCutChangeChangesHash); + RUN_TEST(ae::test_segmented_number_schema::test_TryDecodeRejectsInvalidRank); + return UNITY_END(); +} diff --git a/tests/test-segmented-number-size.cpp b/tests/test-segmented-number-size.cpp index dde0e5b..367f80d 100644 --- a/tests/test-segmented-number-size.cpp +++ b/tests/test-segmented-number-size.cpp @@ -29,7 +29,6 @@ using test_segmented_formats::Co2; using test_segmented_formats::ConnectDuration; using test_segmented_formats::Humidity; using test_segmented_formats::Rssi; -using test_segmented_formats::RssiLookup; using test_segmented_formats::RxWindow; using test_segmented_formats::Temperature; @@ -43,15 +42,11 @@ static_assert(sizeof(ConnectDuration) == sizeof(ConnectDuration::runtime_type)); static_assert(MaxWireBytes() == 1); static_assert(MaxWireBytes() == 2); static_assert(MaxWireBytes() == 4); -static_assert(Rssi::kLookupTableBytes == 0); -static_assert(RssiLookup::kLookupTableBytes == - Rssi::kCodeCount * (sizeof(std::int64_t) + sizeof(std::uint32_t))); void test_FootprintConstants() { TEST_ASSERT_EQUAL_UINT(sizeof(Rssi::runtime_type), sizeof(Rssi)); TEST_ASSERT_EQUAL_UINT(1U, sizeof(Rssi::wire_type)); TEST_ASSERT(Temperature::kFormulaCoefficientBytes > 0); - TEST_ASSERT_EQUAL_UINT(0U, Temperature::kLookupTableBytes); TEST_ASSERT_EQUAL_UINT(3U, Temperature::kSegmentCount); TEST_ASSERT_EQUAL_UINT(1U, Rssi::kSegmentCount); } diff --git a/tools/generate_footprint_docs.py b/tools/generate_footprint_docs.py new file mode 100644 index 0000000..4939ee5 --- /dev/null +++ b/tools/generate_footprint_docs.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +# Copyright 2026 Aethernet Inc. +"""Update generated Markdown sections from footprint/benchmark JSON.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +META = { + "rssi": { + "label": "RSSI", + "codes": 128, + "segments": 1, + "sizeof_runtime": 1, + "sizeof_number": 1, + "sizeof_wire": 1, + "max_wire_bytes": 1, + }, + "temperature": { + "label": "Temperature", + "codes": 1021, + "segments": 3, + "sizeof_runtime": 2, + "sizeof_number": 2, + "sizeof_wire": 2, + "max_wire_bytes": 2, + }, + "humidity": { + "label": "Humidity", + "codes": 256, + "segments": 3, + "sizeof_runtime": 2, + "sizeof_number": 2, + "sizeof_wire": 1, + "max_wire_bytes": 1, + }, + "co2": { + "label": "CO2", + "codes": 822, + "segments": 3, + "sizeof_runtime": 2, + "sizeof_number": 2, + "sizeof_wire": 4, + "max_wire_bytes": 4, + }, + "rx": { + "label": "RX window", + "codes": 3046, + "segments": 4, + "sizeof_runtime": 4, + "sizeof_number": 4, + "sizeof_wire": 4, + "max_wire_bytes": 4, + }, + "battery": { + "label": "Battery", + "codes": 256, + "segments": 2, + "sizeof_runtime": 2, + "sizeof_number": 2, + "sizeof_wire": 1, + "max_wire_bytes": 1, + }, + "connect": { + "label": "ConnectDuration", + "codes": 256, + "segments": 2, + "sizeof_runtime": 4, + "sizeof_number": 4, + "sizeof_wire": 1, + "max_wire_bytes": 1, + }, + "thermometer": { + "label": "Thermometer", + "codes": "-", + "segments": "-", + "sizeof_runtime": "-", + "sizeof_number": "-", + "sizeof_wire": "-", + "max_wire_bytes": "-", + }, + "all": { + "label": "All seven", + "codes": "-", + "segments": "-", + "sizeof_runtime": "-", + "sizeof_number": "-", + "sizeof_wire": "-", + "max_wire_bytes": "-", + }, +} + +CYCLIC_META = { + "cyclic_u8_u16": { + "label": "`uint8_t` -> `uint16_t`", + "wire": 1, + "runtime": 2, + "half": 127, + }, + "cyclic_u8_u32": { + "label": "`uint8_t` -> `uint32_t`", + "wire": 1, + "runtime": 4, + "half": 127, + }, + "cyclic_u16_u32": { + "label": "`uint16_t` -> `uint32_t`", + "wire": 2, + "runtime": 4, + "half": 32767, + }, +} + + +def replace_block(text: str, begin: str, end: str, body: str) -> str: + pattern = re.compile( + re.escape(begin) + r".*?" + re.escape(end), + re.DOTALL, + ) + replacement = f"{begin}\n{body.rstrip()}\n{end}" + if not pattern.search(text): + raise SystemExit(f"markers not found: {begin} … {end}") + return pattern.sub(lambda _m: replacement, text, count=1) + + +def seg_table(data: dict, opt: str) -> str: + rows = [ + "| Format | Opt | .text | .rodata | .data | .bss | Flash | RAM | " + "sizeof(runtime) | sizeof(number) | sizeof(wire) | Codes | Segments |", + "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + for key in [ + "rssi", + "temperature", + "humidity", + "co2", + "rx", + "battery", + "connect", + "thermometer", + "all", + ]: + m = META[key] + s = data["segmented"][opt][key] + rows.append( + f"| {m['label']} | -{opt} | {s['text']} | {s['rodata']} | {s['data']} | " + f"{s['bss']} | {s['flash']} | {s['ram']} | {m['sizeof_runtime']} | " + f"{m['sizeof_number']} | {m['sizeof_wire']} | {m['codes']} | {m['segments']} |" + ) + return "\n".join(rows) + + +def cyclic_table(data: dict, opt: str) -> str: + rows = [ + "| Configuration | Wire B | Runtime B | .text | .rodata | .data | .bss | Flash | RAM | Half-range |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + for key, m in CYCLIC_META.items(): + s = data["cyclic"][opt][key] + rows.append( + f"| {m['label']} | {m['wire']} | {m['runtime']} | {s['text']} | " + f"{s['rodata']} | {s['data']} | {s['bss']} | {s['flash']} | {s['ram']} | " + f"{m['half']} |" + ) + return "\n".join(rows) + + +def sharing_table(data: dict, opt: str) -> str: + s = data["segmented"][opt] + t = s["temperature"]["text"] + c = s["co2"]["text"] + h = s["humidity"]["text"] + b = s["battery"]["text"] + thermo = s["thermometer"]["text"] + all7 = s["all"]["text"] + sum_tc = t + c + sum_thcb = t + h + c + b + sum_all = ( + s["rssi"]["text"] + + t + + h + + c + + s["rx"]["text"] + + b + + s["connect"]["text"] + ) + rows = [ + "| Bundle | Standalone .text sum | Combined .text | Saved |", + "|---|---:|---:|---:|", + f"| Temperature + CO2 (standalone sum) | {sum_tc} | (linked separately) | - |", + f"| Thermometer (T+H+CO2+Bat) | {sum_thcb} | {thermo} | {sum_thcb - thermo} |", + f"| All seven | {sum_all} | {all7} | {sum_all - all7} |", + ] + return "\n".join(rows) + + +def stack_table(data: dict) -> str: + su = data.get("stack_usage", {}) + + def max_for(name: str, patterns: list[str]) -> int: + entries = su.get(name, {}) + best = 0 + for fn, nbytes in entries.items(): + for p in patterns: + if p in fn: + best = max(best, int(nbytes)) + return best + + rows = [ + "| Type | Encode | Decode | Serialize | Deserialize | Worst case | Method |", + "|---|---:|---:|---:|---:|---:|---|", + ] + for name, label in [ + ("temperature", "Temperature"), + ("co2", "CO2"), + ("rx", "RX"), + ("battery", "Battery"), + ("cyclic_u8_u32", "CyclicCounter u8->u32"), + ]: + enc = max_for(name, ["TestEncode"]) + dec = max_for(name, ["TestDecode", "TestRestore"]) + ser = max_for(name, ["TestSerialize"]) + deser = max_for(name, ["TestDecode", "TestRestore", "TestAdvance"]) + if name.startswith("cyclic"): + enc = max_for(name, ["TestRestore", "TestAdvance"]) + ser = max_for(name, ["TestAdvance"]) + deser = max_for(name, ["TestAdvance"]) + worst = max( + enc, + dec, + ser, + deser, + max_for( + name, + [ + "LinearRampApproxRuntime", + "GeomApproxWork", + "EncodeRaw", + "Segmented32MathPolicy", + ], + ), + ) + rows.append( + f"| {label} | {enc} | {dec} | {ser} | {deser} | {worst} | " + f"GCC `-fstack-usage` ESP32-C6 `-Os` (.su) |" + ) + return "\n".join(rows) + + +def parse_bench_lines(path: Path) -> str: + if not path.exists(): + return ( + "_No `docs/benchmark_results.txt` yet. Build `numeric-bench`, redirect " + "stdout to that file, then re-run this script._" + ) + raw = path.read_bytes() + if raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"): + text = raw.decode("utf-16") + else: + text = raw.decode("utf-8-sig") + rows = [ + "| Name | Encode ns | Decode ns | Serialize ns | Deserialize ns | Round-trip ns | Notes |", + "|---|---:|---:|---:|---:|---:|---|", + ] + host = "" + for line in text.splitlines(): + if line.startswith("BENCH_HOST"): + host = line + continue + if line.startswith("BENCH_SEG"): + kv = dict( + part.split("=", 1) for part in line.split()[1:] if "=" in part + ) + rows.append( + f"| {kv.get('name','?')} | {kv.get('encode_ns','')} | " + f"{kv.get('decode_ns','')} | {kv.get('serialize_ns','')} | " + f"{kv.get('deserialize_ns','')} | {kv.get('roundtrip_ns','')} | " + f"logical={kv.get('logical','')} wire_bytes={kv.get('wire_bytes','')} |" + ) + elif line.startswith("BENCH_CYC"): + kv = dict( + part.split("=", 1) for part in line.split()[1:] if "=" in part + ) + rows.append( + f"| CyclicCounter WireValue | {kv.get('wire_ns','')} | - | - | - | - | desktop ns |" + ) + rows.append( + f"| CyclicCounter TryRestore forward | {kv.get('restore_fwd_ns','')} | - | - | - | - | desktop ns |" + ) + rows.append( + f"| CyclicCounter TryRestore backward | {kv.get('restore_back_ns','')} | - | - | - | - | desktop ns |" + ) + rows.append( + f"| CyclicCounter TryAdvance | {kv.get('advance_ns','')} | - | - | - | - | desktop ns |" + ) + rows.append( + f"| CyclicCounter contextual deserialize | {kv.get('ctx_deser_ns','')} | - | - | - | - | desktop ns |" + ) + body = "\n".join(rows) + if host: + body = f"`{host}`\n\n" + body + return body + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1]) + ap.add_argument("--results", type=Path, default=None) + ap.add_argument("--bench", type=Path, default=None) + args = ap.parse_args() + repo: Path = args.repo + results_path = args.results or (repo / "docs" / "footprint_results.json") + bench_path = args.bench or (repo / "docs" / "benchmark_results.txt") + data = json.loads(results_path.read_text(encoding="utf-8")) + + fp_path = repo / "docs" / "footprint.md" + text = fp_path.read_text(encoding="utf-8") + toolchain = Path(str(data.get("toolchain", ""))).name or "riscv32-esp-elf-g++" + body_os = ( + f"Toolchain: `{toolchain}` (Espressif riscv32-esp-elf) \n" + f"Target: {data['target']} \n\n" + "### ESP32-C6 `-Os`\n\n" + + seg_table(data, "Os") + + "\n\n### ESP32-C6 `-O2`\n\n" + + seg_table(data, "O2") + ) + text = replace_block( + text, + "", + "", + body_os, + ) + body_cyc = ( + "### ESP32-C6 `-Os`\n\n" + + cyclic_table(data, "Os") + + "\n\n### ESP32-C6 `-O2`\n\n" + + cyclic_table(data, "O2") + + "\n\nAll three configurations: `.rodata = 0`, `.data = 0`, `.bss = 0`, " + "heap = 0, tables = 0, 64-bit arithmetic helper undefs = 0." + ) + text = replace_block( + text, + "", + "", + body_cyc, + ) + text = replace_block( + text, + "", + "", + "### ESP32-C6 `-Os`\n\n" + + sharing_table(data, "Os") + + "\n\n### ESP32-C6 `-O2`\n\n" + + sharing_table(data, "O2"), + ) + text = replace_block( + text, + "", + "", + stack_table(data), + ) + fp_path.write_text(text, encoding="utf-8", newline="\n") + print(f"updated {fp_path}") + + bench_md = repo / "docs" / "benchmarks.md" + if bench_md.exists(): + btext = bench_md.read_text(encoding="utf-8") + btext = replace_block( + btext, + "", + "", + parse_bench_lines(bench_path), + ) + bench_md.write_text(btext, encoding="utf-8", newline="\n") + print(f"updated {bench_md}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/measure_esp32c6_footprint.py b/tools/measure_esp32c6_footprint.py new file mode 100644 index 0000000..629cbe4 --- /dev/null +++ b/tools/measure_esp32c6_footprint.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# Copyright 2026 Aethernet Inc. +"""Measure ESP32-C6 object footprints and emit docs/footprint_results.json.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + + +SEG_SOURCES = [ + ("empty", "minimal_empty.cpp"), + ("rssi", "minimal_rssi.cpp"), + ("temperature", "minimal_temperature.cpp"), + ("humidity", "minimal_humidity.cpp"), + ("co2", "minimal_co2.cpp"), + ("rx", "minimal_rx.cpp"), + ("battery", "minimal_battery.cpp"), + ("connect", "minimal_connect.cpp"), + ("thermometer", "minimal_thermometer.cpp"), + ("all", "minimal_all.cpp"), +] + +CYCLIC_SOURCES = [ + ("cyclic_u8_u16", "minimal_cyclic_u8_u16.cpp"), + ("cyclic_u8_u32", "minimal_cyclic_u8_u32.cpp"), + ("cyclic_u16_u32", "minimal_cyclic_u16_u32.cpp"), +] + + +def find_esp_gxx() -> Path: + env = os.environ.get("RISCV32_ESP_ELF_GXX") + if env and Path(env).exists(): + return Path(env) + roots = [ + Path(r"C:\Espressif\tools\riscv32-esp-elf"), + Path.home() / ".espressif" / "tools" / "riscv32-esp-elf", + ] + matches: list[Path] = [] + for root in roots: + if not root.exists(): + continue + matches.extend(root.rglob("riscv32-esp-elf-g++.exe")) + matches.extend(root.rglob("riscv32-esp-elf-g++")) + if not matches: + raise SystemExit("riscv32-esp-elf-g++ not found; set RISCV32_ESP_ELF_GXX") + # Prefer newer ESP-IDF toolchains (esp-14.* over esp-2022r1). + def rank(p: Path) -> tuple: + s = str(p).lower() + return ( + 0 if "esp-14" in s or "esp-13" in s else 1, + s, + ) + + return sorted(matches, key=rank)[0] + + +def tool_beside(gxx: Path, name: str) -> Path: + stem = gxx.name.replace("g++", name).replace("g++.exe", f"{name}.exe") + # g++.exe -> size.exe / nm.exe / objdump.exe with prefix + if gxx.name.endswith("g++.exe"): + cand = gxx.with_name(gxx.name.replace("g++.exe", f"{name}.exe")) + elif gxx.name.endswith("g++"): + cand = gxx.with_name(gxx.name.replace("g++", name)) + else: + cand = gxx.parent / name + if cand.exists(): + return cand + raise SystemExit(f"tool not found beside g++: {cand}") + + +def section_sizes(size_bin: Path, obj: Path) -> dict[str, int]: + out = subprocess.check_output([str(size_bin), "-A", str(obj)], text=True) + text = rodata = data = bss = 0 + for line in out.splitlines(): + parts = line.split() + if len(parts) < 2: + continue + name, sz = parts[0], parts[1] + if not sz.isdigit(): + continue + n = int(sz) + if name.startswith(".text"): + text += n + elif name.startswith(".rodata"): + rodata += n + elif name.startswith(".data"): + data += n + elif name.startswith(".bss") or name.startswith(".sbss"): + bss += n + return { + "text": text, + "rodata": rodata, + "data": data, + "bss": bss, + "flash": text + rodata + data, + "ram": data + bss, + } + + +def undef_helpers(nm_bin: Path, obj: Path) -> list[str]: + out = subprocess.check_output([str(nm_bin), "-u", str(obj)], text=True) + pat = re.compile(r"(muldi3|divdi3|udivdi3|moddi3|umoddi3|ashldi3|lshrdi3)") + found = [] + for line in out.splitlines(): + if pat.search(line): + found.append(line.strip()) + return found + + +def compile_one( + gxx: Path, + src: Path, + obj: Path, + include_root: Path, + opt: str, + stack_usage: bool, +) -> None: + flags = [ + str(gxx), + "-std=c++20", + opt, + "-DNDEBUG", + "-ffunction-sections", + "-fdata-sections", + "-fno-exceptions", + "-fno-rtti", + "-march=rv32imac", + "-mabi=ilp32", + f"-I{include_root}", + f"-I{include_root / 'tests' / 'footprint'}", + "-c", + str(src), + "-o", + str(obj), + ] + if stack_usage: + flags.insert(3, "-fstack-usage") + subprocess.check_call(flags) + + +def parse_stack_usage(su_path: Path) -> dict[str, int]: + """Return max static stack bytes per function from GCC .su file.""" + result: dict[str, int] = {} + if not su_path.exists(): + return result + for line in su_path.read_text(encoding="utf-8", errors="replace").splitlines(): + # file:line:col:function\tbytes\tstatic + parts = line.split("\t") + if len(parts) < 2: + continue + head, nbytes = parts[0], parts[1] + if not nbytes.isdigit(): + continue + fn = head.split(":")[-1] + result[fn] = max(result.get(fn, 0), int(nbytes)) + return result + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1]) + ap.add_argument("--out", type=Path, default=None) + ap.add_argument("--opts", nargs="+", default=["-Os", "-O2"]) + args = ap.parse_args() + repo: Path = args.repo + out_json = args.out or (repo / "docs" / "footprint_results.json") + + gxx = find_esp_gxx() + size_bin = tool_beside(gxx, "size") + nm_bin = tool_beside(gxx, "nm") + out_dir = repo / "build-esp32c6-docs" + out_dir.mkdir(parents=True, exist_ok=True) + fp_dir = repo / "tests" / "footprint" + + results: dict = { + "toolchain": str(gxx), + "target": "ESP32-C6 / riscv32 ilp32", + "segmented": {}, + "cyclic": {}, + "stack_usage": {}, + } + + for opt in args.opts: + opt_key = opt.lstrip("-") + results["segmented"][opt_key] = {} + results["cyclic"][opt_key] = {} + for name, src_name in SEG_SOURCES + CYCLIC_SOURCES: + src = fp_dir / src_name + obj = out_dir / f"{name}_{opt_key}.o" + use_su = name in { + "temperature", + "co2", + "rx", + "battery", + "cyclic_u8_u32", + } and opt == "-Os" + compile_one(gxx, src, obj, repo, opt, stack_usage=use_su) + sizes = section_sizes(size_bin, obj) + helpers = undef_helpers(nm_bin, obj) + entry = {**sizes, "helpers64": helpers} + if name.startswith("cyclic_"): + results["cyclic"][opt_key][name] = entry + else: + results["segmented"][opt_key][name] = entry + if use_su: + su = parse_stack_usage(obj.with_suffix(".su")) + results["stack_usage"][name] = su + + out_json.parent.mkdir(parents=True, exist_ok=True) + out_json.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + print(f"wrote {out_json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())