Skip to content

fix: real-domain spectrum renders as +-fc conjugate-symmetric Euler pairs - #40

Merged
striderZA merged 11 commits into
masterfrom
fix/spectrum-euler-conjugate-symmetry
Aug 4, 2026
Merged

fix: real-domain spectrum renders as +-fc conjugate-symmetric Euler pairs#40
striderZA merged 11 commits into
masterfrom
fix/spectrum-euler-conjugate-symmetry

Conversation

@striderZA

Copy link
Copy Markdown
Owner

Summary

Fixes #39. In the "real" (analog) signal domain — everywhere upstream of the ADC's DDC — the spectrum display now correctly honors Euler's formula: a real cosine at fc is physically 0.5*(exp(j*2*pi*fc*t) + exp(-j*2*pi*fc*t)), i.e. equal-power content at both +fc and -fc. The spectrum-analyzer render path previously collapsed this to one full-power bin at +fc with no representation of the mirror.

Design

Full design doc: docs/superpowers/specs/2026-08-03-spectrum-euler-conjugate-symmetry-design.md
Implementation plan: docs/superpowers/plans/2026-08-03-spectrum-euler-conjugate-symmetry.md

  • New Spectrum::is_complex_baseband flag (default false), propagated through every pass-through component exactly like fs_Hz already is.
  • New conjugateSymmetricExpand() helper in common/spectrum.h: splits each non-DC tone into +fc/-fc half-power (-3.0103 dB) entries per Euler's formula. DC tones pass through unchanged.
  • Interior DSP is untouched by design: the signal generator, nonlinear_model.h's harmonic/IM math, every gain/filter/S-parameter stage, and the mixer keep operating on the existing collapsed single-entry-per-tone representation — splitting there would corrupt the nonlinear model's real-power-calibrated harmonic math.
  • The expansion happens at exactly one point: the spectrum-analyzer render path, only for real-domain (is_complex_baseband == false) spectra.
  • AdcEngine's output now sets is_complex_baseband = true. Its DDC power/phase math (alias_frequency, NCO-shift/windowing) is unchanged — verified against the repo's own docs/resources/rf_adc_info.md, image rejection there already correctly discards the mirror, so no additional compensation is needed.

Testing

New standalone executable tests/test_signal_domain.cpp (required by this toolchain's MinGW-w64 TEST_CASE registration ceiling — see tests/AGENTS.md), 24 test cases / 46 assertions, covering:

  • The is_complex_baseband flag defaulting and propagation through every component (generator, amplifier, attenuator, combiner, equalizer, ideal filter, coax, splitter, mixer, PFB channelizer, touchstone S-parameter data, ADC).
  • conjugateSymmetricExpand() unit tests (real tone split, DC tone passthrough, multiple tones, empty input).
  • Spectrum-analyzer render tests: a real-domain tone renders as a +/-fc half-power pair; a complex-baseband tone renders unchanged (no mirroring).
  • Regression pin: the existing "ADC DDC preserves tone power and phase" test continues to pass with unchanged expected values.

Full existing suite (build/bin/tests.exe) verified unchanged throughout every commit: 217 test cases / 65522 assertions, zero regressions.

Commits

7 implementation tasks + design/plan docs + a DOX pass (updated common/AGENTS.md and tests/AGENTS.md per this repo's binding documentation contract) + one review-driven perf fix (avoid an unnecessary tone-vector copy in the common complex-baseband render path).

@striderZA striderZA left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review: PR #40 — Real-domain spectrum Euler/conjugate-symmetry fix

Verified locally (worktree .worktrees/spectrum-euler, commit 2f0b561):

  • Full build: clean (cmake --build build, no warnings from touched files).
  • build/bin/tests.exe: 217 test cases / 65522 assertions, all pass — matches PR claim, no regressions.
  • build/bin/test_signal_domain.exe: 24 test cases / 46 assertions, all pass — matches PR claim.

Design review:

  • The core physics is correct: conjugateSymmetricExpand splits non-DC tones into ±fc pairs at P - 10*log10(2) dBm each, DC passes through unchanged. Confirmed against Euler's formula.
  • Scoping decision to keep interior DSP (generator, nonlinear_model.h, mixer, gain/filter/S-param stages) on the collapsed single-entry representation is correct and well-justified — splitting there would break the amplitude-calibrated harmonic/IM math. Render-time-only expansion is the right call even though issue #39's "Expected behaviour" text nominally also asks for the representation to change; the design doc's §2 rationale for not doing that is sound.
  • ADC power bookkeeping analysis (§5 of the design doc) checks out: since interior tones are never actually split before reaching the ADC, no compensation gain is needed on the DDC output — adding one would double-count and break the existing "ADC DDC preserves tone power and phase" regression test. Confirmed this test still passes unmodified.
  • is_complex_baseband propagation was checked line-by-line against every update() branch in amplifier, attenuator, combiner, equalizer, ideal_filter, coax, splitter, mixer, pfb_channelizer, touchstone/s_parameter_data.cpp. Coverage is complete for every branch that assigns out.tones.
  • renderCombinedSpectrum reuses the fixed integratePowerPerBin, so multi-spectrum overlay rendering picks up the fix for free — no separate gap there.
  • Frequency grids for all real-domain producers span [MIN_FREQ, MAX_FREQ] = [-20GHz, +20GHz] (buildDefaultFrequencyGrid / signal generator), so the -fc mirror always has a bin to land in; no silent-drop risk from a positive-only grid.

Minor, non-blocking findings:

  1. combiner_engine.cpp's two N < 2 degenerate-frequency-grid early-return branches (lines ~103 and ~198) exit before the new out.is_complex_baseband assignment runs, so the flag can go stale in that edge case. This is not a new bug — it's exact parity with the pre-existing out.fs_Hz gap in the same branches (also unset there today). Not worth blocking on; flag if fs_Hz staleness there is ever fixed, fix is_complex_baseband alongside it.
  2. Merge conflict with current master: master has since merged #38 which also appends a new test executable block at the end of tests/CMakeLists.txt (test_issue37_pfb_input_removal), colliding textually with this PR's test_signal_domain block at the same insertion point. Trivial rebase (keep both add_executable/add_test blocks), but the PR needs it before merge — GitHub reports MERGEABLE: DIRTY.

Verdict: Approve pending a rebase onto current master to resolve the tests/CMakeLists.txt conflict. No functional or physics concerns.

@striderZA
striderZA force-pushed the fix/spectrum-euler-conjugate-symmetry branch from 2f0b561 to ca3feda Compare August 4, 2026 18:08
@striderZA

Copy link
Copy Markdown
Owner Author

Rebased onto current master (10abe42) to resolve the tests/CMakeLists.txt conflict with #38's test_issue37_pfb_input_removal — both new test-executable blocks kept, additive change only. Rebuilt clean and reran: tests.exe 217/65522, test_signal_domain.exe 24/46, test_issue37_pfb_input_removal.exe 4/4 — all pass. Force-pushed (2f0b561ca3feda, same 11 commits, only the CMakeLists conflict resolved). PR is now MERGEABLE/CLEAN.

@striderZA striderZA changed the title Real-domain spectrum: render as +-fc conjugate-symmetric Euler pairs fix: real-domain spectrum renders as +-fc conjugate-symmetric Euler pairs Aug 4, 2026
@striderZA
striderZA merged commit 1ff0ec1 into master Aug 4, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Real-signal spectrum should show conjugate-symmetric tones per Euler's formula

1 participant