Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add xspec convolution api #750

Merged
merged 6 commits into from
Jul 24, 2020

Conversation

DougBurke
Copy link
Contributor

@DougBurke DougBurke commented Mar 3, 2020

Summary

Add support for XSPEC convolution-style models - this link is valid for XSPEC 12.10.1 documentation, it may need changing once the next version is released - such as cflux and reflect.

Fixes #68

Notes

Since these models change the results of other models they are applied to models rather than combined with the +, *, -, or / operators. That is, you say xscflux.cfl(mdlexpr) to create a model instance called cfl which is applied to the model stored in mdlexpr (which could be a single component or multiple components). You can not use a model instance (e.g. cfl) directly - that is cfl * xspowerlaw.pl will not work.

Examples

The following were run with

>>> from sherpa.astro import xspec                                          
>>> print(xspec.get_xsversion())                                            
12.11.0

Previous versions used version 12.10.1

Example: cflux

The cflux model is a "hack" that lets a user find out the integrated flux for a model (over the emin to emax range of the cflux component) via the models lg10Flux parameter. It can be applied to multiple components but here I show it calculating the unabsorbed flux for a powerlaw fit. Note that you need to freeze the amplitude of the model expression it convolves (otherwise the parameters are degenerate and the optimiser will have a fit if you try to fit).

In the following I get the unabsorbed flux for the 0.5-7 keV range from cflux and then from Sherpa's calc_energy_flux command. I haven't tried any of the other "sherpa flux" functionality, which might be interesting to look at how the errors compare, but that is outside of this PR, as I just want to show that cflux is behaving sensibly here.

>>> from sherpa.astro import ui
>>> ui.load_pha('sherpa-test-data/sherpatest/3c273.pi')
>>> ui.subtract()
>>> ui.ignore(None, 0.5)
>>> ui.ignore(7, None)
>>> ui.set_source(ui.xsphabs.gal * ui.xscflux.cfl(ui.powlaw1d.pl))
>>> pl.ampl.frozen = True
>>> cfl.emin = 0.5
>>> cfl.emax = 7
>>> print(ui.get_source())
(xsphabs.gal * xscflux.cfl(powlaw1d.pl))
   Param        Type          Value          Min          Max      Units
   -----        ----          -----          ---          ---      -----
   gal.nH       thawed            1            0       100000 10^22 atoms / cm^2
   cfl.Emin     frozen          0.5            0        1e+06        keV
   cfl.Emax     frozen            7            0        1e+06        keV
   cfl.lg10Flux thawed          -12         -100          100        cgs
   pl.gamma     thawed            1          -10           10           
   pl.ref       frozen            1 -3.40282e+38  3.40282e+38           
   pl.ampl      frozen            1            0  3.40282e+38           

>>> print(ui.get_model().name)                                             
apply_rmf(apply_arf((38564.608926889 * (xsphabs.gal * xscflux.cfl(powlaw1d.pl)))))
>>> ui.fit()
Dataset               = 1
Method                = levmar
Statistic             = chi2gehrels
Initial fit statistic = 307.417
Final fit statistic   = 20.9832 at function evaluation 29
Data points           = 40
Degrees of freedom    = 37
Probability [Q-value] = 0.984159
Reduced statistic     = 0.567113
Change in statistic   = 286.434
   gal.nH         0.0110431    +/- 0.0582018   
   cfl.lg10Flux   -12.1021     +/- 0.0409627   
   pl.gamma       1.98732      +/- 0.155614    

>>> ui.conf()
gal.nH lower bound:	-----
gal.nH upper bound:	0.060225
pl.gamma lower bound:	-0.101081
pl.gamma upper bound:	0.15686
cfl.lg10Flux lower bound:	-0.0292446
cfl.lg10Flux upper bound:	0.0419616
Dataset               = 1
Confidence Method     = confidence
Iterative Fit Method  = None
Fitting Method        = levmar
Statistic             = chi2gehrels
confidence 1-sigma (68.2689%) bounds:
   Param            Best-Fit  Lower Bound  Upper Bound
   -----            --------  -----------  -----------
   gal.nH          0.0110431        -----     0.060225
   cfl.lg10Flux     -12.1021   -0.0292446    0.0419616
   pl.gamma          1.98732    -0.101081      0.15686

which can be compared to the Sherpa fit without the component:

