Pass SpectralAxis to physical models rather than energy edges#223
Pass SpectralAxis to physical models rather than energy edges#223jajmitchell wants to merge 29 commits intosunpy:mainfrom
Conversation
Adds SRM units and passes Spectralaxis to thermal
Checks input is SpectralAxis object, converts and warns if not
Adds check input function
ThickTarget and ThinTarget use centers
Albedo now takes SpectralAxis and centers
Pre-commit
scaling.py and models.py now function with centers
Fixes scaling and albedo tests
Adds __array_finalize__ method to SpectralAxis
Fixes tests
Fixes part of fitting_simulated_data.py
Reverts SRM model to main version
Fixes fitting tests
Fixes test_models.py
Fixes Albedo example
Fix Albedo example 2
Fixes examples
Adds changelog
Pre_commit fix
|
Nice is this ready for review? |
@samaloney I need to just check that the fitting works with the physical models, then I think its good to go |
|
@samaloney the complications continue. The issue comes when fitting, specifically in Astropy fitting.py line 255, as can be seen below. This means that for fitting, before being passed to evaluate, the fitter takes the value of the SpectralAxis object, which not only strips the units, but also all other attributes. I'm trying to think of ways around this, but haven't come up with anything yet. |
Adds custom `__call__` to `ThermalEmission`
Fixes test_thermal.py
|
I have added a custom |
Adds meta to SpectralAxis, thermal model can now take Spectrum object, Spectral axis, or array
Can pass array without initialising
Fitting works
Remove unused functions
Fixes continuum and line models
settwi
left a comment
There was a problem hiding this comment.
This is great, I think a few tweaks need to happen, the main two things i noticed:
- rename variables from "e_centers" to spectral axis
- enforce the SpectralAxis API everywhere; throw an error if SpectralAxis not used, don't try to coerce centers or edges into an axis. Keeps the API clear for users.
| def test_albedo_model(): | ||
| e_edges = np.linspace(10, 300, 10) * u.keV | ||
| e_centers = e_edges[0:-1] + (0.5 * np.diff(e_edges)) | ||
| e_centers = SpectralAxis(e_edges, bin_specification="edges") |
There was a problem hiding this comment.
Variable name should change (here and elsewhere) to reflect the updated type
| e_c = e_ph[:-1] + 0.5 * np.diff(e_ph) | ||
| spec_in = e_c**-2 | ||
| e_ph = (np.arange(11) * 2 + 10) * u.keV | ||
| e_c = SpectralAxis(e_ph, bin_specification="edges") |
| mg, al, si, s, ar, ca, fe = 8.15, 7.04, 8.1, 7.27, 6.58, 6.93, 8.1 | ||
| inputs = ( | ||
| energy_edges, temperature, emission_measure, mg, al, si, s, ar, ca, fe, abundance_type | ||
| spectral_axis, temperature, emission_measure, mg, al, si, s, ar, ca, fe, abundance_type |
There was a problem hiding this comment.
might want this to be a dictionary?
| e_centers = SpectralAxis(np.linspace(5, 550, 600) * u.keV, bin_specification='edges') | ||
| source = PowerLaw1D(amplitude=1*u.ph/(u.cm*u.s), x_0=5*u.keV, alpha=3) | ||
| albedo = Albedo(energy_edges=e_edges) | ||
| albedo = Albedo(spectral_axis=e_centers) |
| def _check_input_type(spectral_axis): | ||
| if isinstance(spectral_axis, SpectralAxis): | ||
| energy_edges = spectral_axis.bin_edges | ||
| else: | ||
| warnings.warn( | ||
| "As a SpectralAxis object was not passed, bin edges will be calculated as averages from the centers given.", | ||
| UserWarning, | ||
| ) | ||
| spectral_axis = SpectralAxis(spectral_axis, bin_specification="centers") | ||
| energy_edges = spectral_axis.bin_edges | ||
|
|
||
| return energy_edges |
There was a problem hiding this comment.
should throw an error if spectral axis not passed, not a warning. that's the API change right?
| # def _check_input_type(spectral_axis): | ||
| # if isinstance(spectral_axis, SpectralAxis): | ||
| # energy_edges = spectral_axis.bin_edges | ||
| # else: | ||
| # warnings.warn( | ||
| # "As a SpectralAxis object was not passed, bin edges will be calculated as averages from the centers given.", | ||
| # UserWarning, | ||
| # ) | ||
| # spectral_axis = SpectralAxis(spectral_axis, bin_specification="centers") | ||
| # energy_edges = spectral_axis.bin_edges | ||
|
|
||
| # return energy_edges |
There was a problem hiding this comment.
repeated code from before, so delete
| self.spectral_axis = spectral_axis | ||
|
|
||
| if abundance_type != DEFAULT_ABUNDANCE_TYPE: | ||
| mg, al, si, s, ar, ca, fe = _initialize_abundances(DEFAULT_ABUNDANCES[abundance_type]) |
There was a problem hiding this comment.
does this include the abundance global state fix from main?
| if hasattr(temperature, "unit"): | ||
| return flux | ||
| return flux.value | ||
|
|
There was a problem hiding this comment.
should we support unitless model outputs? (existential question)
| def _spectral_axis_process(temperature, energy_edges, spectral_axis): | ||
| if spectral_axis is not None: | ||
| if isinstance(spectral_axis, SpectralAxis): | ||
| if ( | ||
| hasattr(spectral_axis, "_meta") | ||
| and isinstance(spectral_axis._meta, dict) | ||
| and "photon_axis" in spectral_axis._meta | ||
| ): | ||
| spectral_axis_clean = spectral_axis._meta["photon_axis"] | ||
| warnings.warn("Using user defined photon_axis for evaluation") | ||
| elif hasattr(spectral_axis, "_bin_edges"): | ||
| spectral_axis_clean = spectral_axis._bin_edges | ||
| warnings.warn("Spectrum object has no photon axis stored in meta. Using bin edges.") | ||
| else: | ||
| spectral_axis_clean = SpectralAxis._edges_from_centers(spectral_axis.value, spectral_axis.unit) | ||
| warnings.warn("Calculating bin edges from centers as only centers passed to SpectralAxis.") | ||
| else: | ||
| spectral_axis_clean = None | ||
|
|
||
| if hasattr(temperature, "unit"): | ||
| temperature = temperature.to(u.K) | ||
|
|
||
| if isinstance(energy_edges, SpectralAxis): | ||
| if ( | ||
| hasattr(energy_edges, "_meta") | ||
| and isinstance(energy_edges._meta, dict) | ||
| and "photon_axis" in spectral_axis._meta | ||
| ): | ||
| energy_edges = energy_edges._meta["photon_axis"] | ||
| elif hasattr(energy_edges, "_bin_edges"): | ||
| energy_edges = energy_edges._bin_edges | ||
| else: | ||
| energy_edges = SpectralAxis._edges_from_centers(energy_edges.value, energy_edges.unit) | ||
|
|
||
| if spectral_axis is not None: | ||
| if not np.array_equal(energy_edges, spectral_axis_clean): | ||
| raise ValueError("Evaluation axis must matched initialised spectral axis.") | ||
| energy_edges = spectral_axis_clean | ||
| warnings.warn( | ||
| "User has initialised with a spectral axis, therefore model will be evaluated based on this.", | ||
| UserWarning, | ||
| ) | ||
| else: | ||
| energy_edges = SpectralAxis._edges_from_centers(centers=energy_edges.value, unit=energy_edges.unit) | ||
|
|
||
| else: | ||
| temperature = (temperature * u.MK).to_value(u.K) | ||
|
|
||
| if spectral_axis_clean is not None: | ||
| if not np.array_equal(energy_edges, spectral_axis.value): | ||
| raise ValueError("Evaluation axis must matched initialised spectral axis.") | ||
| energy_edges = spectral_axis_clean.value | ||
| warnings.warn( | ||
| "User has initialised with a spectral axis, therefore model will be evaluated based on this.", | ||
| UserWarning, | ||
| ) | ||
| else: | ||
| energy_edges = SpectralAxis._edges_from_centers(centers=energy_edges, unit=u.keV).value | ||
|
|
||
| return energy_edges, temperature |
There was a problem hiding this comment.
again, isn't the API that the SpectralAxis must be used? probably just check if not isinstance(axis, SpectralAxis): <throw an error>
| @property | ||
| def spectral_axis_index(self): | ||
| return self._spectral_axis_index | ||
| # def __array_finalize__(self, obj): |
This PR enables the physical models to be used with a SpectralAxis
By passing a SpectralAxis to the physical models, they are effectively evaluated at bin centers, but have access to the bin edges as these are stored as an attribute in the SpectralAxis class. This change will allow for a clearer path to using Astropy model classes for fitting as the dimensionality of the input and output will no longer be an issue.