Repository navigation
Centre the oversampled model grid on the detector pixels - #161
Conversation
With points_per_pixel > 1 the fine grid spanned the pixel centres, so binning it in groups of points_per_pixel shifted the model by up to half a pixel. Extend the grid by the half-pixel overhang so each group is centred on its pixel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nal ARTS wavelength unit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are handling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The oversampled model grid, the forward ARTS detector edges and the data-prep resolution-unit edges now all come from tsadar/core/instrument/detector_grid.py. The fine grid is the midpoints of points_per_pixel equal cells inside each pixel, which reproduces the pixel-centred grid to roundoff. - FormFactor builds its axis with oversampled_axis and rejects an npts that is not a multiple of points_per_pixel. - The spectrometer IRF bins onto npts // points_per_pixel pixels, the same grouping the grid uses, instead of CCDsize[0]. - prepare_data drops a partial final wavelength unit only on the uniform-grid path; the detector-bin quadrature keeps it at its true width. A lam_res_unit larger than the axis raises on the grid path. - The covariance throughput correction uses the binned model axis directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadData returns every data type as [wavelength, time/space/angle], so CCDsize[0] is the wavelength pixel count. The non-angular forward path sized npts from CCDsize[1], which only matched because the CCD is square. - calc_series uses CCDsize[0] for every spectype. - Tests that size npts the same way use CCDsize[0]; the fixtures are square, so the values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
One blocking regression: the production-recovery configurations have not been migrated to the new sampling contract and now fail during diagnostic construction. See the inline finding.
Postprocessing: the existing forward plotting/NetCDF path and inverse fit/diagnostic artifact path remain reachable and consume the corresponding model or prepared-data axes; this grid correction does not require a separate analysis pipeline.
Static review only; I did not execute tests or postprocessing. The latest inspected CPU check was still running; the GPU check was skipped.
| h1 = 1024 # 1024 | ||
| lamAxis = jnp.linspace(lambda_range[0], lambda_range[1], npts) | ||
| # lambda_range is the first and last pixel centers; each pixel holds points_per_pixel grid points | ||
| if npts % points_per_pixel: |
There was a problem hiding this comment.
[P2] Migrate the production-recovery decks to the new sampling contract
tests/test_physics/test_production_recovery.py::_recovery_config inherits points_per_pixel: 10 from tests/configs/1d-defaults.yaml, then sets npts to 1024 or 2048 without updating that field. Both EPW/IAW recovery cases therefore hit this new ValueError while constructing ThomsonScatteringDiagnostic, before reaching the recovery assertions. These cases are included in the PR's pytest -m "not slow" tests/ workflow. The slow ARTS recovery deck in tests/test_physics/test_arts_recovery.py::_arts_config similarly inherits 10 and sets npts=64.
Please migrate these callers along with the constructor change, keeping npts, points_per_pixel, and the intended detector-bin count consistent. For the 1D recovery test, preserve its 128 detector pixels while refining the samples within each pixel. Removing only this check is insufficient: _irfs_from_config now also derives the output pixel count from npts // points_per_pixel.
…e decision - detector_grid moves to tsadar/utils so the data layer can import it without loading tsadar.core, JAX and the forward model. - electron_spectrum_is_detector_binned(config) is one helper used by both FitModel and prepare_data, replacing the copy of the condition in data prep. - prepare_data prints a warning naming the wavelength pixels it drops when lam_res_unit does not divide the axis on the uniform-grid path. - add_electron_IRF and add_ion_IRF bin the returned wavelength axis whenever they bin the spectrum, so the axis is the detector pixel centers for any detector_specs.norm. - irf.py uses detector_edges_from_centers instead of its own edge function. - test_1d shrinks its wavelength range onto the grid ThryE-1d.npy was generated on, as test_arts1d_forward_pass does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Request changes: the shared detector-edge refactor introduces a JIT failure in the angular IRF (inline finding).
The previous sampling-contract finding also remains at this head: the production-recovery decks still inherit points_per_pixel: 10 while setting npts to 1024/2048 (1D) or 64 (ARTS), triggering the new constructor check before recovery can run. Please migrate these callers while preserving their intended detector-bin counts.
Postprocessing: existing forward plots/NetCDF output and inverse fit plots, checkpoints, and angular diagnostic artifacts cover the changed spectra and axes; no separate analysis pipeline is needed. The compiled angular workflow is blocked upstream by the IRF regression.
Static review only; I did not execute tests or postprocessing. The latest inspected CPU check was still running; the GPU check was skipped.
|
|
||
| edges = _bin_edges_from_centers(axis) | ||
| # the axis is a static detector coordinate, so the edges are built once in numpy | ||
| edges = jnp.asarray(detector_edges_from_centers(np.asarray(axis)), dtype=axis.dtype) |
There was a problem hiding this comment.
[P1] Keep detector-edge construction outside the traced angular IRF
_gaussian_bin_response first applies jnp.asarray/jnp.ravel to axis. When add_ATS_IRF runs inside a JIT-compiled diagnostic or loss, that result is a tracer even though the original irf.ang_axis is a fixed NumPy calibration. The new np.asarray(axis) therefore raises TracerArrayConversionError for multi-angle spectra. The production LossFunction JIT-compiles its loss/value-and-gradient methods, and tests/test_inverse/test_angular_1v.py::test_arts1d_inverse explicitly compiles the diagnostic's loss with eqx.filter_jit, so angular fitting now stops on its first compiled evaluation. This also affects ARTS2D with resonance quadrature: disabling spectral blur does not skip this angular response.
Please either retain JAX-compatible edge arithmetic here or infer/cache the edges from the original NumPy calibration before entering the traced path. Add a small CPU JIT regression with more than one angular bin; the eager IRF tests and single-angle identity case do not exercise this failure.
The recovery decks inherited points_per_pixel: 10 from 1d-defaults.yaml while setting npts by hand for a 128-pixel (or 64-bin) detector, which FormFactor now rejects. They set points_per_pixel to match their npts: 8 and 16 for the 1D recovery decks, 1 for the ARTS recovery deck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Request changes: the existing P1 angular-IRF JIT finding remains unchanged at this head. In tsadar/core/instrument/irf.py:54–59, jnp.asarray/jnp.ravel turn the axis into a tracer during JIT, so the subsequent np.asarray(axis) raises TracerArrayConversionError for multi-angle spectra. This blocks compiled angular fitting, including the eqx.filter_jit(truth_diagnostic) call in tests/test_physics/test_arts_recovery.py. Disabling spectral blur for resonance quadrature does not bypass the angular response. Keep this arithmetic JAX-compatible or compute the edges from the original NumPy calibration before tracing, and cover it with a small multi-angle CPU JIT regression. Referencing the existing inline thread rather than duplicating it.
The updated points_per_pixel settings in both recovery configurations address the previous sampling-contract finding.
Postprocessing: the existing forward plots/NetCDF output and inverse fit plots, checkpoints, and angular diagnostic artifacts cover the changed spectra and axes; no separate analysis path is needed. The angular workflow remains blocked upstream by the JIT regression.
Static review only; I did not execute tests or postprocessing.
The angular IRF converted its axis to numpy after a JAX operation, which raises TracerArrayConversionError under jit. The edges are now built in numpy first, and a test compiles add_ATS_IRF with jit. - prepare.py and the tests import the edge helpers from tsadar.utils.detector_grid; calibration.py no longer re-exports them. - The dropped-pixel notice in prepare_data is informational, since the default ARTS-1D deck triggers it on every fit. - Docstrings for the uniform spectral response and oversampled_axis describe the uniform grid they rely on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
No remaining material findings. The previously reported sampling-contract and angular-IRF JIT defects are addressed: the recovery decks now specify consistent sampling, and _gaussian_bin_response constructs edges from the static NumPy axis before traced operations. A multi-angle JIT regression has been added.
Postprocessing: the existing forward plots/NetCDF output and inverse fit plots, checkpoints, and angular diagnostic artifacts remain reachable and use the corresponding model or prepared-data axes. This grid correction does not need a separate analysis path; standalone postprocessing remains available.
Static review only; I did not execute tests or postprocessing. At the final check, CPU CI was still running and GPU CI was skipped.
Replaces #160.
Summary
With
points_per_pixel > 1, the model was evaluated on a fine wavelength grid that spanned the first and last detector pixel centres and was then binned in groups ofpoints_per_pixel. Those groups are not centred on the pixels, so the binned model was shifted relative to the data by up to half a pixel.The fine grid is now built from the detector pixel edges: each pixel is split into
points_per_pixelequal cells and sampled at the cell midpoints, so every group is centred on its pixel.All wavelength grids now come from one module,
tsadar/utils/detector_grid.py. The oversampled uniform grid, the ARTS detector-bin quadrature and the data-prep resolution units all start from the same pixel edges.This is a fix, not a breaking change: the shift is at most half a pixel, so refitting gives values very close to an earlier fit.
Changes
Grid construction
tsadar/utils/detector_grid.pyholdsdetector_edges_from_centersandgrouped_detector_edges(moved fromcalibration.py, which still re-exports them), plus two new functions:oversampled_axis(centers, points_per_pixel)builds the fine grid.electron_spectrum_is_detector_binned(config)decides between the detector-bin quadrature and the uniform grid.FitModelandprepare_databoth call it.utilsso that importingtsadar.data.calibrationdoes not loadtsadar.core, JAX and the forward model.FormFactorbuilds its axis withoversampled_axisand raises ifnptsis not a multiple ofpoints_per_pixel._ensure_angular_detector_edgesandirf.pyuse the shared edge function instead of their own copies.Pixel counts
prepare_datasizes the fine grid from the length of the wavelength axis instead ofCCDsize[1].calc_series.pysizes it fromCCDsize[0]for every spectype.loadDatareturns every data type as[wavelength, time/space/angle], soCCDsize[0]is the wavelength pixel count; the oldCCDsize[1]for non-angular decks only worked because the CCD is square.npts // points_per_pixelpixels, the same grouping the grid uses, instead ofCCDsize[0].Angular data with a partial final wavelength unit
When
lam_res_unitdoes not divide the number of wavelength pixels:resonance_quadratureenabled), the leftover pixels are kept as a narrower final unit and integrated over its true width, as before (Integrate unresolved ARTS2D resonances into detector bins #143).lam_res_unit: 5on 1024 pixels gives 204 units instead of 205.lam_res_unitlarger than the axis raises aValueError.Instrument response
add_electron_IRFandadd_ion_IRFbin the returned wavelength axis whenever they bin the spectrum. Before, withdetector_specs.norm > 0they returned a pixel-length spectrum with a fine-length axis.loss_function.pyuses the binned model axis directly instead of rebuilding it withlinspace.Behaviour notes
points_per_pixel: 1gives the same axis to about 1e-13 nm. It is no longer bit-identical, because the axis is now computed from edges.CCDsizelayout is not changed here. After data prep it is[angle, wavelength]for ARTS only, and six angular tests build their dummy data as[wavelength, angle], which matches only because the fixtures are square. That is left for the ARTS work.Tests
test_irf_area.py:oversampled_axison uniform and non-uniform axes and with invalidpoints_per_pixel;prepare_datakeeping the partial unit for a 2D distribution function and dropping it for 1D.test_mcmc.py:test_binned_model_axis_matches_the_detector_pixel_centers(points_per_pixel1, 2, 5).test_1dandtest_arts1d_forward_passcompare against references generated on the old grid (ThryE-1d.npy,ThryE-arts1v.npy). Each test shrinks its wavelength range so the new grid lands on the old points. The references are unchanged.nptsfromCCDsize[1]now useCCDsize[0]. The fixtures are square, so the values are the same.test_irf_area.py, forwardtest_1dandtest_angular_1v,test_sinogram,test_arts2d_spectral_terms,test_spherical_harmonics,test_unresolved_arts2d,test_noncollective, and thetest_mcmc.pyaxis and throughput tests all pass.test_arts2d_marginal.pyhas two failures locally that are test_arts2d_marginal normalization checks fail on Windows CPU: 2e-15 absolute tolerance is at machine precision #157 (a2e-15tolerance on Windows CPU); they fail the same way without this PR.Release
No label: merging cuts a patch release (
v0.4.1).🤖 Generated with Claude Code