Skip to content

Add charge diffusion to optika.sensors.vmr_signal - #195

Merged
roytsmart merged 5 commits into
mainfrom
feature/vmr-signal-diffusion
Aug 17, 2026
Merged

Add charge diffusion to optika.sensors.vmr_signal#195
roytsmart merged 5 commits into
mainfrom
feature/vmr-signal-diffusion

Conversation

@roytsmart

Copy link
Copy Markdown
Collaborator

Summary

optika.sensors.vmr_signal previously ignored charge diffusion, so it disagreed with the Monte Carlo signal() function for sensors with a field-free region (it overestimated the VMR by ~50% for EUV wavelengths on a device like the e2v CCD97).

This PR generalizes the analytic VMR to include charge diffusion. The existing expression is rewritten compactly as

$$F(N_e'') = 1 + \left( \overline{n} + \mathcal{F} - 1 \right) \frac{\langle \eta^2(z) \rangle}{\langle \eta(z) \rangle},$$

where the constant term is the uncorrelated (Poisson) component and the second term is the photon-correlated component, arising because the $\sim \overline{n}$ electrons produced by a single photon are measured together in the same pixel. Charge diffusion weakens exactly this correlation: weighting the correlated component by the probability $D(z)$ that two electrons from the same photon land in the same pixel gives

$$F(N_e'') = 1 + \left( \overline{n} + \mathcal{F} - 1 \right) \frac{\langle D(z) , \eta^2(z) \rangle}{\langle \eta(z) \rangle}.$$

For the Gaussian diffusion kernel used by electrons_measured, $D(z)$ has a closed form per axis (averaged over the photon's uniform sub-pixel position); only the depth average requires numerical quadrature, performed in CDF space of the truncated-exponential absorption-depth distribution.

Changes

  • vmr_signal gains thickness_depletion, thickness_substrate, and width_pixel parameters (defaults mirror signal, so no diffusion and no behavior change by default) plus a diffusion toggle alongside shot/fano/pcc. The derivation is documented in the Notes section, with a new example comparing against a diffused Monte Carlo.
  • New private helper _probability_same_pixel implementing the per-axis pair-coincidence probability.
  • The correction is threaded up the uncertainty stack: AbstractSensorMaterial.uncertainty (and implementations) gain a width_pixel parameter, and AbstractImagingSensor.uncertainty passes width_pixel=self.width_pixel, mirroring what expose() already does for the signal itself. expose(uncertainty=True) therefore now attaches a width consistent with its own diffused signal realization.

Validation

  • Against signal() with thickness_depletion=2 μm, wrap=True, 100–1000 Å: agreement within Monte Carlo noise (~0.5%) at every wavelength (e.g. 9.37 vs 9.37 electron at 100 Å, where the no-diffusion formula gives 14.3).
  • A standalone Monte Carlo replicating the diffusion model (shared depth and sub-pixel position per photon, binomial CCE, Fano noise) confirms the formula to ~0.1%, including the coupled PCC + diffusion case.
  • The 1001-point quadrature is converged to ~4e-5 relative error in the worst case (absorption length ≪ field-free thickness).
  • New test_vmr_signal_diffusion compares the analytic result against signal() to 10%, and the shared test_uncertainty now exercises width_pixel across every material fixture.

The analytic result assumes uniform illumination and a periodic pixel grid (wrap=True); it remains a good approximation away from the edges of a sensor with wrap=False.

🤖 Generated with Claude Code

https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb

Generalize the analytic variance-to-mean ratio of the sensor signal to
account for charge diffusion, so that it matches the Monte Carlo model
in `electrons_measured` for sensors with a field-free region.

The VMR is expressed as an uncorrelated (Poisson) term plus a
photon-correlated term weighted by the probability that two electrons
from the same photon land in the same pixel, which has a closed form
per axis and is averaged over absorption depth by numerical quadrature.

`vmr_signal` gains `thickness_depletion`, `thickness_substrate`,
`width_pixel`, and `diffusion` parameters, and the correction is
threaded through the material and sensor `uncertainty` methods so that
`expose(uncertainty=True)` attaches a width consistent with its own
diffused signal realization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.58%. Comparing base (43f7e49) to head (2ecd0d3).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #195   +/-   ##
=======================================
  Coverage   99.57%   99.58%           
=======================================
  Files         118      118           
  Lines        6880     6933   +53     
=======================================
+ Hits         6851     6904   +53     
  Misses         29       29           
Flag Coverage Δ
unittests 99.58% <100.00%> (+<0.01%) ⬆️

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 3 commits August 17, 2026 13:00
The multinomial fast path (#196) and PMF caching (#197) make the
Monte Carlo cheaper, and since the VMR is independent of the
illumination level, fewer photons per pixel buy more wavelength
samples at no statistical cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb
Match the wavelength range of the first VMR example (10 AA to 1 um);
the multinomial fast path makes the high-quantum-yield short-wavelength
end affordable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb
@roytsmart
roytsmart merged commit 05f161c into main Aug 17, 2026
17 of 18 checks passed
@roytsmart
roytsmart deleted the feature/vmr-signal-diffusion branch August 17, 2026 20:47
roytsmart added a commit to roytsmart/ccd-noise-paper that referenced this pull request Aug 17, 2026
`optika` reworked the sensor-material API from `(rays, normal)` to
`(wavelength, direction=..., n=..., normal=...)`, and both `direction`
and `normal` now default to normal incidence, which is all this article
ever used. Ten call sites across seven modules collapse accordingly,
along with the `RayVectorArray` construction each one needed.

`optika.sensors.vmr_signal()` now models charge diffusion itself
(sun-data/optika#195), replacing the `mean_charge_capture()` and
`vmr_diffusion()` chain with the `thickness_depletion`,
`thickness_substrate`, `width_pixel`, and `diffusion` parameters. The
shot, Fano, and PCC curves pass `diffusion=False` so they remain
undiffused; only the per-instrument curves diffuse.

`optika.sensors.signal()` likewise performs the charge-diffusion Monte
Carlo internally, so `simulations.electrons_measured()` no longer
convolves with a separate diffusion kernel. That convolution produced
fractional electrons and discarded the partition noise arising because
each electron lands in exactly one pixel; `signal()` samples the
per-electron distribution instead. `diffusion.kernel()` is retained,
since the kernel figure in Section 2 still draws it.

Also:

- Pinned `optika>=2.2,<3` and `aastex>=0.4.0,<0.5`. The unbounded pins
  are what allowed this breakage to accumulate silently.
- Raised `requires-python` to 3.12 and moved the test matrix to
  3.12/3.13/3.14. `optika` requires 3.11 or newer, so the 3.10 job could
  never have installed it.
- Bumped `actions/checkout` to v4, `actions/setup-python` to v5, and
  `codecov/codecov-action` to v7, renaming its removed `file` input.
- Declared `vmr_stern`, `keywords`, and `acknowledgements` in `__all__`,
  and dropped the stale `fano_factor` entry, so `ruff` passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011q4461XE8hCcZCKsViMQC1
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