>>> ui.set_source(gal * pl)
>>> pl.ampl.frozen = False
>>> ui.fit()                                                               
Dataset               = 1
Method                = levmar
Statistic             = chi2gehrels
Initial fit statistic = 1.03755e+10
Final fit statistic   = 20.9832 at function evaluation 17
Data points           = 40
Degrees of freedom    = 37
Probability [Q-value] = 0.984159
Reduced statistic     = 0.567113
Change in statistic   = 1.03755e+10
   gal.nH         0.0110321    +/- 0.0581733   
   pl.gamma       1.98731      +/- 0.155599    
   pl.ampl        0.000185457  +/- 3.28374e-05 

>>> ui.conf()
gal.nH lower bound:	-----
gal.nH upper bound:	0.0602362
pl.ampl lower bound:	-1.806e-05
pl.ampl upper bound:	3.5531e-05
pl.gamma lower bound:	-0.101072
pl.gamma upper bound:	0.156001
Dataset               = 1
Confidence Method     = confidence
Iterative Fit Method  = None
Fitting Method        = levmar
Statistic             = chi2gehrels
confidence 1-sigma (68.2689%) bounds:
   Param            Best-Fit  Lower Bound  Upper Bound
   -----            --------  -----------  -----------
   gal.nH          0.0110321        -----    0.0602362
   pl.gamma          1.98731    -0.101072     0.156001
   pl.ampl       0.000185457   -1.806e-05   3.5531e-05

We can see that nH and gamma are essentially the same, and the reduced statistic is the same (0.567), which you would hope if cflux is working correctly. We can compare the flux too

>>> flux = ui.calc_energy_flux(lo=0.5, hi=7, model=pl)
>>> import numpy as np
>>> print(np.log10(flux))
-12.102113047531182
>>> print(cfl.lg10Flux.val)                                                
-12.102111949073732

So they agree to the third decimal place (after rounding).

Example: zashift

The zashift model redshifts an "additive" style model. Here we use it to redshift a flat background plus box (10-14 keV) from a redshift of 1 to the observed band. I chose these limits since the RMF cuts off at 11 keV, so we can see the need for the "regrid" support (and check how this works).

>>> ui.set_source(ui.xszashift.zsh(ui.box1d.box + ui.const1d.bgnd))
>>> print(ui.get_source())                                                 
xszashift.zsh((box1d.box + const1d.bgnd))
   Param        Type          Value          Min          Max      Units
   -----        ----          -----          ---          ---      -----
   zsh.Redshift frozen            0       -0.999           10           
   box.xlow     thawed            0 -3.40282e+38  3.40282e+38           
   box.xhi      thawed            0 -3.40282e+38  3.40282e+38           
   box.ampl     thawed            1           -1            1           
   bgnd.c0      thawed            1 -3.40282e+38  3.40282e+38           

>>> zsh.redshift = 1
>>> box.xlo = 10
>>> box.xhi = 14

We can see what these look like (manually using matplotlib to create two plots since ui.plot does not handle PHA plots well, which is an issue somewhere but I'm not going to look for it as it's irrelevant to this PR):

>>> from matplotlib import pyplot as plt
>>> plt.subplot(2, 1, 1)
>>> ui.plot_source(clearwindow=False)
>>> plt.subplot(2, 1, 2)
>>> ui.plot_model(clearwindow=False)

plots

You can see that the box model has been redshifted from 10-14 keV to 5-7 keV (the background is a constant so it appears unshifted), except that the RMF cut off at 11 keV means that there's no signal above 11/(1+z)=5.5 keV. This is not an issue with this PR, but is my original driver for the regrid code.

Example: zshift + regrid

Can we use the regrid support to extend the array over which the convolution is performed? To avoid known issues with the current regrid support when there's partial overlap of the grid (which are being worked on) I am going to make sure the "regrid" grid covers the RMF grid (which is 0.1 to 11 keV with a spacing of 0.01 keV).

