Integrate the charge-diffusion correction by Gauss-Legendre quadrature - #217
Open
roytsmart wants to merge 2 commits into
Open
Integrate the charge-diffusion correction by Gauss-Legendre quadrature#217roytsmart wants to merge 2 commits into
roytsmart wants to merge 2 commits into
Conversation
The average in the charge-diffusion term of `vmr_signal` has no closed form. The obstruction is the coincidence probability, which contains erf(1 / 2 sigma) with sigma proportional to sqrt(z_f - z); squaring it to cover the two axes of the sensor gives a product of two error functions integrated against the exponential absorption profile. Sympy returns NonElementaryIntegral for the underlying antiderivative, while happily evaluating the same integrand without the reciprocal-argument exponential, so the obstruction is that combination specifically. Routing the calculation through the separation instead gives an incomplete Bessel integral, which is no better and is not in scipy. It was previously evaluated with a 1001-node midpoint rule in the cumulative absorption probability. That converges only algebraically, because sigma(z) puts a square-root branch point at the edge of the field-free region, and it reached only 2.2e-4 across 1 to 10000 angstroms for the e2v CCD97. Substituting s = 1 - r^2 removes that branch point while keeping the nodes distributed by where photons are actually absorbed, which matters because the optical depth of the field-free region spans 0.02 to 1271 over that wavelength range for the same sensor. The interval is split where the implant ends and the differential CCE saturates, and Gauss-Legendre is applied to each part. The optical depth is evaluated as -log(exp(-a z_f) + (1 - exp(-a z_f)) r^2) rather than through s, so it stays finite once exp(-a z_f) underflows, which it does beyond an optical depth of about 745. With 32 nodes per part the worst error over the same wavelength range is 1.6e-7, against 2.2e-4 from 1001 midpoints: three orders of magnitude more accurate with fifteen times fewer evaluations. Convergence was checked separately against optical depths from 0.02 to 3000 to confirm the node count is not tuned to one sensor. A plain sqrt substitution in depth was tried first and rejected: it is spectrally accurate at low optical depth but needs nodes scaling as sqrt(alpha z_f), and fails at 9e-3 with 16 nodes where a real sensor reaches 1271. Results are unchanged to within the quadrature error, and the no-diffusion path is untouched. 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 #217 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 121 121
Lines 7424 7445 +21
=========================================
+ Hits 7424 7445 +21
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:
|
The split exists only because the differential CCE of Stern 1994 is piecewise linear and therefore kinks where the implant ends. A smooth profile would need no split and half as many nodes, so the split reads like an accident worth optimizing away. It is not: the piecewise-linear form is kept deliberately, so the fitted implant thickness and back-surface CCE stay comparable with the values published by Stern and by Boerner et al. Note that no choice of CCE would produce a closed form anyway, since setting eta = 1 still leaves the erf-squared obstruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb
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.
The charge-diffusion average in
vmr_signalwas evaluated with a 1001-node midpoint rule. This replaces it with 64 Gauss-Legendre nodes in a better variable: three orders of magnitude more accurate with fifteen times fewer evaluations.Why there is no closed form
The obstruction is the coincidence probability$\mathcal{P}$ , which contains $\operatorname{erf}(1/2\sigma)$ with $\sigma \propto \sqrt{z_f - z}$ . Squaring it to cover the two axes gives a product of two error functions integrated against the exponential absorption profile. Substituting $x = 1/2\sigma$ reduces everything to three families, and sympy's verdict is:
The control row matters: sympy handles$\operatorname{erf}^2$ integrals fine in general, so the obstruction is specifically its combination with the reciprocal-argument exponential. Integrating over the separation instead yields an incomplete Bessel integral, which is no better and is not in scipy.
Why the old scheme was slow to converge
The substitution
Writing the average over the cumulative absorption probability$s$ keeps nodes where photons are actually absorbed — necessary, since $\alpha z_f$ ranges over 0.02 to 1271 across that wavelength range for one sensor. Then $s = 1-r^2$ removes the branch point, since the diffusion width becomes linear in $r$ there. In $r$ ,
evaluated in this form rather than through$s$ so it stays finite once $e^{-\alpha z_f}$ underflows, which happens beyond optical depth ~745 — well inside the range a real sensor spans. The interval is split where the implant ends and $\eta$ saturates, and Gauss-Legendre is applied to each part.
Accuracy
Against a 400-node reference, over 1–10000 Å:
versus 2.2e-04 from the previous 1001 midpoints. Convergence was also checked directly against optical depths from 0.02 to 3000, so the node count is not tuned to a single sensor.
A rejected alternative
Substituting$t = \sqrt{1-z/z_f}$ in depth is spectrally accurate at low optical depth (1e-13 with 48 nodes on a toy case) but needs nodes scaling as $\sqrt{\alpha z_f}$ , and errs by 9e-3 with 16 nodes at $\alpha z_f = 500$ — while real sensors reach 1271. The CDF-based version is uniformly robust because it adapts to the exponential.
Checks
199 tests pass, including the existing Monte Carlo comparison. New
test_vmr_signal_quadratureasserts the default node count is converged to 1e-5 against an 8× denser rule across the full wavelength range. The no-diffusion path is bit-identical.black,ruff, and the docs build are clean, and the new Notes text was verified to parse into the doctree with no stray markup.🤖 Generated with Claude Code
https://claude.ai/code/session_0192abyvBf2Zw5rq5CQ62JFb