Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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: 2 additions & 0 deletions include/constfilt/constfilt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "discretize.hpp"
#include "elliptic.hpp"
#include "filter.hpp"
#include "fixed_filter.hpp"
#include "fixed_point.hpp"
#include "stability.hpp"

#endif // CONSTFILT_HPP
19 changes: 16 additions & 3 deletions include/constfilt/filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,27 @@ template <typename T, consteig::Size NB, consteig::Size NA> class Filter
}
}

template <typename U>
constexpr explicit Filter(const Filter<U, NB, NA> &other)
{
for (consteig::Size i = 0; i < NB; ++i)
{
_b[i] = static_cast<T>(other.coeffs_b()[i]);
}
for (consteig::Size i = 0; i < NA; ++i)
{
_a[i] = static_cast<T>(other.coeffs_a()[i]);
}
}

private:
constexpr T b_coeff(consteig::Size i) const
{
return i < NB ? _b[i] : T(0);
return i < NB ? _b[i] : T{};
}
constexpr T a_coeff(consteig::Size i) const
{
return i < NA ? _a[i] : T(0);
return i < NA ? _a[i] : T{};
}

public:
Expand Down Expand Up @@ -107,7 +120,7 @@ template <typename T, consteig::Size NB, consteig::Size NA> class Filter
{
for (consteig::Size k = 0; k < M; ++k)
{
_state[k] = static_cast<T>(0);
_state[k] = T{};
}
}
};
Expand Down
30 changes: 30 additions & 0 deletions include/constfilt/fixed_filter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef CONSTFILT_FIXED_FILTER_HPP
#define CONSTFILT_FIXED_FILTER_HPP

#include "filter.hpp"

namespace constfilt
{

// Concrete fixed-point filter. Converts coefficients from any floating-point
// Filter<U, NB, NA> to a stored fixed-point representation at construction.
// All filtering operations are inherited from Filter and run in TFixed
// arithmetic.
//
// Typical use:
// constexpr Butterworth<double, 4> proto(100.0, 1000.0);
// constexpr FixedFilter<FixedPoint<short, int, 15>, 5, 5> filt(proto);
template <typename TFixed, consteig::Size NB, consteig::Size NA>
class FixedFilter : public Filter<TFixed, NB, NA>
{
public:
template <typename U>
constexpr explicit FixedFilter(const Filter<U, NB, NA> &src)
: Filter<TFixed, NB, NA>(src)
{
}
};

} // namespace constfilt

#endif // CONSTFILT_FIXED_FILTER_HPP
235 changes: 235 additions & 0 deletions include/constfilt/fixed_point.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#ifndef CONSTFILT_FIXED_POINT_HPP
#define CONSTFILT_FIXED_POINT_HPP