I think this example - namely how regrid is used to create a new model - has a certain amount of logic behind it, but I am not convinced it is the best API for users of the UI layer, but this is related to the regrid support and is not part of this PR (i.e. I don't want bike-shedding on this part of Sherpa to slow down this PR :-)

>>> rmf = ui.get_rmf()
>>> print((rmf.energ_lo[0], rmf.energ_hi[-1]))                             
(0.10000000149011612, 11.0)
>>> print(rmf.energ_hi[0] - rmf.energ_lo[0])                               
0.009999997913837433
>>> rgrid = np.arange(0.1, 24, 0.01)
>>> print(ui.get_source())                                                 
xszashift.zsh((box1d.box + const1d.bgnd))
   Param        Type          Value          Min          Max      Units
   -----        ----          -----          ---          ---      -----
   zsh.Redshift frozen            1       -0.999           10           
   box.xlow     thawed           10 -3.40282e+38  3.40282e+38           
   box.xhi      thawed           14 -3.40282e+38  3.40282e+38           
   box.ampl     thawed            1           -1            1           
   bgnd.c0      thawed            1 -3.40282e+38  3.40282e+38           

>>> orig_mdl = ui.get_source()
>>> shifted_model = orig_mdl.regrid(rgrid[:-1], rgrid[1:])
>>> ui.set_source(shifted_model)
>>> print(ui.get_source())                                                
regrid1d(xszashift.zsh((box1d.box + const1d.bgnd)))
   Param        Type          Value          Min          Max      Units
   -----        ----          -----          ---          ---      -----
   zsh.Redshift frozen            1       -0.999           10           
   box.xlow     thawed           10 -3.40282e+38  3.40282e+38           
   box.xhi      thawed           14 -3.40282e+38  3.40282e+38           
   box.ampl     thawed            1           -1            1           
   bgnd.c0      thawed            1 -3.40282e+38  3.40282e+38           

After this we can see that the source is evaluated "correctly", in that the box is defined over the 5-7 keV range, and the background is still there at higher energies (this is not meant to be a realistic model, which is why the plot_model output looks strange):

>>> plt.subplot(2, 1, 1)
>>> ui.plot_source(clearwindow=False)
>>> plt.subplot(2, 1, 2)
>>> ui.plot_model(clearwindow=False)

plots

What happens when the models are used incorrectly

That is, how useful are the error messages? I am going to try and compare to existing behavior where possible.

Do not supply an instance name

We see the same error as with the existing models:

>>> ui.set_source(ui.powlaw1d)
...
ArgumentTypeErr: 'source model' must be a model object or model expression string
>>> ui.set_source(ui.xscflux)
...
ArgumentTypeErr: 'source model' must be a model object or model expression string 

and

>>> ui.set_source(ui.xsphabs.gal * ui.powlaw1d)
...
TypeError: float() argument must be a string or a number, not 'ModelWrapper'
>>> ui.set_source(ui.xsphabs.gal * ui.xscflux)
...
TypeError: float() argument must be a string or a number, not 'ModelWrapper'

Do not include a model instance

The following is incorrect, since fluxmodel has no model expression to work with, but there's no error when we call set_source. Instead, we see the error when the model is evaluated (e.g. plot_source, calc_stat, fit, ...), and unfortunately they are different, and neither are particularly useful.

>>> ui.set_source(ui.xscflux.fluxmodel)
>>> ui.plot_source()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-43d3ac1dbe0f> in <module>
----> 1 ui.plot_source()

<string> in plot_source(id, lo, hi, replot, overplot, clearwindow, **kwargs)

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/ui/utils.py in plot_source(self, id, lo, hi, replot, overplot, clearwindow, **kwargs)
  11211             self._plot(id, self._astrosourceplot, None, None, lo, hi,
  11212                        replot=replot, overplot=overplot,
> 11213                        clearwindow=clearwindow, **kwargs)
  11214         else:
  11215             self._plot(id, self._sourceplot, replot=replot,

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/ui/utils.py in _plot(self, id, plotobj, *args, **kwargs)
  11693 
  11694         if not sherpa.utils.bool_cast(kwargs.pop('replot', False)):
> 11695             obj = self._prepare_plotobj(id, plotobj, *args)
  11696         try:
  11697             sherpa.plot.begin()

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/ui/utils.py in _prepare_plotobj(self, id, plotobj, resp_id, bkg_id, lo, hi, orders, model)
  10955             data = self.get_data(id)
  10956             src = self.get_source(id)
> 10957             plotobj.prepare(data, src, lo, hi)
  10958         elif isinstance(plotobj, sherpa.plot.SourcePlot):
  10959             data = self.get_data(id)

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/plot.py in prepare(self, data, src, lo, hi)
    144         self.xlo, self.xhi = data._get_indep(filter=False)
    145         self.mask = filter_bins((lo,), (hi,), (self.xlo,))
--> 146         self.y = src(self.xlo, self.xhi)
    147         prefix_quant = 'E'
    148         quant = 'keV'

TypeError: __call__() takes 2 positional arguments but 3 were given

>>> ui.calc_stat()                                                         
WARNING: data set 1 has associated backgrounds, but they have not been subtracted, nor have background models been set
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-94340e5b917e> in <module>
----> 1 ui.calc_stat()

<string> in calc_stat(id, *otherids)

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/ui/utils.py in calc_stat(self, id, *otherids)
   8072         """
   8073         ids, f = self._get_fit(id, otherids)
-> 8074         return f.calc_stat()
   8075 
   8076     def calc_chisqr(self, id=None, *otherids):

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/fit.py in calc_stat(self)
   1114         """
   1115 
-> 1116         return self._calc_stat()[0]
   1117 
   1118     def calc_chisqr(self):

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/fit.py in _calc_stat(self)
   1095         # TODO: is there anything missing here that
   1096         #       self._iterfit.get_extra_args calculates?
-> 1097         return self.stat.calc_stat(self.data, self.model)
   1098 
   1099     def calc_stat(self):

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/stats/__init__.py in calc_stat(self, data, model)
    544 
    545     def calc_stat(self, data, model):
--> 546         fitdata, modeldata = self._get_fit_model_data(data, model)
    547 
    548         return self._calc(fitdata[0], modeldata,

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/stats/__init__.py in _get_fit_model_data(self, data, model)
    204         data, model = self._validate_inputs(data, model)
    205         fitdata = data.to_fit(staterrfunc=self.calc_staterror)
--> 206         modeldata = data.eval_model_to_fit(model)
    207 
    208         return fitdata, modeldata

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/data.py in eval_model_to_fit(self, modelfuncs)
    885 
    886         for func, data in zip(modelfuncs, self.datasets):
--> 887             total_model.append(data.eval_model_to_fit(func))
    888 
    889         return numpy.concatenate(total_model)

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/data.py in eval_model_to_fit(self, modelfunc)
   1691 
   1692     def eval_model_to_fit(self, modelfunc):
-> 1693         return self.apply_filter(modelfunc(*self.get_indep(filter=True)))
   1694 
   1695     def sum_background_data(self,

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/models/model.py in __call__(self, *args, **kwargs)
    262         if (len(args) == 0 and len(kwargs) == 0):
    263             return self
--> 264         return self.calc([p.val for p in self.pars], *args, **kwargs)
    265 
    266     def _get_thawed_pars(self):

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/instrument.py in calc(self, p, x, xhi, *args, **kwargs)
    567         else:
    568             xhi = self.xhi
--> 569         src = self.model.calc(p, xlo, xhi)
    570         bin_mask = self.rmf.bin_mask
    571         if bin_mask is not None and \

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/models/model.py in calc(self, p, *args, **kwargs)
    614         nlhs = len(self.lhs.pars)
    615         lhs = self.lhs.calc(p[:nlhs], *args, **kwargs)
--> 616         rhs = self.rhs.calc(p[nlhs:], *args, **kwargs)
    617         try:
    618             val = self.op(lhs, rhs)

/lagado2.real/local/anaconda/envs/sherpa-add-xspec-convolution-api/lib/python3.7/site-packages/sherpa-4.7+2194.g06d19b0.dirty-py3.7-linux-x86_64.egg/sherpa/astro/xspec/__init__.py in calc(self, pars, rhs, *args, **kwargs)
   1076         #
   1077         # fluxes = np.asarray(rhs(rpars, *args, **kwargs))
-> 1078         fluxes = np.asarray(rhs(rpars, *args))
   1079         return self._calc(lpars, fluxes, *args)
   1080 

TypeError: 'numpy.ndarray' object is not callable

>>> ui.fit()
... same error as from calc_stat ...

It looks like more-complicated incorrect expressions such as

>>> ui.set_source(ui.xsphabs.gal * ui.xscflux.cfl * ui.xspowerlaw.pl)

return the "numpy.ndarray" type error even for ui.plot_source.

Details

Sherpa contains "C level" bindings to the XSPEC convolution models, but we have not provided "python level" access to these from Sherpa. I have maintained such an interface in the SDS contributed code, but now that the regrid support is in (needed to use a number of XSPEC convolution models), it makes sense to move the code into Sherpa.

Originally I had added a number of "load" routines (based on load_psf), one for each convolution model, to create an instance of the model, but in this PR I have decided not to provide this interface, as it isn't needed. There's nothing in this PR that means we couldn't add in such routines (or a single load routine that takes the convolution-model name as an argument, for instance), if we think it's useful (but I'd rather not as I don't think it helps users and just gives us more things to document/test and increases the API).

Each model is an instance of the XSConvolutionKernel class, and when applied to other models it creates a XSConvolutionModel instance. I based this on how the PSF convolution kernel is set up. It works, but is an area of the design that needs checking (ie that it makes sense). The code changes are quite small however, which suggests that it fits into Sherpa's "design".

@DougBurke DougBurke changed the title WIP: Add xspec convolution api Add xspec convolution api Mar 4, 2020
@DougBurke
Copy link
Contributor Author

I've rebased this to check the tests still work after the regrid changes in #747

@DougBurke
Copy link
Contributor Author

Will rebase this soon because of #732 (a change to a comment line, so no user-visible changes)

Copy link
Contributor

@hamogu hamogu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where this is going and I think it's an important addition to Sherpa supporting all the XSPEC models out there. I can't say that I understand all the code, but the tests look pretty extensive.

From an UI perspective, I wonder if we need to do more for the way that models are defined.
@DougBurke says

That is, you say xscflux.cfl(mdlexpr) to create a model instance called cfl which is applied to the model stored in mdlexpr (which could be a single component or multiple components). You can not use a model instance (e.g. cfl) directly - that is cfl * xspowerlaw.pl will not work.

I agree the "*" should not work, but how about clf(xspowerlaw)? I guess that won't work, because "(..)" calls the model for evaluation, but it's what I naively expected to do. On the other hand, that's how the OO layer allows it (https://github.com/sherpa/sherpa/pull/750/files#diff-ac7835014238b3cefc8770664f5af6e0R493). Is there a way we can make cfl(mdlexpr) (i.e. with a cfl that's been used before e.g. for some other dataset) work, in addition to xscflux.cfl(mdlexpr)? (Or maybe I'm mistaken and it already works?). If not, can we use some other binary operator to express convulution models, e.g. cfl @ mdlexpr or clf | mldexpr (although the later one looks like a pipe and thus the ordering would be confusing (i.e. should it be mldexpr | clf).

I do think that this needs to be explained in the rst docs (I know that this PR is not at the stage yet, but I'm just saying). Most of the text could just be taken from the description of he issue.

@DougBurke
Copy link
Contributor Author

What I meant was you can not say cfl * mdl. You can say cfl(mdl).

@hamogu
Copy link
Contributor

hamogu commented Jul 2, 2020

@DougBurke Fantastic. I agree that that's exactly how it should be. I wonder (not as part if this PR) if we can treat the pile-up model the same way. jdpileup is essentially a convolution model, it's the only pile-up model that's implemented and it currently has all this extra UI associated with just that one model (set_pileup, there is an issue somewhere whose number I don' remember now that you can't unset a pile-up model, etc.).

@DougBurke
Copy link
Contributor Author

I've rebased to pick up the python 3.8 changes. It has also pointed out that the calc_energy_flux-related test had to be changed (thanks to #756) so I've added a commit to fix that.

@DougBurke
Copy link
Contributor Author

DougBurke commented Jul 10, 2020

@hamogu - I've added more documentation. I don't really know what else to add here.

edited to add although saying that, I'm not sure how if the "ui" documentation needs more work, as this has been mainly focussed on the lower level parts of the API. Some of the models do contain hopefully-useful docstrings: e.g.

In [1]: from sherpa.astro import ui
WARNING: imaging routines will not be available,
failed to import sherpa.image.ds9_backend due to
'RuntimeErr: DS9Win unusable: Could not find ds9 on your PATH'

In [2]: ui.xscflux.mdl
Out[2]: <XScflux kernel instance 'xscflux.mdl'>

In [3]: help(mdl)
Help on XScflux in module sherpa.astro.xspec object:

class XScflux(XSConvolutionKernel)
 |  XScflux(name='xscflux')
 |
 |  The XSPEC cflux convolution model: calculate flux
 |
 |  The model is described at [1]_.
 |
 |  Attributes
 |  ----------
 |  Emin
 |      Minimum energy over which the flux is calculated.
 |  Emax
 |      Maximum energy over which the flux is calculated.
 |  lg10Flux
 |      log (base 10) of the flux in erg/cm^2/s
 |
 |  See Also
 |  --------
 |  XSclumin, XScpflux
 |
 |  Notes
 |  -----
 |  Unlike XSPEC, the convolution model is applied directly to the model, or
 |  models, rather then using the multiplication symbol.
 |
 |  Examples
 |  --------
 |
 |  With the following definitions:
 |
 |  >>> cflux = XScflux()
 |  >>> absmdl = XSphabs()
 |  >>> plmdl = XSpowerlaw()
 |  >>> gmdl = XSgaussian()
 |  >>> srcmdl = plmdl + gmdl
 |
 |  then the model can be applied in a number of ways, such as:
 |
 |  >>> mdl1 = cflux(absmdl * srcmdl)
 |  >>> mdl2 = absmdl * cflux(srcmdl)
 |  >>> mdl3 = absmdl * (plmdl + cflux(gmdl))
 |
 |  See [1]_ for the meaning and restrictions.
 |
 |  References
 |  ----------
...

@anetasie
Copy link
Contributor

@DougBurke I build it and tested. The API is good. Naming a convolution model is consistent with the Sherpa general treatment of the models, so it should be clear to the users.

One question that I have is whether in case of xscflux the flux is treated as a parameter, so it is fitted and this is why the normalization of other parameter needs to be frozen. In my test I kept p1.norm thaw (forgot to freeze) and the p1.norm exceeded 1e11. I set it to 1 and froze, and at this point nothing has changed during the fit and statistic and parameters were the same.

Dataset               = 1
Method                = levmar
Statistic             = chi2gehrels
Initial fit statistic = 1023.38
Final fit statistic   = 110.763 at function evaluation 56
Data points           = 446
Degrees of freedom    = 442
Probability [Q-value] = 1
Reduced statistic     = 0.250595
Change in statistic   = 912.618
   abs1.nH        0.0627024    +/- 0.0408719   
   cfl.lg10Flux   -12.6722     +/- 0.032427    
   p1.PhoIndex    1.97217      +/- 0.201197    
   p1.norm        2.13647e+11  +/- 0           

p1.norm = 1                                                                            

In [50]: freeze(p1.norm)                                                                        

In [51]: fit()                                                                                  
Dataset               = 1
Method                = levmar
Statistic             = chi2gehrels
Initial fit statistic = 110.763
Final fit statistic   = 110.763 at function evaluation 9
Data points           = 446
Degrees of freedom    = 443
Probability [Q-value] = 1
Reduced statistic     = 0.25003
Change in statistic   = 4.36486e-07
   abs1.nH        0.0627136    +/- 0.0408664   
   cfl.lg10Flux   -12.6722     +/- 0.0324225   
   p1.PhoIndex    1.97223      +/- 0.201226    

I know in your example the p1 was fit without cfl and then calc_energy_flux() agreed with cfl values.
I also tried to calculate the flux of p1 model after fitting with cfl component present and there is no agreement between the clf.log10Flux and the results. I have not used this model in XSPEC and I'm not sure if it is expected that cfl would be the same as p1 or not.

calc_energy_flux(0.5,10, model=p1)                                                     
Out[60]: 4.909644413155519e-09

@DougBurke
Copy link
Contributor Author

DougBurke commented Jul 16, 2020

@anetasie - the cflux parameter is the log of it, so

In [1]: 10**(-12.6722)
Out[1]: 2.1271592273629827e-13

and also I can't remember what the default limits that cflux uses (its emin/max parameters, or whatever they're called)

EDITED TO ADD just checked, and the default Emin/Emax are 0.5 to 10. It's a bit hard to compare your results as you need to

a) make sure that pl.norm = 1 and is frozen (this is mentioned in the XSPEC help, where the link https://heasarc.gsfc.nasa.gov/xanadu/xspec/manual/node280.html is valid for 12.11.0, but it's not particularly clear: if you don't have the norm frozen then it is degenerate with the lg10Flux value, and you want it set to 1 otherwise the flux will be off by 1/norm

b) it's not clear from above what happened in the lines in-between the fit and the calc_energy_flux call, so hard to know what has differed, but you'd want to

set_source(abs1 * p1)
thaw(p1.norm)
fit() [check the gamma and nh values are as they are from the cglux fit)
calc_energy_flux(0.5, 10, model=pl)

@DougBurke DougBurke requested a review from dtnguyen2 July 17, 2020 02:32
@DougBurke
Copy link
Contributor Author

@hamogu - do you still have comments on this, or are the doc updates sufficient?

@hamogu
Copy link
Contributor

hamogu commented Jul 21, 2020

Fine with me!

@DougBurke
Copy link
Contributor Author

This will need rebasing now that XSPEC 12.11.0 support (#772) has been merged.

Copy link
Contributor

@dtnguyen2 dtnguyen2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should explain why one does (or does not) do something, as opposed to mimic what the code does:

def calc(self, pars, rhs, *args, **kwargs):
         """Evaluate the convolved model.

         Note that this method is not cached by
         sherpa.models.modelCacher1d (may change in the future).

I still don't know why you decided not to cache these functions.

@DougBurke
Copy link
Contributor Author

@dtnguyen2 FYI I'm going to be rebasing this patch so that it will work now we've got support for XSPEC 12.11.0. It foesn't change any of the logic, but the commits will get changed.

@DougBurke
Copy link
Contributor Author

Comment should explain why one does (or does not) do something, as opposed to mimic what the code does:

def calc(self, pars, rhs, *args, **kwargs):
         """Evaluate the convolved model.

         Note that this method is not cached by
         sherpa.models.modelCacher1d (may change in the future).

The Python "standard" for docstrings says that the first line should be a short synopsis of the function.

I still don't know why you decided not to cache these functions.

A number of reasons:

  • it is an optimization that can come later, and does not need to be done in this PR when I am adding the base functionality
  • we don't have any existing cache code that includes a model as a parameter, which means the cache code would need a little bit of work to get going
  • I don't think we cache RMF code (which is similar in spirit if not in execution)

@hamogu
Copy link
Contributor

hamogu commented Jul 23, 2020

I agree with @DougBurke : The docstring of a function is meant to users on how to use that particular function. For that, they don't have to know why something was done, just how. Of course, inline comments in the code that are only seen by other developers by all means should explain why something was done as @dtnguyen2 suggests.

This commit takes code that was used in the SDS "sherpa contribute"
routines and moves them into Sherpa itself. It adds Python-level
support for the XSPEC convolution models. In the SDS version I had
explicit load_xs<convolution model> routines, following on from
the existing load_psf/table style routines, but I have decided that
this adds an extra layer of complexity that may not be needed. So
this commit emphasizes "direct" creation of these convolution
models. The load_xs<> style routines could be added on top of this
commit if needed.

The XSPEC cflux convolution model (that calculates the energy
flux of a model) can be used like:

    from sherpa.astro import ui
    ui.load_pha('3c273.pi')
    ui.subtract()
    ui.notice(0.5, 7)
    ui.set_source(ui.xsphabs.gal * ui.xscflux.cfl(ui.powlaw1d.pl))
    pl.ampl.frozen = True
    ui.fit()
    ui.conf()

You can also use them with regridded models, as shown below

    convsource = ui.xszashift.zsh(ui.box1d.box + ui.const1d.bgnd)
    zsh.redshift = 0.8
    rgrid = np.arange(0.1, 23, 0.01)
    box.xlo = 10
    box.xhi = 14
    shiftsource = convsource.regrid(rgrid[:-1], rgrid[1:])
    ui.set_source(shiftsource)

Documentation relies on the XSPEC documentation, but some additions
have been made (e.g. the need for a fixed normalisation/amplitude
parameter for the flux/lumin models).
There was a section that had

  if XSPEC 12.9.1
  ...
    if XSPEC > 12.9.1
    ...
    endif
  endif

I have moved them so they do not overlap.
Change some links to match the 12.11.0 version (changed since 12.10.1). I have
asked team XSPEC if we can have stable URLs for these (they used to exist
but disappeared recently).

Fixed some URLs:
  - http to https
  - add gsfc to host name (the old version no-longer works)
@DougBurke
Copy link
Contributor Author

@dtnguyen2 - I've finished rebasing / fixing things for the XSPEC 12.11.0 changes which were recently in #772

@dtnguyen2
Copy link
Contributor

@Marie-Terrell is in the process of building xspec 12.11.0 which is needed to test this PR, I will resume the review this PR once the build is complete.

@DougBurke
Copy link
Contributor Author

@dtnguyen2 - just to note that this can be run against XSPEC 12.10.1. There's only one convolution model that would be excluded by using 12.10.1 (and thanks to the code that @olaurino wrote ages ago, it will still work woith 12.10.1, it'll just error out at runttime if you try to use this model.

@dtnguyen2
Copy link
Contributor

@DougBurke, the first example that you wrote, doesn't work with 12.10.1n

(sherpa) [dtn@devel12 sherpa]$ python
Python 3.7.3 (default, Mar 27 2019, 22:11:17) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from sherpa.astro import xspec
>>> print(xspec.get_xsversion())
12.10.1n
>>> 

@DougBurke
Copy link
Contributor Author

@dtnguyen2 - it should do. What was the error you got?

This just removes the use of SherpaTest and changes things to use pytest
or basic asserts, rather than the SherpaTest functionality. The tests
are the same.
@dtnguyen2
Copy link
Contributor

...
read background file /export/sherpa/sherpa-test-data/sherpatest/3c273_bg.pi
Traceback (most recent call last):
  File "tmp.py", line 6, in <module>
    ui.set_source(ui.xsphabs.gal * ui.xscflux.cfl(ui.powlaw1d.pl))
AttributeError: module 'sherpa.astro.ui' has no attribute 'xscflux'

@DougBurke
Copy link
Contributor Author

I can't replicate this:

ipython
Python 3.8.3 (default, Jul  2 2020, 16:21:59)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.16.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from sherpa.astro import ui
WARNING: imaging routines will not be available,
failed to import sherpa.image.ds9_backend due to
'RuntimeErr: DS9Win unusable: Could not find ds9 on your PATH'

In [2]: from sherpa.astro import xspec

In [3]: xspec.get_xsversion()
Out[3]: '12.10.1m'

In [4]: mdl = ui.xsphabs.gal * ui.xscflux.cfl(ui.powlaw1d.pl)

In [5]:

The difference between patch m and n here is not relevant.

The XSPEC team have restored the additive/multiplicative/convolution
links so we can refer to them using stable URLS.

This also makes some "old" updates just to add documentation for
some 12.9.1 and later models (not a significant change).
@dtnguyen2
Copy link
Contributor

(sherpa) [dtn@devel12 dtn]$ python
Python 3.7.3 (default, Mar 27 2019, 22:11:17) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from sherpa.astro import ui
WARNING: imaging routines will not be available, 
failed to import sherpa.image.ds9_backend due to 
'RuntimeErr: DS9Win unusable: Could not find ds9 on your PATH'
>>> from sherpa.astro import xspec
>>> xspec.get_xsversion()
'12.10.1n'
>>> mdl = ui.xsphabs.gal * ui.xscflux.cfl(ui.powlaw1d.pl)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'sherpa.astro.ui' has no attribute 'xscflux'
>>> 

@DougBurke
Copy link
Contributor Author

It looks to me like a build or environment issue. If you run the following it will point you to the init.py file

% python -c 'import sherpa.astro.xspec; print(sherpa.astro.xspec.__file__)'
/home/dburke/sherpa/sherpa-xspec-12101/sherpa/astro/xspec/__init__.py

You can then see if it contains the string Convolution - e.g.

.. [4] https://heasarc.gsfc.nasa.gov/docs/xanadu/xspec/manual/Convolution.html
class XSConvolutionKernel(XSModel):
    (`XSConvolutionModel`) that is. This wrapping is done by applying
    .. [1] https://heasarc.gsfc.nasa.gov/docs/xanadu/xspec/manual/Convolution.html
        return XSConvolutionModel(model, self)
class XSConvolutionModel(CompositeModel, XSModel):
    wrapper : sherpa.astro.xspec.XSConvolutionKernel instance
    are instances of XSConvolutionModel:
class XScflux(XSConvolutionKernel):
        XSConvolutionKernel.__init__(self, name, (self.Emin,
class XSclumin(XSConvolutionKernel):
        XSConvolutionKernel.__init__(self, name, (self.Emin,
... lots more ...

@dtnguyen2
Copy link
Contributor

I had to merge a bunch of files to get it to work. How in the world did the sherpa command box.xlo = 10 work for you?

Traceback (most recent call last):
  File "tmp.py", line 28, in <module>
    box.xlo = 10
  File "/export/sherpa/sherpa/models/model.py", line 199, in __setattr__
    NoNewAttributesAfterInit.__setattr__(self, name, val)
  File "/export/sherpa/sherpa/utils/__init__.py", line 146, in __setattr__
    (type(self).__name__, name))
AttributeError: 'Box1D' object has no attribute 'xlo'

cause Box1D is defined as:

    def __init__(self, name='box1d'):
        self.xlow = Parameter(name, 'xlow', 0)
        self.xhi = Parameter(name, 'xhi', 0)
        self.ampl = Parameter(name, 'ampl', 1, -1, 1)
        ArithmeticModel.__init__(self, name, (self.xlow, self.xhi, self.ampl))

@DougBurke
Copy link
Contributor Author

DougBurke commented Jul 23, 2020 via email

@dtnguyen2
Copy link
Contributor

I rebased one of my PRs and it messed up my setup, will fix it soon.

@wmclaugh wmclaugh merged commit 00546ce into sherpa:master Jul 24, 2020
@DougBurke DougBurke deleted the add-xspec-convolution-api branch July 24, 2020 12:45
@DougBurke
Copy link
Contributor Author

Yay - thanks @dtnguyen2 and @wmclaugh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Python classes for XSpec convolution models
5 participants