Conversation
SRM model handles units
Adds changelog
Fixes test_models
Import requires modules in test_models
Fixes examples
Tidy Comments
Merge main
Removes astropy fitting simulated data example
Pre-commit
Renames input and output units as user facing
Fixes Tests
There was a problem hiding this comment.
Pull request overview
This PR introduces a unit-aware SRM (MatrixModel) so that photon-space models can be propagated through an instrument response into count space with correct unit handling, and updates the simulated-data example and docs accordingly.
Changes:
- Replace the old scalar
MatrixModelwith an SRM-styleMatrixModelthat knows about input/output axes, bin widths, and spectral units, and update tests to exercise the new behavior. - Rework the
fitting_simulated_dataexample to useastropy.units,SpectralAxis, andSpectrum, piping a physical photon model through the SRM into count space and fitting in count space. - Tidy up documentation examples (RST link targets, math formatting) and pin the Sphinx version for docs builds, plus add a changelog entry.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
sunkit_spex/models/instrument_response.py |
Replaces the previous minimal MatrixModel with a unit-aware SRM implementation that uses input/output spectral axes, bin widths, and a conversion parameter c to map photon-space spectra into count-space, and declares input/return units for Astropy modeling. |
sunkit_spex/tests/test_models.py |
Extends the model tests to construct MatrixModel with explicit axes and units and to verify that composition with StraightLineModel produces the expected result. |
examples/fitting_simulated_data.py |
Refactors the simulated-data fitting example to use astropy.units, quantity_support, SpectralAxis, and Spectrum, and to fit a count-space model built from a photon-space model piped through the SRM; updates plotting and summary table to work with quantities. |
examples/fitting_attenuated_RHESSI_spectra.py |
Adjusts the narrative docstring to use an RST-style reference name for the Knuth & Glesener 2020 citation and adds the corresponding hyperlink target. |
examples/fitting_RHESSI_spectra.py |
Cleans up the comparison table comments and converts various values to inline LaTeX math for consistency, while keeping the example’s numerical results unchanged. |
examples/fitting_NuSTAR_spectra-simultaneously.py |
Updates the intro docstring to use an RST reference for Cooper et al. 2021 and adds the link target; leaves the fitting workflow intact. |
examples/fitting_NuSTAR_spectra-glesener2020.py |
Updates documentation-style comparison tables in comments to use inline LaTeX math and an RST reference for Glesener et al. 2020. |
examples/fitting_NuSTAR_spectra-duncan2021.py |
Switches the introduction and comparison comments to use RST references and LaTeX math, and slightly reflows long explanatory comments. |
examples/fitting_attenuated_RHESSI_spectra.py |
Introduces an RST hyperlink target for the Knuth & Glesener 2020 reference in the docstring. |
pyproject.toml |
Pins the Sphinx dependency for docs builds to <9.0 to avoid incompatibilities with future major releases. |
changelog/227.feature.rst |
Adds a changelog entry announcing that MatrixModel now has unit handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def evaluate(self, x, c): | ||
| # Requires input must have a specific dimensionality | ||
| return model_y @ self.matrix | ||
|
|
||
| input_widths = np.diff(self.input_axis) | ||
| output_widths = np.diff(self.output_axis) | ||
|
|
||
| flux = ((x * input_widths) @ self.matrix * c) / output_widths | ||
|
|
||
| if hasattr(c, "unit"): | ||
| return flux | ||
| return flux.value |
There was a problem hiding this comment.
The new unit-handling logic in MatrixModel.evaluate (using input_axis/output_axis widths, matrix multiplication, and unit-aware c) is only tested for dimensionless units in test_MatrixModel, and there is no test that covers a fully unitful case like the fitting_simulated_data example (keV, ph/keV, ct/keV). To guard against regressions in the SRM unit handling, it would be valuable to add a test that constructs a MatrixModel with physical units on the axes and spectra, asserts the returned quantity has the expected ct keV^-1 units, and checks the numerical result against a known value.
There was a problem hiding this comment.
i agree with the Copilot note
There was a problem hiding this comment.
I wrote a similar comment later then found this. I agree too.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
I have read through the co-pilot suggestions, I think it makes sense to merge as I am going to work immediately on the fitter which will require a different approach of unit handling and testing anyway |
|
hey @jajmitchell i'm taking a look through this, i started a review and will request a couple minor changes but i think it looks good overall. i might just push some commits up fixing typos if that is ok |
|
does f38afa1 really remove the example? |
|
|
||
| ph_mod_4astropyfit = StraightLineModel(**guess_cont) + GaussianModel(**guess_line) | ||
| count_model_4astropyfit = (ph_mod_4fit | srm_model) + GaussianModel(**guess_gauss) | ||
| # guess_cont = {"slope": -0.5 * u.ph / u.keV**2, "intercept": 80 * u.ph / u.keV} |
There was a problem hiding this comment.
@jajmitchell i'm taking a first pass through this PR. i think we need to remove commented blocks if they aren't part of the actual example
|
|
||
| c = Parameter(fixed=True) | ||
|
|
||
| def __init__(self, matrix, input_axis, output_axis, input_spec_units, output_spec_units, c): |
There was a problem hiding this comment.
i think this could benefit from type annotations
| self.input_axis = input_axis | ||
| self.output_axis = output_axis | ||
| self.matrix = matrix | ||
| super().__init__(c) |
There was a problem hiding this comment.
what is "c" ? i'm surprised Ruff didn't complain about this
There was a problem hiding this comment.
I think, I think, c is a stand in for when the matrix model will have response parameters for gain slopes and offsets. The other reason was because Astropy complains when models don't have a fittable parameter. @jajmitchell can correct me on this.
There was a problem hiding this comment.
Yea it's the way get the units + model to work with astropy
| _input_units_allow_dimensionless = True | ||
|
|
||
| def evaluate(self, x, c): | ||
| # Requires input must have a specific dimensionality |
There was a problem hiding this comment.
# Requires input to have a specific dimensionality
| sim_count_model + (2 * np_rand.random(sim_count_model.size)) * np.sqrt(sim_count_model.value) * u.ct / u.keV | ||
| ) | ||
|
|
||
| obs_spec = Spectrum(sim_count_model_wn.reshape(-1), spectral_axis=ph_energies) |
There was a problem hiding this comment.
@jajmitchell this is not used after plotting, especially now since the astropy model is commented out.
i think this example needs to get reworked from the ground up to remove stray bits like this. if i was a user and saw this object created then ignored, i would be rather confused. i was actually just confused looking through this example
| def evaluate(self, x, c): | ||
| # Requires input must have a specific dimensionality | ||
| return model_y @ self.matrix | ||
|
|
||
| input_widths = np.diff(self.input_axis) | ||
| output_widths = np.diff(self.output_axis) | ||
|
|
||
| flux = ((x * input_widths) @ self.matrix * c) / output_widths | ||
|
|
||
| if hasattr(c, "unit"): | ||
| return flux | ||
| return flux.value |
There was a problem hiding this comment.
i agree with the Copilot note
| plt.plot(ph_energies_centers, (ph_model | srm_model)(ph_energies), label="photon model features") | ||
| plt.plot(ph_energies_centers, GaussianModel(**sim_gauss)(ph_energies), label="gaussian feature") | ||
| plt.plot(ph_energies_centers, sim_count_model, label="total sim. spectrum") | ||
| plt.plot(obs_spec._spectral_axis, obs_spec.data, label="total sim. spectrum + noise", lw=0.5) |
There was a problem hiding this comment.
These should be plotted using plt.stairs to show that the X-ray spectra are binned
There was a problem hiding this comment.
I would go further and say any models should use plt.stairs but the final data plt.plot should use plt.errorbar.
| true_vals = np.array(tuple(sim_cont.values())[-2:] + tuple(sim_line.values())[-3:] + tuple(sim_gauss.values())[-3:]) | ||
| guess_vals = np.array( | ||
| tuple(guess_cont.values())[-2:] + tuple(guess_line.values())[-3:] + tuple(guess_gauss.values())[-3:] | ||
| tuple(sim_cont) + tuple(f"{p}1" for p in tuple(sim_line)) + ("C",) + tuple(f"{p}2" for p in tuple(sim_gauss)) |
There was a problem hiding this comment.
The scipy method does not respect the fixed=True status on the photon/ct scaling parameter
- pass units rather than dict to matrix model - make conversion factor transparent to user (input/output units) - fix quantity_support units in simulated data example
| plt.plot(ph_energies_centers, (ph_model | srm_model)(ph_energies), label="photon model features") | ||
| plt.plot(ph_energies_centers, GaussianModel(**sim_gauss)(ph_energies), label="gaussian feature") | ||
| plt.plot(ph_energies_centers, sim_count_model, label="total sim. spectrum") | ||
| plt.plot(obs_spec._spectral_axis, obs_spec.data, label="total sim. spectrum + noise", lw=0.5) |
There was a problem hiding this comment.
I would go further and say any models should use plt.stairs but the final data plt.plot should use plt.errorbar.
| def evaluate(self, x, c): | ||
| # Requires input must have a specific dimensionality | ||
| return model_y @ self.matrix | ||
|
|
||
| input_widths = np.diff(self.input_axis) | ||
| output_widths = np.diff(self.output_axis) | ||
|
|
||
| flux = ((x * input_widths) @ self.matrix * c) / output_widths | ||
|
|
||
| if hasattr(c, "unit"): | ||
| return flux | ||
| return flux.value |
There was a problem hiding this comment.
I wrote a similar comment later then found this. I agree too.
| self.input_axis = input_axis | ||
| self.output_axis = output_axis | ||
| self.matrix = matrix | ||
| super().__init__(c) |
There was a problem hiding this comment.
I think, I think, c is a stand in for when the matrix model will have response parameters for gain slopes and offsets. The other reason was because Astropy complains when models don't have a fittable parameter. @jajmitchell can correct me on this.
| input_widths = np.diff(self.input_axis) | ||
| output_widths = np.diff(self.output_axis) | ||
|
|
||
| flux = ((x * input_widths) @ self.matrix * c) / output_widths |
There was a problem hiding this comment.
Just looking at the maths here, I think the /output_widths here is a typo. This would likely move the returned flux away from the data units, would it not?
|
@settwi @KriSun95, thanks for the comments, I will make some changes based on the suggestions, however perhaps at this stage its best to remove the |
Sure, good idea. Feel free to revert the matrix commit I made btw. I’m not sure if it lines up with the intention of the “c” parameter. Making it transparent (you don’t need to pass it explicitly to the constructor) seemed like a better approach to me |
|
@settwi @samaloney @KriSun95 would it make sense to sort this and merge it, or given that the unit handling will completely change within the next week or two with the implementation of the fitter, should we close this PR and add the SRM in the fitting PR, along with the changes to the unit handling of the other models? |
|
I don't have a strong opinion either way, but prob lean towards merging. Also there's now bunch of changes to fix the doc builds so if we don't merge this I'll cherry pick then to a separate PR. |
|
Sure! I'm happy to vote to merge then decide on dimension handling stuff later. When it comes to the A simulated/custom data fit example for the legacy code already exists in the Fitting Custom Spectra example. |
SRM model handles units
Adds in the SRM model developed in the PR https://github.com/sunpy/sunkit-spex/pull/163.
This enables physical models to be piped through the SRM model with correct unit handling and then fit to data in count space.