namespace constfilt
{

namespace detail
{

template <typename T> struct make_unsigned;

template <> struct make_unsigned<signed char>
{
using type = unsigned char;
};

template <> struct make_unsigned<short>
{
using type = unsigned short;
};

template <> struct make_unsigned<int>
{
using type = unsigned int;
};

// long long / __int128 tier: only available with compiler extension support.
// TWider = __int128 is required when TInt = long long.
template <> struct make_unsigned<long long>
{
using type = unsigned long long;
};

} // namespace detail

// Fixed-point scalar type.
//
// Template parameters:
// TInt - signed integer storage type (signed char, short, int, long long)
// TWider - signed integer type strictly wider than TInt, used as multiply
// intermediate to prevent overflow (e.g. short->int, int->long
// long)
// FracBits - number of fractional bits; must satisfy 0 < FracBits <
// 8*sizeof(TInt)
//
// Supported tiers (without compiler extensions):
// FixedPoint<signed char, short, 7> Q1.7
// FixedPoint<short, int, 15> Q1.15
// FixedPoint<int, long long, 31> Q1.31
//
// Overflow on construction and all arithmetic wraps silently (two's
// complement). The right-shift in operator* is arithmetic; this is
// implementation-defined in C++17 but holds on all supported targets (GCC/Clang
// with two's complement).
template <typename TInt, typename TWider, unsigned FracBits> class FixedPoint
{
static_assert(sizeof(TInt) < sizeof(TWider),
"TWider must be strictly wider than TInt");
static_assert(TInt(-1) < TInt(0), "TInt must be a signed type");
static_assert(TWider(-1) < TWider(0), "TWider must be a signed type");
static_assert(FracBits > 0u, "FracBits must be greater than zero");
static_assert(FracBits < sizeof(TInt) * 8u,
"FracBits must be less than the bit width of TInt");

using UInt = typename detail::make_unsigned<TInt>::type;

static constexpr TWider SCALE = TWider(1) << FracBits;
static constexpr TInt INT_MAX_VAL =
static_cast<TInt>(static_cast<UInt>(-1) >> 1);
static constexpr TInt INT_MIN_VAL = static_cast<TInt>(-INT_MAX_VAL - 1);

TInt _raw;

struct raw_tag
{
};
constexpr FixedPoint(TInt val, raw_tag) noexcept : _raw(val)
{
}

public:
constexpr FixedPoint() noexcept : _raw(0)
{
}

// Construct from double. Rounds to nearest (ties away from zero).
// Saturates at INT_MAX_VAL / INT_MIN_VAL if v is outside the representable
// range. Saturation avoids undefined behavior from out-of-range
// float-to-int casts in constexpr contexts.
constexpr explicit FixedPoint(double v) noexcept : _raw(to_raw(v))
{
}

private:
static constexpr TInt to_raw(double v) noexcept
{
const double scaled = v >= 0.0 ? v * static_cast<double>(SCALE) + 0.5
: v * static_cast<double>(SCALE) - 0.5;
return scaled >= static_cast<double>(INT_MAX_VAL)
? INT_MAX_VAL
: (scaled <= static_cast<double>(INT_MIN_VAL)
? INT_MIN_VAL
: static_cast<TInt>(scaled));
}

public:
// Construct from integer. The value is scaled by 2^FracBits.
// Wraps silently if integer_val is outside the representable range.
constexpr explicit FixedPoint(TInt integer_val) noexcept
: _raw(static_cast<TInt>(static_cast<TWider>(integer_val) * SCALE))
{
}

constexpr TInt raw_value() const noexcept
{
return _raw;
}

static constexpr FixedPoint from_raw(TInt val) noexcept
{
return {val, raw_tag{}};
}

constexpr double to_double() const noexcept
{
return static_cast<double>(_raw) / static_cast<double>(SCALE);
}

// Arithmetic
constexpr FixedPoint operator+(FixedPoint rhs) const noexcept
{
return {static_cast<TInt>(_raw + rhs._raw), raw_tag{}};
}
constexpr FixedPoint operator-(FixedPoint rhs) const noexcept
{
return {static_cast<TInt>(_raw - rhs._raw), raw_tag{}};
}
constexpr FixedPoint operator-() const noexcept
{
return {static_cast<TInt>(-_raw), raw_tag{}};
}
constexpr FixedPoint operator*(FixedPoint rhs) const noexcept
{
return {static_cast<TInt>((static_cast<TWider>(_raw) *
static_cast<TWider>(rhs._raw)) >>
FracBits),
raw_tag{}};
}
constexpr FixedPoint operator/(FixedPoint rhs) const noexcept
{
return {static_cast<TInt>((static_cast<TWider>(_raw) * SCALE) /
static_cast<TWider>(rhs._raw)),
raw_tag{}};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Compound assignment
constexpr FixedPoint &operator+=(FixedPoint rhs) noexcept
{
_raw += rhs._raw;
return *this;
}
constexpr FixedPoint &operator-=(FixedPoint rhs) noexcept
{
_raw -= rhs._raw;
return *this;
}
constexpr FixedPoint &operator*=(FixedPoint rhs) noexcept
{
return *this = *this * rhs;
}
constexpr FixedPoint &operator/=(FixedPoint rhs) noexcept
{
return *this = *this / rhs;
}

// Comparison
constexpr bool operator==(FixedPoint rhs) const noexcept
{
return _raw == rhs._raw;
}
constexpr bool operator!=(FixedPoint rhs) const noexcept
{
return _raw != rhs._raw;
}
constexpr bool operator<(FixedPoint rhs) const noexcept
{
return _raw < rhs._raw;
}
constexpr bool operator>(FixedPoint rhs) const noexcept
{
return _raw > rhs._raw;
}
constexpr bool operator<=(FixedPoint rhs) const noexcept
{
return _raw <= rhs._raw;
}
constexpr bool operator>=(FixedPoint rhs) const noexcept
{
return _raw >= rhs._raw;
}
};

// Numeric limits for FixedPoint. Replaces std::numeric_limits for freestanding
// targets. min/max are computed via unsigned bit manipulation (no stdlib
// required).
template <typename TInt, typename TWider, unsigned FracBits> struct fixed_limits
{
using F = FixedPoint<TInt, TWider, FracBits>;
using UInt = typename detail::make_unsigned<TInt>::type;

static constexpr TInt int_max =
static_cast<TInt>(static_cast<UInt>(-1) >> 1);
static constexpr TInt int_min = static_cast<TInt>(-int_max - 1);

static constexpr F min() noexcept
{
return F::from_raw(int_min);
}
static constexpr F max() noexcept
{
return F::from_raw(int_max);
}
static constexpr F lowest() noexcept
{
return min();
}
static constexpr F epsilon() noexcept
{
return F::from_raw(1);
}
};

} // namespace constfilt

#endif // CONSTFILT_FIXED_POINT_HPP
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ add_constfilt_test(butterworth butterworth.test.cpp)
add_constfilt_test(analog_filter analog_filter.test.cpp)
add_constfilt_test(elliptic elliptic.test.cpp)
add_constfilt_test(constexpr_asserts constexpr_asserts.cpp)
add_constfilt_test(fixed_point fixed_point.test.cpp)
add_constfilt_test(fixed_filter fixed_filter.test.cpp)
Loading