Skip to content

feat: temporal apodization windows for PhasorDetector - #428

Open
bruxillensis wants to merge 4 commits into
ymahlau:mainfrom
bruxillensis:pr2-detector-apodization
Open

feat: temporal apodization windows for PhasorDetector#428
bruxillensis wants to merge 4 commits into
ymahlau:mainfrom
bruxillensis:pr2-detector-apodization

Conversation

@bruxillensis

@bruxillensis bruxillensis commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Adds an optional apodization window to PhasorDetector, applied to each sample of its running DFT. The default (None) is the existing hard rectangular gate, so nothing changes unless it is set.

A rectangular gate leaks spectrally: a signal still ringing when the detector switches off hits a discontinuity, and the sidelobes land on neighbouring frequencies. Measuring a feature 1000x below a nearby strong tone, the gate overstates it by ~60x; a Tukey window brings it to the right order.

Windows live in core/window.py alongside the envelope helpers, which are extracted from the inline copies in objects/sources/profile.py so the source profiles and the windows share one definition of each shape. WindowProfile takes get_window(time), with apodization typed to it so a source profile is rejected at construction.

Continuous-mode scaling becomes the window's coherent gain 2 / sum(w), which reduces to the previous 2 / N for the rectangular case. The sum is taken over the recorded steps, so this composes with dft_subsample.

Summary by CodeRabbit

  • New Features
    • Added shared temporal window/envelope utilities and new WindowProfile implementations: GaussianWindowProfile and TukeyWindowProfile.
    • Enabled optional temporal apodization for PhasorDetector via WindowProfile, including coherent-gain correction for continuous recordings.
  • Documentation
    • Extended API docs to include WindowProfile, GaussianWindowProfile, and TukeyWindowProfile.
  • Refactor
    • Updated source temporal shaping to use shared Gaussian and linear ramp helpers for smoother transitions.
  • Tests
    • Added unit tests for envelope/window shapes, parameter validation, apodization edge cases (including rejection for invalid/underflowed windows), and detector subsampling behavior.
    • Added physics regression coverage to confirm reduced spectral leakage off DFT bins with Tukey windows.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reusable Gaussian, linear-ramp, and Tukey envelopes with public window profiles, integrates optional temporal apodization into PhasorDetector, reuses helpers in source profiles, and adds API documentation plus unit and physics regression tests.

Changes

Temporal windows and apodization

Layer / File(s) Summary
Window profiles and envelope helpers
src/fdtdx/core/window.py, src/fdtdx/__init__.py, src/fdtdx/objects/sources/profile.py, docs/source/07_api.rst, tests/unit/core/test_window.py
Adds JAX envelope functions and WindowProfile implementations, exports and documents the new classes, reuses helpers in source profiles, and tests envelope, profile, inheritance, validation, and regression behavior.
Phasor detector apodization
src/fdtdx/objects/detectors/phasor.py, tests/unit/objects/detectors/test_apodization.py
Adds optional window validation, cached weights, coherent-gain scaling, weighted accumulation, subsampling behavior, invalid-gain checks, and detector apodization coverage.
Physics leakage validation
tests/simulation/physics/detectors/test_apodization_leakage.py
Removes debug output from the FDTD regression fixture while retaining rectangular and Tukey detector leakage assertions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FDTDSimulation
  participant PhasorDetector
  participant WindowProfile
  participant PhasorAccumulation
  participant LeakageAssertions
  FDTDSimulation->>PhasorDetector: update sampled fields
  PhasorDetector->>WindowProfile: obtain cached window weight
  WindowProfile-->>PhasorDetector: return window weight
  PhasorDetector->>PhasorAccumulation: accumulate weighted phasor
  PhasorAccumulation-->>PhasorDetector: apply coherent-gain scaling
  PhasorDetector-->>LeakageAssertions: return detector magnitudes
  LeakageAssertions->>LeakageAssertions: compare amplitude and leakage thresholds
Loading

Possibly related PRs

  • ymahlau/fdtdx#406: Updates PhasorDetector subsampling and scaling logic that this apodization composes with.

Poem

