Skip to content

Commit c89a72a

Browse files
quentin kuttenkulerclaude
authored andcommitted
Add BDCT kernel: paired radix-4 half-shift transform + DCT-IV
Adds a new header-only kernel (src/detail/bdct_kernel.hpp) alongside the Bruun real kernel, computing the native half-bin-shifted transform H[k] = sum_n x[n] exp(-2*pi*i*(k+1/2)*n/N) for real input via a paired radix-4 decimation. Each output position k is paired with its partner kp = M-1-k inside the radix-4 combine so the two share the three child twiddle multiplies (t, t^2, t^3), halving the twiddle multiplies of the naive radix-4 half-shift combine. Output is the contiguous packed lower half H[0..N/2-1]; no DC/Nyquist special case, no final reorder. The inverse is the exact algebraic inverse (machine-precision roundtrip). A length-N DCT-IV is built on top via a length-2N half-shift of the zero-padded input plus one output rotation. Details: - Templated for float and double (no long double; matches the real kernel). - Reuses heap_array, complex_t/complex_f32_t and the SIMD-backend macros from bruun_kernel.hpp; only the decimation arithmetic is new. - Explicit 128-bit SoA SIMD specialization for the double forward combine, running on NEON and SSE2 through the shared Bruun V2 macros; the scalar path remains the exact reference for every backend, precision and tail. Public API alongside the bfft headers: - include/bfft/bdct.h (C ABI; reuses bfft_complex / bfft_status) - include/bfft/bdct.hpp (RAII bfft::half_shift / bfft::dctiv) - src/bdct.cpp implements the C ABI into libbfft. - Makefile and CMakeLists wire the new source and headers into both builds. Validation: examples/dctiv_benchmark.cpp checks correctness in float and double via analytic exact tests (impulse half-shift, DCT-IV basis vector), an independent same-precision direct-DFT cross-check, and the exact inverse roundtrip, plus timing. Existing test suite still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f8c8f02 commit c89a72a

7 files changed

Lines changed: 985 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ option(BFFT_ENABLE_PFFFT_BENCHMARK "Enable PFFFT in the benchmark example when f
1616

1717
set(BFFT_PUBLIC_HEADERS
1818
include/bfft/bfft.h
19-
include/bfft/bfft.hpp)
19+
include/bfft/bfft.hpp
20+
include/bfft/bdct.h
21+
include/bfft/bdct.hpp)
2022

2123
set(BFFT_SOURCES
22-
src/bfft.cpp)
24+
src/bfft.cpp
25+
src/bdct.cpp)
2326

2427
set(BFFT_PLATFORM_LIBS)
2528
if(NOT WIN32)

Makefile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ LIB_CXXFLAGS := $(CXXFLAGS) $(AUTO_SIMD_FLAGS)
4646

4747
SRC := src/bfft.cpp
4848
OBJ := $(BUILD_DIR)/src/bfft.o
49+
BDCT_SRC := src/bdct.cpp
50+
BDCT_OBJ := $(BUILD_DIR)/src/bdct.o
51+
LIB_OBJS := $(OBJ) $(BDCT_OBJ)
4952
STATIC_LIB := $(BUILD_DIR)/lib$(LIB_NAME).a
5053
SHARED_LIB := $(BUILD_DIR)/lib$(LIB_NAME).so
5154
PC_FILE := $(BUILD_DIR)/$(LIB_NAME).pc
5255
BENCH := $(BUILD_DIR)/examples/benchmark
56+
DCTIV_BENCH := $(BUILD_DIR)/examples/dctiv_benchmark
5357
APPLE_BENCH := $(BUILD_DIR)/examples/apple_benchmark
5458
LOCALITY_PROBE := $(BUILD_DIR)/examples/locality_probe
5559
C_DEMO := $(BUILD_DIR)/examples/c_api_demo
@@ -81,10 +85,13 @@ $(BUILD_DIR):
8185
$(OBJ): $(SRC) include/bfft/bfft.h src/detail/bruun_kernel.hpp | $(BUILD_DIR)
8286
$(CXX) $(LIB_CPPFLAGS) $(LIB_CXXFLAGS) -c $< -o $@
8387

84-
$(STATIC_LIB): $(OBJ)
88+
$(BDCT_OBJ): $(BDCT_SRC) include/bfft/bdct.h include/bfft/bfft.h src/detail/bdct_kernel.hpp src/detail/bruun_kernel.hpp | $(BUILD_DIR)
89+
$(CXX) $(LIB_CPPFLAGS) $(LIB_CXXFLAGS) -c $< -o $@
90+
91+
$(STATIC_LIB): $(LIB_OBJS)
8592
$(AR) rcs $@ $^
8693

