Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c7cb5ae
Add FixedPoint scalar type for freestanding fixed-point filter support
MitchellThompkins Jul 7, 2026
b250ad5
Add FixedFilter and fixed-point converting constructor
MitchellThompkins Jul 7, 2026
e4ac5b0
Add fixed_point tests
MitchellThompkins Jul 7, 2026
7978150
Add fixed_filter integration tests
MitchellThompkins Jul 8, 2026
d886073
Add fixed_point and fixed_filter to umbrella include
MitchellThompkins Jul 8, 2026
5f96f48
Fix T{} zero-init and add saturation to FixedPoint double constructor
MitchellThompkins Jul 8, 2026
4c5dce3
Add FixedButterworth, FixedElliptic, and common Q-format aliases
MitchellThompkins Jul 8, 2026
e9f737b
Update fixed_filter tests to use FixedButterworth and built-in Q aliases
MitchellThompkins Jul 8, 2026
9c8fde4
Address PR review: {} init syntax, operator/ precondition docs
MitchellThompkins Jul 8, 2026
f5902ad
Add fixed point filter documentation
MitchellThompkins Jul 8, 2026
6c8e91e
Fix signed overflow UB in FixedPoint add/sub/negate
MitchellThompkins Jul 13, 2026
a051f31
Round to nearest in FixedPoint multiply
MitchellThompkins Jul 13, 2026
8546f77
Fix Q-format naming in FixedPoint comments
MitchellThompkins Jul 13, 2026
aee5fd4
Document a[0]=1 saturation and coeffs_b() range check
MitchellThompkins Jul 13, 2026
ef5a5c5
Document ambiguous FixedPoint integer construction
MitchellThompkins Jul 13, 2026
84d20a9
Brace all loop bodies in fixed_filter.test.cpp
MitchellThompkins Jul 14, 2026
9136cbd
Add UBSan CI job
MitchellThompkins Jul 16, 2026
67b0b14
Use range-based for loops to fill test arrays
MitchellThompkins Jul 16, 2026
5c6d868
Fix misleading FixedPoint disambiguation example in docs
MitchellThompkins Jul 16, 2026
3c3dd54
Drop operator/ from custom scalar requirements
MitchellThompkins Jul 16, 2026
c4a2445
Document the fixed-point test layer in verification.md
MitchellThompkins Jul 16, 2026
99e6572
Saturate FixedPoint arithmetic on overflow instead of wrapping
MitchellThompkins Aug 7, 2026
debab9d
Finish fixed-point docs and add long tier support
MitchellThompkins Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
image: ghcr.io/mitchellthompkins/consteig_dev_image:2026-04-27
strategy:
matrix:
compiler: [gcc, clang]
compiler: [gcc, clang, gcc-ubsan]
steps:
- name: Checkout
env:
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
)
endif()

# GCC only: the dev container's Clang has no sanitizer runtimes installed
# for this musl target, and GCC's libasan is broken on musl (unresolved
# __sanitizer::struct_sock_fprog_sz), so this is UBSan-only, GCC-only.
option(CONSTFILT_SANITIZE
"Build tests with UndefinedBehaviorSanitizer (GCC only)"
OFF)
if(CONSTFILT_SANITIZE)
target_compile_options(constfilt_warnings INTERFACE
-fsanitize=undefined
-fno-sanitize-recover=undefined
)
target_link_options(constfilt_warnings INTERFACE
-fsanitize=undefined
)
endif()

if(CONSTFILT_BUILD_TESTS)
add_subdirectory(test_dependencies)
enable_testing()
Expand Down
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ generate-reference:

.PHONY: remove
remove:
rm -rf build/ build-gcc/ build-clang/ profiling/build/ profiling/build-gcc/ profiling/build-clang/
rm -rf build/ build-gcc/ build-clang/ build-gcc-ubsan/ profiling/build/ profiling/build-gcc/ profiling/build-clang/

.PHONY: format
format:
Expand Down Expand Up @@ -56,6 +56,21 @@ build.clang:
test.clang: build.clang
ctest --test-dir $(BUILD_PREFIX)-clang -j$$(getconf _NPROCESSORS_ONLN)