A rabbit sees windows, Tukey and bright,
Soft paws trace the ramps through the night.
Phasors hop on with gain neatly spun,
While tests chase leakage from dusk to sun.
“A tidy new signal!” the rabbit declares.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding temporal apodization windows for PhasorDetector.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/fdtdx/core/window.py`:
- Around line 74-110: Validate constructor parameters in
GaussianWindowProfile.__post_init__ and TukeyWindowProfile.__post_init__: reject
sigma_time <= 0, end_time <= start_time, and alpha values outside the inclusive
[0, 1] range, using the project’s established validation/error convention. Add
regression tests covering each invalid case while preserving valid window
behavior.

In `@src/fdtdx/objects/detectors/phasor.py`:
- Around line 150-156: Update the class-level scaling documentation near
_static_scale to describe continuous-mode scaling as 2 / sum(w), including when
apodization is enabled, instead of stating 2 / N. Keep the existing pulse-mode
documentation unchanged.
- Around line 138-147: In the window-building logic that caches
_window_at_time_step_arr and _window_sum, validate immediately after masking
that the coherent-gain sum is finite and strictly positive, rejecting zero,
negative, or non-finite sums and weights before phasor processing can continue.
Add a regression test covering an out-of-interval/underflowed window and
non-finite weights, ensuring invalid windows are rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 29baa815-65d4-44e5-ad86-baeb775fc01d

📥 Commits

Reviewing files that changed from the base of the PR and between 6e841ba and 7ab7f8a.

📒 Files selected for processing (7)
  • docs/source/07_api.rst
  • src/fdtdx/__init__.py
  • src/fdtdx/core/window.py
  • src/fdtdx/objects/detectors/phasor.py
  • src/fdtdx/objects/sources/profile.py
  • tests/unit/core/test_window.py
  • tests/unit/objects/detectors/test_apodization.py

Comment thread src/fdtdx/core/window.py
Comment thread src/fdtdx/objects/detectors/phasor.py Outdated
Comment thread src/fdtdx/objects/detectors/phasor.py
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.52941% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.67%. Comparing base (6e841ba) to head (2e3b95c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/fdtdx/core/window.py 97.72% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #428      +/-   ##
==========================================
+ Coverage   90.52%   90.67%   +0.14%     
==========================================
  Files          92       94       +2     
  Lines       11920    12229     +309     
  Branches     1820     1871      +51     
==========================================
+ Hits        10791    11089     +298     
- Misses        783      788       +5     
- Partials      346      352       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bruxillensis
bruxillensis force-pushed the pr2-detector-apodization branch from 7ab7f8a to d2defef Compare July 29, 2026 15:11
Adds an optional `apodization` window to `PhasorDetector`, applied to each
sample of its running DFT. The default (`None`) is the existing hard
rectangular gate, so nothing changes unless it is set.

A rectangular gate leaks spectrally: a signal still ringing when the detector
switches off hits a discontinuity, and the sidelobes land on neighbouring
frequencies. Measuring a feature 1000x below a nearby strong tone, the gate
overstates it by ~60x; a Tukey window brings it to the right order.

Windows live in `core/window.py` alongside the envelope helpers, which are
extracted from the inline copies in `objects/sources/profile.py` so the source
profiles and the windows share one definition of each shape. `WindowProfile`
takes `get_window(time)`, with `apodization` typed to it so a source profile is
rejected at construction.

Continuous-mode scaling becomes the window's coherent gain `2 / sum(w)`, which
reduces to the previous `2 / N` for the rectangular case. The sum is taken over
the recorded steps, so this composes with `dft_subsample`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bruxillensis
bruxillensis force-pushed the pr2-detector-apodization branch from d2defef to 510c099 Compare July 29, 2026 17:16
Runs a CW plane wave and reads the same plane with two PhasorDetectors, one
rectangular and one Tukey, probing the tone plus two off-tone frequencies.

Both detectors are gated to the steady-state part of the run. The source's
linear startup ramp makes the field non-stationary for the first few periods,
and a rectangular gate weights that transient equally while a taper de-weights
it; ungated, that alone shifted the on-frequency amplitude by 6.7% for reasons
unrelated to leakage. Gated, the two agree to 0.05%, so the coherent-gain
correction is pinned to 1%.

Probe frequencies sit at fractional bin offsets -- integer offsets land on the
nulls of the rectangular window's kernel and hide the leakage entirely.
Measured suppression is 3.0x at 2.5 bins and 3.9x at 4.5 bins; the test requires
2x, and separately asserts the rectangular leakage is present so the comparison
cannot pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/simulation/physics/detectors/test_apodization_leakage.py (1)

113-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop the debug print from the fixture.

The module-scoped fixture prints the spectra on every run, adding noise to CI output. If the values are useful for triage, emit them only on failure via the existing assertion messages.

♻️ Proposed cleanup
-    print({k: np.array2string(v, precision=4) for k, v in out.items()})
     return out
🤖 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 `@tests/simulation/physics/detectors/test_apodization_leakage.py` at line 113,
Remove the unconditional debug print from the module-scoped fixture in
test_apodization_leakage.py. Preserve any useful diagnostic spectra by including
them in the existing assertion messages so they are emitted only when the
assertions fail.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@tests/simulation/physics/detectors/test_apodization_leakage.py`:
- Line 113: Remove the unconditional debug print from the module-scoped fixture
in test_apodization_leakage.py. Preserve any useful diagnostic spectra by
including them in the existing assertion messages so they are emitted only when
the assertions fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca7660f8-89bf-4a76-aaee-1d5fad53fa2d