87-
$(SHARED_LIB): $(OBJ)
94+
$(SHARED_LIB): $(LIB_OBJS)
8895
$(CXX) -shared $(LDFLAGS) -o $@ $^ $(LDLIBS)
8996

9097
$(PC_FILE): pkgconfig/bfft.pc.in | $(BUILD_DIR)
@@ -94,7 +101,7 @@ $(PC_FILE): pkgconfig/bfft.pc.in | $(BUILD_DIR)
94101
-e 's|@PROJECT_VERSION@|$(VERSION)|g' \
95102
$< > $@
96103

97-
examples: $(BENCH) $(APPLE_EXAMPLES) $(LOCALITY_PROBE) $(C_DEMO) $(CPP_DEMO)
104+
examples: $(BENCH) $(DCTIV_BENCH) $(APPLE_EXAMPLES) $(LOCALITY_PROBE) $(C_DEMO) $(CPP_DEMO)
98105

99106
asm-check: $(ASM_OUTPUTS)
100107
@if [ -z "$(ASM_OUTPUTS)" ]; then echo "No x86 assembly variants supported by $(CXX)."; fi
@@ -108,6 +115,9 @@ $(SSE2_ASM): $(SRC) include/bfft/bfft.h src/detail/bruun_kernel.hpp | $(BUILD_DI
108115
$(BENCH): examples/benchmark.cpp include/bfft/bfft.hpp $(STATIC_LIB) | $(BUILD_DIR)
109116
$(CXX) $(CPPFLAGS) $(INCLUDES) $(CXXFLAGS) $(AUTO_SIMD_FLAGS) $< $(STATIC_LIB) $(LDLIBS) $(DL_LIBS) -o $@
110117

118+
$(DCTIV_BENCH): examples/dctiv_benchmark.cpp src/detail/bdct_kernel.hpp src/detail/bruun_kernel.hpp | $(BUILD_DIR)
119+
$(CXX) $(CPPFLAGS) $(INCLUDES) $(CXXFLAGS) $(AUTO_SIMD_FLAGS) $< $(LDLIBS) -o $@
120+
111121
$(APPLE_BENCH): examples/apple_benchmark.cpp include/bfft/bfft.hpp $(STATIC_LIB) | $(BUILD_DIR)
112122
$(CXX) $(CPPFLAGS) $(INCLUDES) $(CXXFLAGS) $< $(STATIC_LIB) $(LDLIBS) $(DL_LIBS) $(ACCELERATE_LIBS) -o $@
113123

@@ -167,13 +177,17 @@ install: all $(PC_FILE)
167177
$(INSTALL) -d $(DESTDIR)$(PREFIX)/lib/pkgconfig
168178
$(INSTALL) -m 0644 include/bfft/bfft.h $(DESTDIR)$(PREFIX)/include/bfft/bfft.h
169179
$(INSTALL) -m 0644 include/bfft/bfft.hpp $(DESTDIR)$(PREFIX)/include/bfft/bfft.hpp
180+
$(INSTALL) -m 0644 include/bfft/bdct.h $(DESTDIR)$(PREFIX)/include/bfft/bdct.h
181+
$(INSTALL) -m 0644 include/bfft/bdct.hpp $(DESTDIR)$(PREFIX)/include/bfft/bdct.hpp
170182
$(INSTALL) -m 0644 $(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/lib$(LIB_NAME).a
171183
$(INSTALL) -m 0755 $(SHARED_LIB) $(DESTDIR)$(PREFIX)/lib/lib$(LIB_NAME).so
172184
$(INSTALL) -m 0644 $(PC_FILE) $(DESTDIR)$(PREFIX)/lib/pkgconfig/$(LIB_NAME).pc
173185

174186
uninstall:
175187
rm -f $(DESTDIR)$(PREFIX)/include/bfft/bfft.h
176188
rm -f $(DESTDIR)$(PREFIX)/include/bfft/bfft.hpp
189+
rm -f $(DESTDIR)$(PREFIX)/include/bfft/bdct.h
190+
rm -f $(DESTDIR)$(PREFIX)/include/bfft/bdct.hpp
177191
rm -f $(DESTDIR)$(PREFIX)/lib/lib$(LIB_NAME).a
178192
rm -f $(DESTDIR)$(PREFIX)/lib/lib$(LIB_NAME).so
179193
rm -f $(DESTDIR)$(PREFIX)/lib/pkgconfig/$(LIB_NAME).pc

examples/dctiv_benchmark.cpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// DCT-IV / half-shift (BDCT) benchmark and validation.
2+
//
3+
// Validates the paired radix-4 half-shift kernel and the DCT-IV built on it,
4+
// in both double and single precision, without any long double (there is no
5+
// extended precision on the NEON target). Correctness is checked three ways:
6+
//
7+
// 1. analytic exact: half-shift of a unit impulse has |H[k]| == 1 with a
8+
// closed-form phase, and DCT-IV of a single basis vector is a clean spike;
9+
// 2. independent cross-check against a direct same-precision DFT;
10+
// 3. exact inverse roundtrip.
11+
//
12+
// Build (from repo root):
13+
// c++ -O3 -std=c++17 -Iinclude examples/dctiv_benchmark.cpp -lm -o build/examples/dctiv_benchmark
14+
15+
#include "../src/detail/bdct_kernel.hpp"
16+
17+
#include <chrono>
18+
#include <cmath>
19+
#include <cstdio>
20+
#include <cstdlib>
21+
#include <random>
22+
#include <vector>
23+
24+
namespace {
25+
26+
constexpr double PI = 3.141592653589793238462643383279502884;
27+
28+
template <class RT>
29+
std::vector<RT> make_input(int n, std::mt19937_64& rng) {
30+
std::vector<RT> x(static_cast<std::size_t>(n));
31+
std::uniform_real_distribution<double> dist(-1.0, 1.0);
32+
for (int i = 0; i < n; ++i) x[static_cast<std::size_t>(i)] = static_cast<RT>(dist(rng));
33+
return x;
34+
}
35+
36+
// --- correctness for one precision -----------------------------------------
37+
38+
template <class HS, class DCT, class CT, class RT>
39+
void run_correctness(int N, std::mt19937_64& rng, const char* tag) {
40+
HS hs(N);
41+
DCT dct(N);
42+
const int bins = N / 2;
43+
44+
// (1a) half-shift of a unit impulse at n0: H[k] = exp(-2*pi*i*(k+1/2)*n0/N).
45+
std::vector<RT> imp(static_cast<std::size_t>(N), static_cast<RT>(0));
46+
const int n0 = 1;
47+
imp[static_cast<std::size_t>(n0)] = static_cast<RT>(1);
48+
std::vector<CT> Himp(static_cast<std::size_t>(bins));
49+
hs.forward(imp.data(), Himp.data());
50+
double imp_err = 0.0;
51+
for (int k = 0; k < bins; ++k) {
52+
const double ang = -2.0 * PI * (static_cast<double>(k) + 0.5) *
53+
static_cast<double>(n0) / static_cast<double>(N);
54+
const double dr = static_cast<double>(Himp[static_cast<std::size_t>(k)].re) - std::cos(ang);
55+
const double di = static_cast<double>(Himp[static_cast<std::size_t>(k)].im) - std::sin(ang);
56+
imp_err = std::max(imp_err, std::sqrt(dr * dr + di * di));
57+
}
58+
59+
// (1b) DCT-IV of basis vector m: x[n]=cos(pi*(m+1/2)*(n+1/2)/N) -> (N/2)*delta_km.
60+
const int m = (N >= 8) ? 3 : 1;
61+
std::vector<RT> basis(static_cast<std::size_t>(N));
62+
for (int n = 0; n < N; ++n) {
63+
basis[static_cast<std::size_t>(n)] = static_cast<RT>(
64+
std::cos(PI * (m + 0.5) * (n + 0.5) / N));
65+
}
66+
std::vector<RT> c(static_cast<std::size_t>(N));
67+
dct.forward(basis.data(), c.data());
68+
double dct_err = 0.0;
69+
for (int k = 0; k < N; ++k) {
70+
const double expect = (k == m) ? 0.5 * N : 0.0;
71+
dct_err = std::max(dct_err, std::fabs(static_cast<double>(c[static_cast<std::size_t>(k)]) - expect));
72+
}
73+
74+
// (2) random signal vs direct same-precision DFT, and (3) roundtrip.
75+
std::vector<RT> x = make_input<RT>(N, rng);
76+
std::vector<CT> H(static_cast<std::size_t>(bins));
77+
hs.forward(x.data(), H.data());
78+
double xcheck = 0.0, ref_mag = 0.0;
79+
for (int k = 0; k < bins; ++k) {
80+
double sr = 0.0, si = 0.0;
81+
for (int n = 0; n < N; ++n) {
82+
const double ang = -2.0 * PI * (k + 0.5) * n / N;
83+
sr += static_cast<double>(x[static_cast<std::size_t>(n)]) * std::cos(ang);
84+
si += static_cast<double>(x[static_cast<std::size_t>(n)]) * std::sin(ang);
85+
}
86+
const double dr = static_cast<double>(H[static_cast<std::size_t>(k)].re) - sr;
87+
const double di = static_cast<double>(H[static_cast<std::size_t>(k)].im) - si;
88+
xcheck = std::max(xcheck, std::sqrt(dr * dr + di * di));
89+
ref_mag = std::max(ref_mag, std::sqrt(sr * sr + si * si));
90+
}
91+
92+
std::vector<RT> xr(static_cast<std::size_t>(N));
93+
hs.inverse(H.data(), xr.data());
94+
double round_err = 0.0;
95+
for (int n = 0; n < N; ++n) {
96+
round_err = std::max(round_err,
97+
std::fabs(static_cast<double>(xr[static_cast<std::size_t>(n)] -
98+
x[static_cast<std::size_t>(n)])));
99+
}
100+
101+
std::printf("%-6s N %7d hs_impulse %.3e dctiv_basis %.3e "
102+
"hs_vs_dft(rel) %.3e roundtrip %.3e\n",
103+
tag, N, imp_err, dct_err,
104+
ref_mag > 0 ? xcheck / ref_mag : 0.0, round_err);
105+
}
106+
107+
// --- timing ----------------------------------------------------------------
108+
109+
template <class HS, class DCT, class CT, class RT>
110+
void run_timing(int N, std::mt19937_64& rng, const char* tag) {
111+
std::vector<RT> x = make_input<RT>(N, rng);
112+
HS hs(N);
113+
DCT dct(N);
114+
std::vector<CT> H(static_cast<std::size_t>(N / 2));
115+
std::vector<RT> c(static_cast<std::size_t>(N));
116+
117+
long iters = 1;
118+
{
119+
const double per = static_cast<double>(N) * std::log2(static_cast<double>(N));
120+
iters = static_cast<long>(2.0e8 / per);
121+
if (iters < 1) iters = 1;
122+
}
123+
124+
using clock = std::chrono::steady_clock;
125+
hs.forward(x.data(), H.data());
126+
dct.forward(x.data(), c.data());
127+
128+
auto t0 = clock::now();
129+
for (long i = 0; i < iters; ++i) hs.forward(x.data(), H.data());
130+
auto t1 = clock::now();
131+
for (long i = 0; i < iters; ++i) dct.forward(x.data(), c.data());
132+
auto t2 = clock::now();
133+
134+
const double hs_ns = std::chrono::duration<double, std::nano>(t1 - t0).count() / iters;
135+
const double dct_ns = std::chrono::duration<double, std::nano>(t2 - t1).count() / iters;
136+
std::printf("%-6s N %7d halfshift %10.1f ns dctiv %10.1f ns (%ld iters)\n",
137+
tag, N, hs_ns, dct_ns, iters);
138+
}
139+
140+
} // namespace
141+
142+
int main(int argc, char** argv) {
143+
std::mt19937_64 rng(12345);
144+
145+
std::printf("backend: %s\n", bruun::simd_backend_name());
146+
147+
std::printf("\n== correctness (double) ==\n");
148+
for (int N = 4; N <= 8192; N <<= 1)
149+
run_correctness<bdct::half_shift_plan, bdct::dctiv_plan, bdct::complex_t, double>(N, rng, "f64");
150+
151+
std::printf("\n== correctness (float) ==\n");
152+
for (int N = 4; N <= 8192; N <<= 1)
153+
run_correctness<bdct::half_shift_plan_f32, bdct::dctiv_plan_f32, bdct::complex_f32_t, float>(N, rng, "f32");
154+
155+
std::printf("\n== timing (double) ==\n");
156+
for (int N = 64; N <= (1 << 20); N <<= 1)
157+
run_timing<bdct::half_shift_plan, bdct::dctiv_plan, bdct::complex_t, double>(N, rng, "f64");
158+
159+
std::printf("\n== timing (float) ==\n");
160+
for (int N = 64; N <= (1 << 20); N <<= 1)
161+
run_timing<bdct::half_shift_plan_f32, bdct::dctiv_plan_f32, bdct::complex_f32_t, float>(N, rng, "f32");
162+
163+
(void)argc;
164+
(void)argv;
165+
return 0;
166+
}

include/bfft/bdct.h

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#ifndef BFFT_BDCT_H
2+
#define BFFT_BDCT_H
3+
4+
/* Public C ABI for the BDCT kernel: the native half-bin-shifted real transform
5+
and the DCT-IV built on it. This header lives alongside <bfft/bfft.h> and
6+
reuses its complex and status types. */
7+
8+
#include <bfft/bfft.h>
9+
10+
#include <stddef.h>
11+
12+
#ifdef __cplusplus
13+
extern "C" {
14+
#endif
15+
16+
/* Opaque half-shift transform plan. Create with bdct_plan_create and destroy
17+
with bdct_plan_destroy. */
18+
typedef struct bdct_plan bdct_plan;
19+
20+
/* Opaque DCT-IV plan. */
21+
typedef struct bdct_dctiv_plan bdct_dctiv_plan;
22+
23+
/* SIMD backend selected by the build target (shared with the BFFT kernel). */
24+
const char* bdct_backend_name(void);
25+
26+
/* -- Half-shift transform --------------------------------------------------
27+
28+
Forward maps N real samples to N/2 packed complex bins:
29+
30+
H[k] = sum_{n=0}^{N-1} x[n] * exp(-2*pi*i*(k+1/2)*n/N), k = 0..N/2-1.
31+
32+
The upper half is recovered by H[N-1-k] = conj(H[k]). Inverse maps the N/2
33+
packed bins back to the N real samples exactly. */
34+
35+
/* Create a half-shift plan for a power-of-two transform size N >= 2. */
36+
bfft_status bdct_plan_create(size_t n, bdct_plan** plan);
37+
38+
/* Destroy a plan. Passing NULL is allowed. */
39+
void bdct_plan_destroy(bdct_plan* plan);
40+
41+
/* Transform size N, and the packed bin count N/2. Return 0 for a NULL plan. */
42+
size_t bdct_plan_size(const bdct_plan* plan);
43+
size_t bdct_plan_bins(const bdct_plan* plan);
44+
45+
/* Double-precision forward and inverse. input/output have N doubles; the packed
46+
spectrum has bdct_plan_bins(plan) complex values. */
47+
bfft_status bdct_forward(const bdct_plan* plan,
48+
const double* input,
49+
bfft_complex* output);
50+
bfft_status bdct_inverse(const bdct_plan* plan,
51+
const bfft_complex* input,
52+
double* output);
53+
54+
/* Single-precision forward and inverse. */
55+
bfft_status bdct_forward_f32(const bdct_plan* plan,
56+
const float* input,
57+
bfft_complex_f32* output);
58+
bfft_status bdct_inverse_f32(const bdct_plan* plan,
59+
const bfft_complex_f32* input,
60+
float* output);
61+
62+
/* -- DCT-IV ----------------------------------------------------------------
63+
64+
Unnormalized orthogonal DCT-IV:
65+
66+
C[k] = sum_{n=0}^{N-1} x[n] * cos( (pi/N) * (k+1/2) * (n+1/2) ).
67+
68+
Forward and inverse are real-to-real of length N. DCT-IV is its own inverse
69+
up to a 2/N scale, which bdct_dctiv_inverse applies. */
70+
71+
/* Create a DCT-IV plan for a power-of-two transform size N >= 2. */
72+
bfft_status bdct_dctiv_plan_create(size_t n, bdct_dctiv_plan** plan);
73+
74+
/* Destroy a DCT-IV plan. Passing NULL is allowed. */
75+
void bdct_dctiv_plan_destroy(bdct_dctiv_plan* plan);
76+
77+
/* Transform size N, or 0 for a NULL plan. */
78+
size_t bdct_dctiv_plan_size(const bdct_dctiv_plan* plan);
79+
80+
/* Double-precision forward (unnormalized) and inverse (scaled by 2/N). input
81+
and output have N doubles. */
82+
bfft_status bdct_dctiv_forward(const bdct_dctiv_plan* plan,
83+
const double* input,
84+
double* output);
85+
bfft_status bdct_dctiv_inverse(const bdct_dctiv_plan* plan,
86+
const double* input,
87+
double* output);
88+
89+
/* Single-precision forward and inverse. */
90+
bfft_status bdct_dctiv_forward_f32(const bdct_dctiv_plan* plan,
91+
const float* input,
92+
float* output);
93+
bfft_status bdct_dctiv_inverse_f32(const bdct_dctiv_plan* plan,
94+
const float* input,
95+
float* output);
96+
97+
#ifdef __cplusplus
98+
}
99+
#endif
100+
101+
#endif

0 commit comments

Comments
 (0)