# GCC-only: see CONSTFILT_SANITIZE comment in CMakeLists.txt for why this
# isn't also offered for Clang or with ASan.
.PHONY: build.gcc-ubsan
build.gcc-ubsan:
cmake -S . -B $(BUILD_PREFIX)-gcc-ubsan -G $(CMAKE_GENERATOR) \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER=g++ \
-DCONSTFILT_BUILD_TESTS=ON \
-DCONSTFILT_SANITIZE=ON
cmake --build $(BUILD_PREFIX)-gcc-ubsan --target all -- $(JOB_FLAG)

.PHONY: test.gcc-ubsan
test.gcc-ubsan: build.gcc-ubsan
ctest --test-dir $(BUILD_PREFIX)-gcc-ubsan -j$$(getconf _NPROCESSORS_ONLN)

################################################################################
# Cross-compile targets (compile-only, static_assert IS the test)
################################################################################
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ All at compile time, constfilt supports:
default, `TustinNW` standard) of arbitrary continuous-time transfer functions.
- Direct Form II Transposed filter implementation, with both a real-time
sample-by-sample interface and a `constexpr`-capable batch interface.
- Fixed-point coefficient storage via `FixedButterworth` and `FixedElliptic`,
for targets without floating-point hardware. Coefficients are computed in
`double` at compile time and converted once to the chosen Q format.

