Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feature/diffusion-slab #255 +/- ##
=========================================================
Coverage 100.00% 100.00%
=========================================================
Files 131 133 +2
Lines 11074 11553 +479
=========================================================
+ Hits 11074 11553 +479
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:
|
Request: the covariance between pixels from charge diffusion (motivated by Hinode/XRT)This stack covers almost everything needed to model the noise of the Hinode/XRT CCD. The missing piece is the covariance between neighboring pixels. I'd suggest it as a follow-up PR stacked on this one rather than more scope here. Motivationhinode (sun-data/hinode v0.2.0) computes the per-pixel uncertainty of XRT as compound-Poisson photon noise, I measured the real noise from differences of consecutive frames:
Interpretation. The across-edge, diagonal and two-apart values match a charge cloud of about 5 µm on XRT's 13.5 µm pixels, as in the ccd-snr-paper. That spread puts two electrons from one photon in the same pixel only about 36% of the time, which alone would cut the variance to about 0.36. The extra correlation inside blocks, and the ratio rising with signal, are JPEG quantization zeroing the high-frequency DCT components of the noise. That accounts for roughly another ×0.45. I couldn't find this in the XRT literature. Kano et al. (2008) give the CCD (e2v CCD42-40, back-illuminated, 13.5 µm, 57 e⁻/DN) but no depletion depth and nothing on charge spreading. Kobelski et al. (2014) estimate the photon noise from a model and never compare it with measured scatter. What it changes. In a joint AIA + XRT DEM ( Plan. Model the XRT CCD with this stack, simulate XRT's JPEG in hinode, and run the same frame-difference analysis on simulated frames. Then compare the variance ratio and the correlation pattern with the data. The correlations are the cleanest handle on What this stack already covers
What's missing: the two-point covarianceFor uniform illumination, the photon arrivals are compound Poisson. So the covariance between pixels offset by Summing over where the photon lands in the uniform field, and averaging over its position within the pixel and over depth, gives the covariance per absorbed photon, in electrons²: Here $\mathcal{P}_0 = $
Proposed API
Tests that would pin it down
Not for optikaThe JPEG simulation is XRT-specific and will live in hinode, with XRT's quantization tables per |
30fb395 to
73d0dc8
Compare
|
Re #255 (comment): implemented in draft #256, stacked on this PR.
🤖 Generated with Claude Code |
887d1a1 to
1403cee
Compare
73d0dc8 to
40dc0c4
Compare
… diffusion The expected signal ignored charge diffusion, which the Monte Carlo and the uncertainty included, so the noiseless forward model was not the mean of the noisy one. Add `optika.sensors.kernel_signal`, the expected electrons measured in each pixel around an absorbed photon, and spread the expected signal of `signal`, the material and the sensor with it, over a gather that allows a kernel in every pixel, interpolated in optical depth where the direction varies over the pixels. Add the material's `kernel`, `signal_transposed` and `backproject`, and the sensor's `kernel`, `kernel_variance`, `expose_transposed` and `backproject`, which replace `photons_absorbed`, and make the sensor's `uncertainty` private. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE
Gather each pixel of the result in one thread, in a fixed order, so the spread is the same for any number of threads, and interpolate the kernel of each pixel as it is used, instead of a pass over the image for every pixel of the kernel. A 2048 by 1024 frame with a 9 by 9 kernel takes 0.13 s per wavelength on one thread instead of 0.56 s, and 0.33 s instead of 5.9 s with a kernel interpolated in every pixel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE
- The compiled blur spreads the nominal values and each sample of the distributions separately, so the expected signal of a model holding uncertain values is uncertain, as its Monte Carlo is. - The docstrings of the kernels say that a default kernel of more than 101 pixels raises an error. - 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
1403cee to
5ac92e7
Compare
40dc0c4 to
60a2c44
Compare
Stacked on #251 (which is stacked on #244). This is the third part of the diffusion series. It completes the "everything needs to match" goal of #244: the noiseless signal of a sensor now spreads charge with the same kernel that the Monte Carlo samples.
Until now,
signal(method="expected")andexpose(noise=False)ignored charge diffusion entirely. The Monte Carlo and the uncertainty included it. So the noiseless forward model that inversions use was not the mean of the noisy one.New
optika.sensors.kernel_signal(wavelength, *, axis_x, axis_y, ...): the expected electrons in the pixel a photon is absorbed in and in each pixel around it, per absorbed photon. It averages over the photon's position within its pixel and over the depth it is absorbed at, and is a sibling ofvmr_signal.kernel.kernel.signal_transposed: the exact adjoint of the noiselesssignal.backproject: replacesphotons_absorbed.kernel: the sensor's point-spread function, on axes named after the pixel axes.kernel_variance: the VMR times the kernel. This is the first phase from the plan; an exact variance kernel can come later.expose_transposed: the exact adjoint ofexpose(noise=False), for gradients.backproject: replacesphotons_absorbed.Changed
signal(method="expected")with a model of diffusion spreads the photons absorbed in each pixel withkernel_signal.axis_xyandwidth_pixel, as the Monte Carlo already did.wrap: charge that leaves the grid is lost unless it wraps around.material.signal(noise=False)andsensor.expose(noise=False)are blurred too.collect.LinearSystempasses it, the kernel is exact.backprojectis (1/g²) Kᵀ, where g is the gain, the sum of the kernel.photons_absorbed, e/g.LinearSystem.backproject_from_weightsuse it.AbstractImagingSensor.uncertaintyis now private (_uncertainty). Onlyexposeused it.Breaking
AbstractImagingSensor.photons_absorbedandAbstractSensorMaterial.photons_absorbedare removed. Usebackproject.AbstractImagingSensor.uncertaintyis removed. Useexpose(uncertainty=True).AbstractSensorMaterialhas three new abstract methods:kernel,signal_transposedandbackproject.expose(noise=False)changes for any material with a model of diffusion, since its expected image is now blurred.signal(method="expected")with a model of diffusion now needsaxis_xyand photons that vary along both axes.Nothing outside optika calls
photons_absorbedoruncertainty. ctis callssensor.expose(noise=False)to probe the gain and assumes the sensor is diagonal, so that needs updating when ctis moves to this release. It is pinned tooptika<4.Things to know
Gain truncation: the kernel averages over the photons absorbed in the substrate, as the Monte Carlo does, but
charge_collection_efficiencyassumes an infinitely thick substrate. So for photons the substrate absorbs weakly, the gain with diffusion differs from the gain without it. For the 14 µm CCD97:The kernel's docstring says so. I left
charge_collection_efficiencyalone, since its form is deliberate (comparability with Stern 1994).Cost: for a 2048 × 1024 frame with a 9 × 9 kernel, per wavelength. The first numpy version of this PR took 0.56 s for one kernel everywhere, and 5.9 s for interpolated per-pixel kernels.
The transpose costs the same.
The deferred items from Make models of charge diffusion the one public interface for diffusion #244:
cce_backsurface < 1is now tested against the Monte Carlo.kernel_signalis evaluated at 16 nodes rather than once per pixel.Tests
test_kernel_signal:cce_backsurface=1, the result is the quantum yield timeskernel_average;numis validated;test_kernel_signal_monte_carlo: the Monte Carlo mean around one pixel equals the kernel, for photons absorbed in the implant withcce_backsurface=0.2, for the Janesick and slab models.test_signal_expected_diffusion: a single source spreads with the kernel, with and withoutwrap.test_signal_expected_direction: interpolated per-pixel kernels versus exact ones._blur_test.py: the spread is the product with the explicit matrix whose columns are the kernels, to 1e-14. This covers one kernel everywhere, a kernel per pixel and an interpolated kernel, with and withoutwrap, forward and transposed. It also checks units, including nodes in a different unit than the positions.kernelmatches the noiseless signal;signal_transposedis the adjoint to 1e-12, including per-pixel directions and wrap;backprojectinverts uniform light on a periodic grid to 1e-5.exposeof one bright pixel is the kernel;kernel_varianceis the VMR times the kernel;expose_transposedis the adjoint ofexpose;backprojectinvertsexposewithout diffusion, and inverts uniform light in the middle of a wide sensor with it.🤖 Generated with Claude Code
https://claude.ai/code/session_01BAw6Lv7Cy2ULGmsMJ4b1dE