Skip to content

Modified optika.sensors.signal() to model charge diffusion using a Monte-Carlo simulation.#162

Merged
roytsmart merged 19 commits into
mainfrom
feature/signal-charge-diffusion
Jun 10, 2026
Merged

Modified optika.sensors.signal() to model charge diffusion using a Monte-Carlo simulation.#162
roytsmart merged 19 commits into
mainfrom
feature/signal-charge-diffusion

Conversation

@roytsmart

Copy link
Copy Markdown
Collaborator

No description provided.

@roytsmart roytsmart force-pushed the feature/signal-charge-diffusion branch from 6f14ac3 to 58915e2 Compare June 2, 2026 21:30
roytsmart and others added 4 commits June 6, 2026 22:20
…or refactor.

Sensor pipeline:
- Rename the sensor methods to `collect` -> `expose` -> `measure`
  (was `bin_rays` -> `expose` -> `readout`), update the `_sequential.py` caller.
- `expose` centers the wavelength bin edges and is shared by any per-pixel
  photon image; tidy the empty-pixel mean-cosine division.

Review fixes (internal consistency of the breaking changes):
- `fit_eqe` now passes the cosine `direction=1` instead of a 3d vector to the
  (now cosine-based) standalone `quantum_efficiency_effective`. This was masked
  by the joblib cache, so a cold cache would have broken all three device
  materials; verified by running the fit uncached.
- Update the stale docstring examples that still used the old API
  (`quantum_efficiency_effective(rays=...)`, `width_charge_diffusion(rays=...)`,
  and a vector `direction` to the standalone QE); confirmed by a full doc build.
- Revert the `RayVectorArray.intensity` default back to `1` and fix a cosmetic
  typo in the new `RayVectorArray.n` property.

Add a `test_n` exercising the `RayVectorArray.n` complex-index property.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@roytsmart roytsmart force-pushed the feature/signal-charge-diffusion branch from 88ff3fc to 7057014 Compare June 7, 2026 18:47
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.34%. Comparing base (44177ca) to head (092eaa3).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #162      +/-   ##
==========================================
+ Coverage   99.32%   99.34%   +0.01%     
==========================================
  Files         116      116              
  Lines        5797     5923     +126     
==========================================
+ Hits         5758     5884     +126     
  Misses         39       39              
Flag Coverage Δ
unittests 99.34% <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 15 commits June 7, 2026 13:00
- Document the `n` property (complex index of refraction: real part is the
  index of refraction, imaginary part is the extinction coefficient
  k = alpha * lambda / 4 pi).
- Run black on the sensor material modules to satisfy the formatting check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Rename its first argument `photons_absorbed` -> `photons_transmitted` and
  model the transmitted->absorbed step (the fraction `1 - e^(-alpha t)` that is
  absorbed before escaping out the back), so it is now drop-in comparable with
  the Monte-Carlo `electrons_measured()`: at 5.9 keV the two agree to ~0.003%
  on the mean and ~0.6% on the std.
- Add the matching `thickness_substrate` parameter and align the shared
  parameter docstrings/summary with `electrons_measured()`.
- Fix two latent bugs: the docstring example called `electrons_measured`
  with the old `photons_absorbed=` keyword, and the broadcast shape accidentally
  referenced the module-level `absorbance` function.
- Update the MC summary line, which still said "photons absorbed" despite the
  argument now being `photons_transmitted`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mark the defensive `ValueError` branches with `# pragma: nocover`, matching
  the existing convention (e.g. `_sequential.py`): the `expose` ambiguous
  wavelength-axis check and the `absorbance` method dispatch.
- Mark the `@numba.njit` `_electrons_measured_numba` kernel `# pragma: nocover`;
  JIT-compiled code cannot be traced by coverage.py.
- Extend `test_electrons_measured` with an `axis_xy` parametrize and a per-pixel
  photon grid so the charge-diffusion branch of `electrons_measured` is
  exercised. `_ramanathan_2020.py` is now at 100% line coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Parametrize `test_absorbance` over `method` ("Beer-Lambert" and "exact") so
  the exact (`layer_absorbance`) branch is exercised.
