CTnoise.add: use MATLAB's default photon count and clamp non-positive readings - #781
Conversation
|
I said in the description that I could not run these tests against a compiled TIGRE, only against a numpy model of Setup: TIGRE built from this branch with nvcc 12.4, running on an NVIDIA GeForce RTX 3070 Laptop GPU (driver 610.88), Python 3.14, numpy 2.5.3. To isolate the change I swapped only With With The numbers I quoted in the issue from the stubbed RNG hold up on real hardware: about 400 NaN (385 here) and a mean absolute error around 140 (139.43 here). One aside, unrelated to this PR: importing the package prints Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5. |
|
Claude AI seems to keep adding unlimited tests to the test folder. Please uncommit these, and I will merge the PR. |
… readings With Poisson left to its default, the Python port set the photon count to ceil(log2(max(|proj|))), which is 9 for projections peaking at 300. That turns the requested realistic noise into noise as large as the signal, and the Gaussian term then pushes many readings to zero or below, so -log() returns NaN and inf. MATLAB's addCTnoise uses I0 = 60000 (max(proj)/5 if the projections exceed it) and sets readings <= 0 to 1e-6 before the log; do the same here. Fixes CERN#780
0a8ff95 to
2a4491e
Compare
|
Fair point on the tests, sorry about that. The test file is out and the PR now only touches |
Fixes #780
I was comparing
Python/tigre/utilities/CTnoise.pywithMATLAB/Utilities/addCTnoise.mand found that the Python default forPoissonisnp.ceil(np.log2(np.max(np.abs(projections)))). That is the exponent of the next power of two, not a photon count. For projections that peak at 300 it gives 9 photons, so the "realistic" noise ends up as large as the signal. On top of that, the Gaussian term drives some readings to zero or below, and-np.log()turns those into NaN or inf. MATLAB avoids both problems: it defaults toI0 = 60000(ormax(proj)/5when the projections go above that) and setsIm(Im<=0) = 1e-6before taking the log.This PR does the same in Python:
max(projections) / 5when the projections exceed 60000, following MATLAB;<= 0after the noise is added are set to1e-6before the log, which also covers an explicitly lowPoisson.Nothing changes when
Poissonis passed (every demo passesPoisson=1e5), except that a reading that used to become NaN or inf now comes out finite. The default value was never documented, and in practice it could not be used, so I don't think anyone depends on it. One thing to note: the help header ofaddCTnoise.msays the default is 1e5, while its code uses 60000. I followed the code; if you prefer 1e5, it is a one-line change.I checked the change with two tests: that the default call returns finite projections with small noise, and that
Poisson=5, Gaussian=[0, 2]returns finite values. They were inPython/tests/test_ctnoise_defaults.pyin the first version of this PR and were removed on request, so the PR now only touchesCTnoise.py. The machine I work on has no NVIDIA GPU, so I ran these tests with_RandomNumberGeneratorreplaced by a numpy model ofGeneratePoissonAddGaussian(Poisson of the input plusnormal * sigma + mu, float32). On master both tests fail: the default call gives about 400 NaN out of 32,768 values and finite values from about -250 to several thousand for a signal that peaks at 300. With the change, both pass, and the reproduction script from the issue printsTruewith a mean absolute error of about 1.4.AI Usage
Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.
Python/tigre/utilities/CTnoise.py(default photon count and clamp before the log).MATLAB/Utilities/addCTnoise.m, then confirming with a test that fails before the change and passes after it (RNG stubbed with a numpy model of the CUDA kernel, since no NVIDIA GPU was available).CTnoise.py.Nothing this pipeline produces is submitted without human approval. The entire pull request — the code change, the tests and this description — was reviewed by a human before the pipeline was allowed to submit it.
(@Dev-next-gen)