Full documentation can be found at
[documentation](https://mitchellthompkins.github.io/constfilt/).
Expand All @@ -41,6 +44,10 @@ Simply `#include <constfilt/constfilt.hpp>` (optionally consume via CMake with

static constexpr constfilt::Butterworth<double, 4> bw(100.0, 1000.0);
static constexpr constfilt::Elliptic<double, 4> el(100.0, 0.5, 60.0, 1000.0);

// Fixed-point: coefficients computed in double, stored as Q0.15 (16-bit).
static constexpr constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH>
fbw(100.0, 1000.0);
```

For a quick reference on getting started, including examples, see
Expand Down
8 changes: 8 additions & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ make container.make.format
`container.make.<target>` runs `make <target>` inside the container, so any
host Makefile target can be invoked this way.

`test.gcc-ubsan` builds and runs the same test suite with
UndefinedBehaviorSanitizer enabled (`-fsanitize=undefined
-fno-sanitize-recover=undefined`, so a finding fails the test instead of just
printing a diagnostic). CI runs it alongside the plain GCC and Clang builds.
It is GCC-only and UBSan-only: the dev container's Clang has no sanitizer
runtimes installed for this musl target, and GCC's `libasan` is broken on
musl in this image.

For an interactive shell:

```sh
Expand Down
198 changes: 198 additions & 0 deletions docs/fixed-point.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Fixed-Point Filters

constfilt can compute filter coefficients in `double` at compile time and then
convert them to a fixed-point type for deployment on targets that lack
floating-point hardware.

## Quick start

```cpp
#include <constfilt/constfilt.hpp>

// 1st-order Butterworth lowpass, coefficients stored as Q0.15 (16-bit).
static constexpr constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH>
filt(100.0, 1000.0);

// Batch filtering.
constfilt::Q0_15 input[8]{};
constfilt::Q0_15 output[8]{};
filt(input, output);

// Real-time sample-by-sample filtering.
constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH> rt(100.0, 1000.0);
constfilt::Q0_15 y = rt(constfilt::Q0_15{0.5});
```

All coefficient math runs at compile time in `double`. The conversion to
fixed-point happens once, also at compile time. At runtime only the
Direct Form II Transposed arithmetic executes in fixed-point.

## Choosing a Q format

A Q format defines how many fractional bits a fixed-point value carries and
therefore its representable range. For a signed N-bit type with F fractional bits
the range is `[-2^(N-1-F), 2^(N-1-F))` and the resolution is `2^(-F)`.

Filter coefficients can exceed `[-1, 1)` for higher-order designs, and `b[k]`
coefficients can exceed `[-1, 1)` too for highpass or elliptic designs with
gain. The Q format must be wide enough to hold the largest coefficient in
either array. Check both `coeffs_a()` and `coeffs_b()` on the `double`
prototype if you are unsure.

By convention `a[0] = 1.0`, but in a `Q0` format (range `[-1, 1)`) `1.0`
itself is not representable, so the converting constructor saturates it to
the format maximum instead (about `0.99997` for `Q0_15`). This is harmless
for filtering, since the Direct Form II Transposed recurrence never reads
`a[0]`, but `coeffs_a()[0]` will not read back exactly `1.0` in a `Q0`
format.

### Built-in aliases

| Alias | Storage | FracBits | Range | Resolution |
|---------|---------------|----------|------------|-------------|
| `Q0_7` | `signed char` | 7 | `[-1, 1)` | ~7.8e-3 |
| `Q1_6` | `signed char` | 6 | `[-2, 2)` | ~1.6e-2 |
| `Q0_15` | `short` | 15 | `[-1, 1)` | ~3.1e-5 |
| `Q1_14` | `short` | 14 | `[-2, 2)` | ~6.1e-5 |
| `Q0_31` | `int` | 31 | `[-1, 1)` | ~4.7e-10 |
| `Q1_30` | `int` | 30 | `[-2, 2)` | ~9.3e-10 |

All aliases are in the `constfilt` namespace and are defined in
`include/constfilt/fixed_point_aliases.hpp`, which is included by the umbrella
header.

### Rule of thumb

Use a `Q0` variant (range `[-1, 1)`) for 1st-order filters and verify the
coefficients fit. Use a `Q1` variant (range `[-2, 2)`) when denominator
coefficients exceed `[-1, 1)`, which is common for 2nd-order and higher designs.
Higher precision (`Q0_31` / `Q1_30`) reduces quantization error at the cost of
wider arithmetic.

## FixedButterworth and FixedElliptic

These are the primary user-facing types. They mirror the constructor signatures
of `Butterworth` and `Elliptic` exactly, with the floating-point scalar type
replaced by the fixed-point type.

```cpp
// FixedButterworth<TFixed, N, Method, FilterType>
static constexpr constfilt::FixedButterworth<constfilt::Q1_30, 2, constfilt::ZOH>
bw(100.0, 1000.0);

// FixedElliptic<TFixed, N, Method, FilterType>
static constexpr constfilt::FixedElliptic<constfilt::Q1_30, 2, constfilt::ZOH>
el(100.0, 0.5, 60.0, 1000.0);
```

`Method` and `FilterType` default to `TustinPW` and `LowPass` respectively,
matching the floating-point counterparts.

## FixedFilter (advanced)

`FixedFilter` is the lower-level building block. It converts any
`Filter<double, NB, NA>` to a fixed-point filter by taking the double filter as
a constructor argument. Use it when you need to convert an `AnalogFilter`, or when you prefer to
manage the double prototype explicitly.
Comment on lines +93 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the actual prototype type accepted by FixedFilter.

The constructor accepts Filter<U, NB, NA> for a templated U; it is not limited to Filter<double, NB, NA>. Use Filter<U, NB, NA> or “a floating-point Filter” in this section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/fixed-point.md` around lines 93 - 96, Update the FixedFilter
documentation to describe its constructor prototype as Filter<U, NB, NA>, or
equivalently as a floating-point Filter, instead of limiting it to
Filter<double, NB, NA>. Preserve the guidance about converting an AnalogFilter
and explicitly managing the double prototype.


```cpp
static constexpr constfilt::Butterworth<double, 1, constfilt::ZOH> proto(
100.0, 1000.0);
static constexpr constfilt::FixedFilter<constfilt::Q0_15, 2, 2> filt(proto);
```

The `NB` and `NA` template arguments must match the source filter. For a
Butterworth or Elliptic of order N both are `N + 1`.

## Custom fixed-point types

constfilt supplies `FixedPoint<TInt, TWider, FracBits>` as a ready-to-use
scalar, but `FixedFilter`, `FixedButterworth`, and `FixedElliptic` accept any
type `T` that provides the arithmetic operators used by `Filter`

```
T operator+(T) const
T operator-(T) const
T operator-() const
T operator*(T) const
T() // default constructor produces zero
explicit T(double) // conversion from double coefficient
```

`T` must also support value-initialization (`T{}` produces zero) and
`static_cast<T>(double_value)` must be valid. `T` must also be a literal
type and copy assignable, both of which the `constexpr` batch path relies
on through `T _b[NB]{}` and the assignments inside `Filter::operator()`.

## FixedPoint reference

```cpp
template <typename TInt, typename TWider, unsigned FracBits>
class constfilt::FixedPoint;
```

**Template parameters**

| Parameter | Description |
|------------|----------------------------------------------------------------|
| `TInt` | Signed integer storage type (`signed char`, `short`, `int`, `long`, `long long`) |
| `TWider` | Signed integer strictly wider than `TInt`, used as multiply intermediate |
| `FracBits` | Number of fractional bits; must satisfy `0 < FracBits < 8*sizeof(TInt)` |

**Construction**

| Expression | Result |
|-------------------------|--------------------------------------------------|
| `FixedPoint{}` | Zero |
| `FixedPoint{double v}` | Nearest representable value; saturates at limits |
| `FixedPoint{TInt n}` | Integer `n` scaled by `2^FracBits` |
| `FixedPoint::from_raw(TInt r)` | Raw bit pattern `r` without scaling |

An integer literal such as `Q1_14{1}` fails to compile: it converts equally
well to `TInt` (the integer constructor) and to `double` (the double
constructor), so the call is ambiguous. Cast the literal to the storage type
or to `double` to disambiguate, for example `Q1_14{static_cast<short>(1)}` or
`Q1_14{1.0}` (both produce raw value `16384`, the representation of `1.0` in
`Q1_14`'s `[-2, 2)` range).

The two forms are not interchangeable in general, and picking the wrong one
matters most near a format's limits. The integer constructor scales its
argument by `2^FracBits` and wraps on overflow; the double constructor
rounds to the nearest representable value and saturates at the format's
limits. In a `Q0` format (range `[-1, 1)`) the value `1` is not representable
at all, so `Q0_15{static_cast<short>(1)}` wraps to a negative raw value while
`Q0_15{1.0}` saturates to just under the maximum. Pick the form that matches
what the value actually means.
Comment on lines +151 to +165

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'integer_val|static_cast<TInt>.*SCALE' include/constfilt/fixed_point.hpp

Repository: MitchellThompkins/constfilt

Length of output: 1487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- fixed_point.hpp relevant implementation ---'
sed -n '80,145p' include/constfilt/fixed_point.hpp

printf '%s\n' '--- project standard and target assumptions ---'
rg -n -C 3 'CXX_STANDARD|cxx_std_17|std=c\+\+17|CMAKE_CXX|two.?complement|wrap|integer constructor|Wraps silently' \
  CMakeLists.txt cmake include test tests docs 2>/dev/null || true

printf '%s\n' '--- uses and tests for the integer constructor ---'
rg -n -C 5 'FixedPoint<|Q0_15|Q1_14|raw_value\(\)|wrap|overflow' \
  test tests include docs 2>/dev/null || true

Repository: MitchellThompkins/constfilt

Length of output: 50385


🌐 Web query:

C++17 [conv.integral] out-of-range conversion to signed integer implementation-defined signal

💡 Result:

Under the C++17 standard, specifically section [conv.integral], when an integer value is converted to a signed integer type and the value cannot be represented in the destination type, the resulting value is implementation-defined [1][2]. Unlike the C standard (specifically C11, section 6.3.1.3), which explicitly states that such a conversion results in either an implementation-defined value or an implementation-defined signal [3], the C++17 standard does not mention signals [1][2]. It simply specifies that the result is implementation-defined [1][2]. In practice, virtually all modern C++ implementations define this behavior as a silent wraparound (e.g., using two's complement representation) [4][5], though the standard requires that the specific behavior be documented by the implementation [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- focused integer-construction tests ---'
sed -n '24,72p' tests/fixed_point.test.cpp
rg -n -C 4 'FromInteger|static_cast<short>|static_cast<signed char>|integer_val|Q0_15.*1|Q1_14.*1' \
  tests/fixed_point.test.cpp docs/fixed-point.md include/constfilt/fixed_point.hpp

printf '%s\n' '--- supported target documentation ---'
rg -n -C 3 'ARM|target|platform|two.?complement|signed integer|supported' \
  README.md docs CMakeLists.txt cmake/toolchains include/constfilt/fixed_point.hpp 2>/dev/null | head -n 180

Repository: MitchellThompkins/constfilt

Length of output: 19046


Define signed wrapping for the integer constructor.

C++17 makes an out-of-range conversion to TInt implementation-defined. The documented negative raw value is not portable across the project’s C++17 targets. Implement explicit wrapping, or qualify the documentation and tests for supported two’s-complement targets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/fixed-point.md` around lines 151 - 165, Make the integer constructor’s
out-of-range behavior explicit and portable across supported C++17 targets,
rather than relying on implementation-defined conversion to TInt. Update the
constructor implementation and its documentation/tests so signed overflow wraps
deterministically to the documented negative raw value, or explicitly restrict
and qualify the behavior for two’s-complement targets.

Sources: Coding guidelines, Path instructions


**Members**

| Member | Description |
|-------------------|---------------------------------------------|
| `raw_value()` | Returns the raw integer representation |
| `to_double()` | Converts back to `double` |
| `from_raw(TInt)` | Constructs from raw bits (static) |

Arithmetic follows Q-format rules. Multiplication widens to `TWider`, adds a
half LSB bias, and shifts right by `FracBits` to renormalize. This rounds
half up, ties toward positive infinity, which is not the same tie rule as
the double constructor's ties away from zero. Division widens and shifts
left before dividing, then truncates toward zero rather than rounding, so
`operator/` and `operator*` do not agree on rounding. `Filter` never
divides, so this asymmetry only matters if `FixedPoint` is used as a
general purpose scalar outside `Filter`. Addition, subtraction, unary
negation, and multiplication saturate at the format's limits on overflow,
matching CMSIS DSP and TI IQmath convention. Wrapping inside an IIR
feedback loop flips the sign of the accumulated value and can drive
sustained large amplitude oscillation rather than clipping gracefully,
which is why saturation is preferred. The integer constructor is the one
place that still wraps on overflow, see above. Division by zero is
undefined behavior.

`fixed_limits<TInt, TWider, FracBits>` provides `min()`, `max()`, `lowest()`,
and `epsilon()` without requiring `<limits>`. This follows the integer
convention rather than the floating point one `std::numeric_limits` uses.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use floating-point as a compound adjective.

Change “floating point one” to “floating-point one.”

🧰 Tools
🪛 LanguageTool

[grammar] ~193-~193: Use a hyphen to join words.
Context: ...eger convention rather than the floating point one std::numeric_limits uses. `m...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/fixed-point.md` at line 193, Update the prose around std::numeric_limits
to hyphenate “floating-point” when it is used as a compound adjective, changing
“floating point one” accordingly.

Source: Linters/SAST tools

`min()` returns the most negative representable value, not the smallest
positive one, and `lowest()` is identical to `min()` since `FixedPoint` has
no separate notion of a smallest finite value. `max()` returns the most
positive representable value, and `epsilon()` returns one LSB, the smallest
representable step, `2^(-FracBits)`.
22 changes: 22 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ constexpr double b0 = bw.coeffs_b()[0];
constexpr double a1 = bw.coeffs_a()[1];
```

## Fixed-point filters

For targets without floating-point hardware, use `FixedButterworth` or
`FixedElliptic`. Coefficients are computed in `double` at compile time and
converted once to the chosen Q format.

```cpp
// Q0_15 is short storage with 15 fractional bits, range [-1, 1).
// Q1_30 is int storage with 30 fractional bits, range [-2, 2).
// Use Q1_30 when denominator coefficients exceed [-1, 1), which occurs
// for 2nd-order and higher designs at many cutoff/sample-rate combinations.

static constexpr constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH>
bw1(100.0, 1000.0);

static constexpr constfilt::FixedButterworth<constfilt::Q1_30, 2, constfilt::ZOH>
bw2(100.0, 1000.0);
```

See [Fixed-Point Filters](fixed-point.md) for the full guide including Q format
selection, the built-in aliases, and the `FixedPoint` scalar type reference.

## Stability checking

`AnalogFilter` (the base of `Butterworth` and `Elliptic`) exposes a `CheckStab`
Expand Down
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ All at compile time:
continuous-time transfer functions.
- Direct Form II Transposed filter implementation, with both a real-time
sample-by-sample interface and a `constexpr`-capable batch interface.
- Fixed-point coefficient storage via `FixedButterworth` and `FixedElliptic`,
for targets without floating-point hardware. Coefficients are computed in
`double` at compile time and converted once to the chosen Q format.

Practical filter order is bounded only by `double` precision; the library
imposes no upper limit.
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- Butterworth: butterworth.md
- Elliptic: elliptic.md
- Discretization Methods: discretization.md
- Fixed-Point Filters: fixed-point.md
- Development:
- Building & Testing: building.md
- Verification: verification.md
Expand Down
14 changes: 14 additions & 0 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The layers, from the inside out:
| `analog_filter.test.cpp` | Continuous-time transfer function to state-space conversion and stability classification; full filter output checks (coefficients, step, impulse, chirp) against Octave references for ZOH, Matched-Z, and Tustin. |
| `butterworth.test.cpp` | Full Butterworth pipeline: pole computation, state-space, discretization, filtering, lowpass and highpass, ZOH, Matched-Z, and Tustin. |
| `elliptic.test.cpp` | Full Elliptic pipeline: nome-series design, discretization, filtering, lowpass and highpass, ZOH, Matched-Z, and Tustin. |
| `fixed_point.test.cpp` | `FixedPoint` scalar type in isolation: construction, arithmetic, comparisons, overflow saturation, `fixed_limits`. |
| `fixed_filter.test.cpp` | Fixed-point filters end to end: batch and real-time output tracked against the `double` prototype, `constexpr` construction. |

## Philosophy

Expand All @@ -24,6 +26,16 @@ installation and any disagreement shows up as a hard failure. See
[building.md](building.md#regenerating-reference-data) for how references are
regenerated.

The fixed-point layer is the one exception: it is verified against the
`double` prototype rather than against Octave directly. Fixed-point
quantization error sits on top of the already-Octave-validated
double-precision algorithm, not in place of it, so the fixed-point tests
exist to prove the conversion and fixed-point arithmetic are correct, not to
re-verify coefficient computation. Comparing straight against Octave would
require folding quantization error into the same tolerance used for the
double path, which would mask real regressions in the double algorithm
behind the much larger fixed-point error.

Filter correctness is checked against step, impulse, and chirp inputs.

Because constfilt coefficients are `constexpr`, filter behavior must be
Expand All @@ -41,3 +53,5 @@ bit-identical output to confirm they implement the same computation.
| Analytic ZOH / Matched-Z checks | $10^{-10}$ | Closed-form references carry no approximation error beyond `double` representation. |
| Analytic Tustin checks | $10^{-12}$ | Reference values are exact rationals, so a tighter bound is achievable than for ZOH / Matched-Z whose references involve transcendental `exp()` values. |
| DF2T hand-trace | $10^{-13}$ to $10^{-14}$ | Rational inputs yield nearly exact results; tight bounds catch any arithmetic mistake. |
| Fixed-point vs. `double` prototype (`Q0_15`) | $5 \times 10^{-3}$ | Dominated by quantization, not algorithm error; `Q0_15` resolution is $1/32768 \approx 3 \times 10^{-5}$, and the bound is set conservatively above the accumulated per-sample rounding in a recursive filter. |
| Fixed-point vs. `double` prototype (`Q1_30`) | $10^{-6}$ | Same reasoning at `Q1_30` resolution, $1/2^{30} \approx 10^{-9}$. |
5 changes: 5 additions & 0 deletions include/constfilt/constfilt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
#include "discretize.hpp"
#include "elliptic.hpp"
#include "filter.hpp"
#include "fixed_butterworth.hpp"
#include "fixed_elliptic.hpp"
#include "fixed_filter.hpp"
#include "fixed_point.hpp"
#include "fixed_point_aliases.hpp"
#include "stability.hpp"

#endif // CONSTFILT_HPP
Loading