Skip to content

Add the covariance between pixels that charge diffusion gives the noise - #256

Draft
roytsmart wants to merge 2 commits into
feature/diffusion-kernelfrom
feature/diffusion-covariance
Draft

roytsmart wants to merge 2 commits into
feature/diffusion-kernelfrom
feature/diffusion-covariance

Conversation

@roytsmart

Copy link
Copy Markdown
Collaborator

Stacked on #255 (→ #251 → #244). Requested in #255 (comment) to model the correlated noise of Hinode/XRT.

What it adds

Charge diffusion spreads the electrons of each photon over several pixels, which correlates the noise of neighboring pixels. Under uniform illumination the covariance depends only on the offset between the two pixels. This PR computes it.

  • AbstractDiffusionModel.kernel_pair(depth, thickness_substrate, width_pixel, axis_x, axis_y, num=None)
    • It returns the probability that two electrons of the same photon are collected Δ pixels apart, averaged over where the photon lands in its pixel.
    • It is built from the public mixture added to Add SlabDiffusionModel, a model of diffusion across the field-free region #251. Given the two Gaussians the electrons are drawn from, their separation is Gaussian with variance vᵢ + vⱼ. Along each axis, a separation δ puts them Δ pixels apart with probability (1 − |δ − Δ|)₊, which is the same rule as a single electron. So each pair of Gaussians has the existing _kernel_1d with variance vᵢ + vⱼ.
    • Its center is probability_same_pixel, and it sums to one.
    • _num_kernel(..., pair=True) sizes it from the tails of the separations. For the slab model it is about √2 times as wide as kernel: 13 pixels against 11 for 13 µm pixels.
  • optika.sensors.covariance_signal(wavelength, *, axis_x, axis_y, ...)
    • It gives the covariance per absorbed photon in e²/ph, as a FunctionArray over offsets. It is a sibling of vmr_signal and kernel_signal.

    • It is written as the undiffused variance at the center plus a term that only moves variance between pixels:

      C_Δ = σ₀² δ_Δ0 + n̄(n̄ + 𝓕 − 1) ⟨(P_Δ − δ_Δ0) η²⟩

      Here σ₀² is vmr_signal(diffusion=None) × iqy × cce.

    • So its center equals vmr_signal × iqy × cce exactly, and its sum equals the undiffused variance, at every wavelength from 1 to 7000 Å. Writing C_Δ directly as n̄⟨η⟩δ + n̄(n̄+𝓕−1)⟨P_Δ η²⟩ would mix vmr_signal's infinite substrate with the truncated depth average, and would only match between about 20 and 5000 Å.

  • kernel_covariance
    • It is added to AbstractSensorMaterial (abstract), IdealSensorMaterial (a single pixel of 1 e²/ph), AbstractBackIlluminatedSiliconSensorMaterial and AbstractImagingSensor, next to kernel and kernel_variance.
    • Under uniform illumination, the sum of kernel_variance equals the center of kernel_covariance. kernel_variance's docstring now says the two differ.

The docstring example plots the correlation coefficients for a CCD97 with 16 µm pixels at 304 Å: 0.239 side by side and 0.084 diagonally.

Checks

  • kernel_pair:
    • Its center matches probability_same_pixel to 2e-16.
    • It sums to 1 within 1e-7 and is symmetric.
    • Along each axis it matches a brute-force sum over pixels and positions, with each electron drawn from the mixture, to 1e-7 for the Janesick and slab models.
  • covariance_signal:
    • Its center matches vmr_signal × iqy × cce to 8e-16 from 1 to 7000 Å.
    • Its sum matches the undiffused variance to 5e-8.
  • Monte Carlo:
    • The test uses flat fields of 20 photons per pixel at 304 Å on a periodic 64 × 64 grid, over 64 frames, for the Janesick and slab models with cce_backsurface = 0.2.
    • It compares the covariance at offsets 0, (1, 0), (1, 1) and (2, 0) to six times the standard error from the scatter between frames, so the tolerance isn't hand-picked.
    • Over 8 repeated runs the z-scores had an rms of 0.65 to 1.35 with no bias. The relative standard errors are 0.3% at the center, 0.8% side by side and 1.6% diagonally.
    • The chance of a random failure per CI run is about 1e-6, and the test takes under a second per model.
  • Results: 1555 tests pass in optika/sensors and optika/systems/_linear_test.py locally. Patch coverage is 211/211, measured locally.

Not in scope

  • Non-uniform illumination: the covariance there depends on where the two pixels are, and so does the exact per-pixel variance. expose and its uncertainty are unchanged.
  • XRT's JPEG compression: it belongs in hinode.
  • The physics of X-ray charge clouds: the model treats each electron as diffusing independently from one point. At X-ray energies the initial size and self-repulsion of the cloud correlate the electrons and narrow the pair separations. The Monte Carlo makes the same assumption, so the tests can't catch this.

Like the rest of the stack, this stays off main until the 4.0 release is decided.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE

@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (60a2c44) to head (3b44daa).

Additional details and impacted files
@@                     Coverage Diff                     @@
##           feature/diffusion-kernel      #256    +/-   ##
===========================================================
  Coverage                    100.00%   100.00%            
===========================================================
  Files                           133       133            
  Lines                         11553     11773   +220     
===========================================================
+ Hits                          11553     11773   +220     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

roytsmart and others added 2 commits October 13, 2026 11:59
Charge diffusion spreads the electrons of each photon over several pixels,
which correlates the noise of neighboring pixels. Under uniform
illumination the covariance depends only on the offset between them.

- AbstractDiffusionModel.kernel_pair: the probability that two electrons
  of the same photon are collected a given offset apart, from the pairs of
  Gaussians of the mixture. Its center is probability_same_pixel and it
  sums to one. _num_kernel sizes it from the separations of the pairs.
- optika.sensors.covariance_signal: the covariance per absorbed photon.
  It is written as the variance without diffusion in the center plus a
  term that only moves variance between pixels, so its center is
  vmr_signal times the gain and its sum is the variance without diffusion
  at every wavelength.
- kernel_covariance on the sensor materials and on AbstractImagingSensor,
  next to kernel and kernel_variance.

The Monte Carlo test compares the covariance with flat fields simulated on
a periodic grid at four offsets, to six times the standard error from the
scatter between 64 frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE
- The docstrings of the kernels of pairs say that a default kernel of more
  than 101 pixels raises an error, and that of `covariance_signal` that a
  pixel of zero width turns off diffusion.
- The tests check that zero-width pixels give the covariance without a
  model, and that the covariance of a model holding uncertain values is
  that of its nominal values and of each sample.
- Every test function this PR adds or changes is annotated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE
@roytsmart
roytsmart force-pushed the feature/diffusion-kernel branch from 40dc0c4 to 60a2c44 Compare October 13, 2026 18:01
@roytsmart
roytsmart force-pushed the feature/diffusion-covariance branch from 1ac8e1f to 3b44daa Compare October 13, 2026 18:01
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.

1 participant