📥 Commits

Reviewing files that changed from the base of the PR and between 510c099 and d4f826e.

📒 Files selected for processing (1)
  • tests/simulation/physics/detectors/test_apodization_leakage.py

bruxillensis and others added 2 commits July 30, 2026 09:54
Leftover from working out the tolerances; the values it printed are recorded in
the assertions and their docstrings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shipped assertions carried 12x-63,000x unused slack: they caught the gross
defect they were written for but would not have caught drift. Tolerances are now
set from measured values, per an audit that found no source defect.

  on-frequency amplitude (sim)   rel=1e-2  -> 2e-3   (measured 8.5e-4)
  leakage suppression (sim)      >2x       -> >2.5x  (measured 3.01x, 3.85x)
  CW amplitude, no window        abs=2e-2  -> 2e-3   (measured 5.7e-4)
  CW amplitude, Tukey            abs=3e-2  -> 1e-5   (measured 4.8e-7)
  CW amplitude, Tukey + stride   abs=3e-2  -> 1e-5   (measured 2.4e-7)

The apodized cases are ~1000x more accurate than the rectangular one because the
taper suppresses the single-frequency DFT's 2w residual; 1e-5 is ~84 float32 ULP,
so it is not tightened further.

Two tolerance-free tests are added. A rectangular Tukey (alpha=0) must reproduce
the un-apodized phasor bit-for-bit, which pins window construction and its
alignment with the OnOffSwitch mask -- an off-by-one at either recording edge is
absorbed by every amplitude tolerance above but fails this immediately. And
suppression must grow with bin offset, which catches a window that is applied but
wrong where a flat ratio bound would not.

The subsample sweep moves from strides 1/4/8 to 1/2/3/4. Stride 8 leaves 2.2
samples per period and aliases by design -- the detector warns about it -- so its
amplitude was necessarily bounded 30x looser while exercising no mechanism that
stride 4 does not; test_phasor_subsample.py covers heavy strides.

Verified: a mutation dropping the gain correction is caught by 6 tests, and a
0.4% scale drift that passed every old tolerance now fails 6. The simulation
amplitude test still passes under drift by design -- it is a ratio of two
detectors, so a common scale error cancels, which is what isolates the window's
effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bruxillensis

Copy link
Copy Markdown
Contributor Author

@ymahlau I think this is ready for a review. I tried to be more thorough in testing.

@l-berg

l-berg commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Thank you for this PR and the thorough test cases.

My only gripe is with the naming scheme. Having *Profile and *WindowProfile classes that do different things might be confusing to other users. What do you think about using TemporalWindow / GaussianWindow / TukeyWindow instead?

Other than that, it looks fine to merge.

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.

2 participants