- Add a `transmittance` method test mirroring the other method tests, covering
  its `direction`/`normal` None-default handling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…otons_transmitted`.

The Monte-Carlo kernel now samples the absorption depth from an exponential
truncated to the light-sensitive region, so every input photon is treated as
absorbed. The escape fraction is applied as Poisson thinning in `signal()`
(exactly equivalent). `electrons_measured_approx()` reverts to splitting the
absorbed photons into partial/complete charge collection, and `signal()`
computes absorbance from the start rather than transmittance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…within the substrate.

The exact `electrons_measured` kernel samples the absorption depth from an
exponential truncated to the substrate `[0, D)`, so the approximate model must
do the same. The partial-collection fraction is now the implant fraction
conditioned on absorption within the substrate,
`(1 - exp(-alpha*W)) / (1 - exp(-alpha*D))`, which restores agreement between
`electrons_measured_approx` and `electrons_measured`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `signal()` `method` parameter docstring described nonexistent `exact`/
`approx` methods; it now documents the actual `monte-carlo` and `expected`
options, and notes that the per-pixel `expected` method applies no charge
diffusion.

Adds `test_electrons_measured_diffusion`, which injects photons into a single
pixel and checks that the spatial spread of the diffused charge matches the
analytic width from `optika.sensors.charge_diffusion`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…` method.

The per-pixel charge-collection model needs the cosine of the refracted angle
inside the light-sensitive region, which depends on the ambient index of
refraction. Rather than threading an ambient-index argument through
`expose`/`signal`, `collect` now refracts each ray with its own `rays.n` via the
new `AbstractSensorMaterial.direction_refracted` method and flux-weights the
resulting (complex) substrate-side cosine per pixel. `material.signal` consumes
this already-refracted cosine and forwards it to `signal` with equal
ambient/substrate indices so the internal Snell step is a no-op while the
effective absorption still uses the correct substrate index; its `n` parameter
is removed. `AbstractBackIlluminatedSiliconSensorMaterial.electrons_measured` is
refactored to reuse `direction_refracted`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dal `wrap`.

The Monte-Carlo charge-diffusion kernel previously wrapped charge that diffused
past the edge of the pixel grid back onto the opposite edge (a toroidal
boundary), which is unphysical for a finite sensor. By default, charge that
diffuses off the grid is now lost, as it would be at the physical edge of a
sensor. The old toroidal behavior is still available via a new `wrap` keyword
argument, threaded from the kernel up through `electrons_measured`, `signal`,
and the sensor-material `signal`/`electrons_measured` methods (but not the
sensor-level `expose`/`measure`, which use the default). Adds a test covering
both boundary behaviors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the absorption coefficient is zero, the reciprocal absorption length `1/a`
and the truncated-exponential depth sampling are undefined and previously
produced NaN depths. The kernel now samples the absorption depth from the
uniform distribution over the substrate, which is the `a -> 0` limit of the
truncated exponential, so a direct call at a non-absorbing wavelength returns
finite results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ges.

After moving the refraction into `collect`, `direction` carries the cosine of
the refracted angle inside the light-sensitive region, not the incidence angle.
Update the `expose` and `AbstractSensorMaterial.signal` docstrings accordingly,
and drop the stale "approximate" from the standalone `signal` summary (its
monte-carlo method simulates every photon, matching `electrons_measured`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`collect` always passes an explicit direction and normal, so the
normal-incidence defaults in `direction_refracted` were never exercised,
failing patch coverage. Add `test_direction_refracted` to the shared sensor
material test base so the defaults run for every material type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@roytsmart roytsmart merged commit 52b4bc2 into main Jun 10, 2026
12 checks passed
@roytsmart roytsmart deleted the feature/signal-charge-diffusion branch June 10, 2026 04:52
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