Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
verification/vectors/fir_q15/*.csv text eol=lf
25 changes: 0 additions & 25 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ Describe the DSP kernel, test, benchmark, packaging, or documentation change.
## Evidence

List commands, tests, benchmark results, plots, or reports used to verify the change.

## Risks

Describe numerical assumptions, changed tolerances and possible regressions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,8 @@ the code is small enough to study, but structured enough to demonstrate how DSP
## License

MIT
## Fixed-point FIR evidence

The [shared Q15 vector package](verification/vectors/fir_q15/README.md) checks
Python reference, C++ float and C++ fixed-point convolution with explicit
rounding and saturation. RTL and hardware qualification remain future work.
8 changes: 8 additions & 0 deletions dsp_core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,20 @@ endif()
if(BUILD_TESTING)
add_executable(dsp_core_tests
tests/dsp_fir_test.cpp
tests/dsp_fixed_fir_test.cpp
tests/dsp_goertzel_test.cpp
tests/dsp_gcc_phat_test.cpp
tests/dsp_resampler_test.cpp
tests/dsp_api_validation_test.cpp
)
target_link_libraries(dsp_core_tests PRIVATE dsp_core::dsp GTest::gtest_main)
target_compile_definitions(dsp_core_tests PRIVATE
DSP_FIR_VECTOR_DIR="${CMAKE_CURRENT_SOURCE_DIR}/../verification/vectors/fir_q15")
find_package(Python3 COMPONENTS Interpreter QUIET)
if(Python3_Interpreter_FOUND)
add_test(NAME fir_vector_regeneration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/../tools/generate_fir_vectors.py --check)
endif()
dsp_core_enable_warnings(dsp_core_tests)

include(GoogleTest)
Expand Down
36 changes: 36 additions & 0 deletions dsp_core/include/dsp_core/fixed_fir.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <vector>

namespace dsp_core::dsp {

// Full convolution, signed Q1.15 inputs/taps, zero initial state and zero tail.
// Accumulate exactly in int64, round once (nearest, ties away from zero),
// then saturate. The tap bound keeps even worst-case sums well inside int64.
inline std::vector<std::int16_t> convolve_q15(
const std::vector<std::int16_t>& input,
const std::vector<std::int16_t>& taps) {
if (taps.size() > 65536) {
throw std::invalid_argument("Q15 FIR supports at most 65536 taps");
}
if (input.empty() || taps.empty()) return {};
std::vector<std::int16_t> output(input.size() + taps.size() - 1);
for (std::size_t n = 0; n < output.size(); ++n) {
std::int64_t sum = 0;
const auto first = n >= input.size() ? n - input.size() + 1 : 0;
const auto last = std::min(n + 1, taps.size());
for (std::size_t k = first; k < last; ++k) {
sum += static_cast<std::int64_t>(input[n - k]) * taps[k];
}
// Signed division is defined in C++17; no implementation-defined
// right shift of a negative integer is used here.
const auto rounded = sum < 0 ? -((-sum + 16384) / 32768) : (sum + 16384) / 32768;
output[n] = static_cast<std::int16_t>(std::clamp<std::int64_t>(rounded, -32768, 32767));
}
return output;
}

} // namespace dsp_core::dsp
59 changes: 59 additions & 0 deletions dsp_core/tests/dsp_fixed_fir_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "dsp_core/dsp.hpp"
#include "dsp_core/fixed_fir.hpp"
#include <fstream>
#include <sstream>
#include <string>
#include <gtest/gtest.h>

namespace {
std::vector<std::int16_t> load(const std::string& path) {
std::ifstream stream(path);
if (!stream) throw std::runtime_error("missing vector: " + path);
std::string line;
std::getline(stream, line);
std::vector<std::int16_t> values;
while (std::getline(stream, line)) values.push_back(static_cast<std::int16_t>(std::stoi(line)));
return values;
}
}

TEST(DspFixedFir, SharedPythonVectorsMatchFixedAndFloatingConvolution) {
for (const auto* name : {"impulse", "signed", "ties", "saturation", "negative_taps"}) {
SCOPED_TRACE(name);
const std::string base = std::string(DSP_FIR_VECTOR_DIR) + "/" + name;
const auto input = load(base + "_input.csv");
const auto taps = load(base + "_taps.csv");
const auto fixed = dsp_core::dsp::convolve_q15(input, taps);
dsp_core::dsp::Signal x, h;
for (auto value : input) x.push_back(value / 32768.0);
for (auto value : taps) h.push_back(value / 32768.0);
const auto floating = dsp_core::dsp::convolve(x, h);
std::ifstream expected(base + "_expected.csv");
ASSERT_TRUE(expected.good());
std::string line;
std::getline(expected, line);
std::size_t n = 0;
while (std::getline(expected, line)) {
ASSERT_LT(n, fixed.size());
ASSERT_LT(n, floating.size());
const auto comma = line.find(',');
ASSERT_NE(comma, std::string::npos);
EXPECT_EQ(fixed[n], std::stoi(line.substr(0, comma)));
const auto reference = std::stod(line.substr(comma + 1));
EXPECT_NEAR(floating[n], reference, 1e-12);
if (reference >= -1.0 && reference <= 32767.0 / 32768.0) {
EXPECT_LE(std::abs(reference - fixed[n] / 32768.0), 0.5 / 32768.0);
}
++n;
}
EXPECT_EQ(n, fixed.size());
EXPECT_EQ(n, floating.size());
}
}

TEST(DspFixedFir, EmptyInputsAndAccumulatorBound) {
EXPECT_TRUE(dsp_core::dsp::convolve_q15({}, {1}).empty());
EXPECT_TRUE(dsp_core::dsp::convolve_q15({1}, {}).empty());
EXPECT_THROW(dsp_core::dsp::convolve_q15({1}, std::vector<std::int16_t>(65537)), std::invalid_argument);
EXPECT_EQ(dsp_core::dsp::convolve_q15({-32768}, {-32768})[0], 32767);
}
49 changes: 49 additions & 0 deletions tools/generate_fir_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Independent integer-convolution fixtures, shared as plain CSV; no numpy needed."""
import argparse
from fractions import Fraction
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1] / "verification/vectors/fir_q15"
CASES = {
"impulse": ([32767, 0, 0, 0], [8192, 16384, 8192]),
"signed": ([1234, -2345, 32767, -32768, 1, -1, 0], [16384, -8192, 4096]),
"ties": ([1, -1, 3, -3, 32767, -32768], [16384]),
"saturation": ([32767, 32767, -32768, -32768], [32767, 32767]),
"negative_taps": ([-32768, 32767, 0, 0], [-32768, -32768]),
}


def fixtures():
for name, (samples, taps) in CASES.items():
products = [0] * (len(samples) + len(taps) - 1)
for i, sample in enumerate(samples):
for j, tap in enumerate(taps):
products[i + j] += sample * tap
yield f"{name}_input.csv", "q15\n" + "".join(f"{v}\n" for v in samples)
yield f"{name}_taps.csv", "q15\n" + "".join(f"{v}\n" for v in taps)
rows = ["q15,float64\n"]
for value in products:
exact = Fraction(value, 32768)
rounded = int(abs(exact) + Fraction(1, 2)) * (-1 if exact < 0 else 1)
rows.append(f"{max(-32768, min(32767, rounded))},{value / 1073741824:.17g}\n")
yield f"{name}_expected.csv", "".join(rows)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
ROOT.mkdir(parents=True, exist_ok=True)
for name, value in fixtures():
path = ROOT / name
if args.check:
if not path.exists() or path.read_bytes() != value.encode("ascii"):
raise SystemExit(f"stale vector: {path}")
else:
path.write_bytes(value.encode("ascii"))
print("FIR Q15 shared vectors: PASS")


if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions verification/vectors/fir_q15/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Shared FIR Q15 vectors

Run `python tools/generate_fir_vectors.py --check` from the repository root.
Omit `--check` to regenerate the committed CSV files with the independent Python
integer/Fraction reference. CTest consumes these same files for C++ floating
convolution and `dsp_core::dsp::convolve_q15`.

Inputs and coefficients are signed Q1.15 integers. Output is full convolution
including the zero-padded tail; state starts at zero. The fixed path accumulates
in int64, rounds once to nearest with ties away from zero and saturates to
[-32768, 32767]. At most 65536 taps are accepted. The float column is the
unclipped convolution of the quantized operands, not a separately designed
floating filter. Unclipped fixed output must agree within half a Q15 LSB.

Cases exercise impulse response, signed samples/taps, positive and negative
rounding ties, both saturation rails and the -32768 endpoint. Plain CSV allows
MATLAB (`readtable`) and a future RTL testbench to consume the identical data.
This is Python/C++ simulation evidence; MATLAB, RTL and board agreement remain
unmeasured. It does not establish filter-design quantization error or RF accuracy.
7 changes: 7 additions & 0 deletions verification/vectors/fir_q15/impulse_expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
q15,float64
8192,0.24999237060546875
16384,0.4999847412109375
8192,0.24999237060546875
0,0
0,0
0,0
5 changes: 5 additions & 0 deletions verification/vectors/fir_q15/impulse_input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
q15
32767
0
0
0
4 changes: 4 additions & 0 deletions verification/vectors/fir_q15/impulse_taps.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
q15
8192
16384
8192
6 changes: 6 additions & 0 deletions verification/vectors/fir_q15/negative_taps_expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
q15,float64
32767,1
1,3.0517578125e-05
-32767,-0.999969482421875
0,0
0,0
5 changes: 5 additions & 0 deletions verification/vectors/fir_q15/negative_taps_input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
q15
-32768
32767
0
0
3 changes: 3 additions & 0 deletions verification/vectors/fir_q15/negative_taps_taps.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
q15
-32768
-32768
6 changes: 6 additions & 0 deletions verification/vectors/fir_q15/saturation_expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
q15,float64
32766,0.99993896577507257
32767,1.9998779315501451
-1,-3.0516646802425385e-05
-32768,-1.99993896484375
-32767,-0.999969482421875
5 changes: 5 additions & 0 deletions verification/vectors/fir_q15/saturation_input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
q15
32767
32767
-32768
-32768
3 changes: 3 additions & 0 deletions verification/vectors/fir_q15/saturation_taps.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
q15
32767
32767
10 changes: 10 additions & 0 deletions verification/vectors/fir_q15/signed_expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
q15,float64
617,0.018829345703125
-1481,-0.045196533203125
17124,0.5225830078125
-24869,-0.75893783569335938
12288,0.37501144409179688
-4097,-0.12502288818359375
0,1.1444091796875e-05
0,-3.814697265625e-06
0,0
8 changes: 8 additions & 0 deletions verification/vectors/fir_q15/signed_input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
q15
1234
-2345
32767
-32768
1
-1
0
4 changes: 4 additions & 0 deletions verification/vectors/fir_q15/signed_taps.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
q15
16384
-8192
4096
7 changes: 7 additions & 0 deletions verification/vectors/fir_q15/ties_expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
q15,float64
1,1.52587890625e-05
-1,-1.52587890625e-05
2,4.57763671875e-05
-2,-4.57763671875e-05
16384,0.4999847412109375
-16384,-0.5
7 changes: 7 additions & 0 deletions verification/vectors/fir_q15/ties_input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
q15
1
-1
3
-3
32767
-32768
2 changes: 2 additions & 0 deletions verification/vectors/fir_q15/ties_taps.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
q15
16384
Loading