Add charge diffusion to optika.sensors.vmr_signal - #195
Merged
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb
This was referenced Aug 17, 2026
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
optika.sensors.vmr_signalpreviously ignored charge diffusion, so it disagreed with the Monte Carlosignal()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
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
For the Gaussian diffusion kernel used by$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.
electrons_measured,Changes
vmr_signalgainsthickness_depletion,thickness_substrate, andwidth_pixelparameters (defaults mirrorsignal, so no diffusion and no behavior change by default) plus adiffusiontoggle alongsideshot/fano/pcc. The derivation is documented in the Notes section, with a new example comparing against a diffused Monte Carlo._probability_same_pixelimplementing the per-axis pair-coincidence probability.AbstractSensorMaterial.uncertainty(and implementations) gain awidth_pixelparameter, andAbstractImagingSensor.uncertaintypasseswidth_pixel=self.width_pixel, mirroring whatexpose()already does for the signal itself.expose(uncertainty=True)therefore now attaches a width consistent with its own diffused signal realization.Validation
signal()withthickness_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).test_vmr_signal_diffusioncompares the analytic result againstsignal()to 10%, and the sharedtest_uncertaintynow exerciseswidth_pixelacross 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 withwrap=False.🤖 Generated with Claude Code
https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb