StingraySoftware Notebook
StingraySoftware Notebook
Who should be using this API? Basically, anyone who wants to model power spectral
products with parametric functions. The purpose of this API is two-fold: (1) provide
convenient methods and classes in order to model a large range of typical data
representations implemented in Stingray (2) provide a more general framework for
users to build their own models
A note on terminology: in this tutorial, we largely use model to denote both the
parametric model describing the underlying process that generated the data, and the
statistical model used to account for uncertainties in the measurement process.
The modeling subpackage defines a wider range of classes for typical statistical
models than most standard modelling packages in X-ray astronomy, including
likelihoods for Gaussian-distributed uncertainties (what astronomers call the χ2
likelihood), Poisson-distributed data (e.g. light curves) and χ2-distributed data
(confusingly, not what astronomers call the χ2 likelihood, but the likelihood of data with
χ2-distributed uncertainties appropriate for power spectra). It also defines a superclass
LogLikelihood that make extending the framework to other types of data
uncertainties straightforward. It supports Bayesian modelling via the Posterior
class and its subclasses (for different types of data, equivalent to the likelihood classes)
and provides support for defining priors.
Some background
Modeling power spectra and light curves with parametric models is a fairly standard
task. Stingray aims to make solving these problems as easy as possible.
We aim to integrate our existing code with astropy.modeling for for maximum
compatibility. Please note, however, that we are only using the models, not the fitting
interface, which is too constrained for our purposes.
1 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [1]:
%load_ext autoreload
%autoreload 2
# ignore warnings to make notebook easier to see online
# COMMENT OUT THESE LINES FOR ACTUAL ANALYSIS
import warnings
warnings.filterwarnings("ignore")
In [2]:
%matplotlib inline
import matplotlib.pyplot as plt
try:
import seaborn as sns
sns.set_palette("colorblind")
except ImportError:
print("Install seaborn. It help you make prettier figures!")
import numpy as np
In [3]:
g = models.Gaussian1D()
In [4]:
# Generate fake data
np.random.seed(0)
x = np.linspace(-5., 5., 200)
y = 3 * np.exp(-0.5 * (x - 1.3)**2 / 0.8**2)
y += np.random.normal(0., 0.2, x.shape)
yerr = 0.2
plt.figure(figsize=(8,5))
plt.errorbar(x, y, yerr=yerr, fmt='ko')
2 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [5]:
# define power law component
pl = models.PowerLaw1D()
# define constant
c = models.Const1D()
We're going to pick some fairly standard parameters for our data:
In [6]:
# parameters for fake data.
alpha = 2.0
amplitude = 5.0
white_noise = 2.0
In [7]:
freq = np.linspace(0.01, 10.0, int(10.0/0.01))
In [8]:
from astropy.modeling.fitting import _fitter_to_model_params
In [9]:
psd_shape = plc(freq)
As a last step, we need to add noise by picking from a chi-square distribution with 2
degrees of freedom:
In [10]:
powers = psd_shape*np.random.chisquare(2, size=psd_shape.shape[0])/2.0
3 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [11]:
plt.figure(figsize=(12,7))
plt.loglog(freq, powers, ds="steps-mid", label="periodogram realization"
plt.loglog(freq, psd_shape, label="power spectrum")
plt.legend()
<matplotlib.legend.Legend at 0x7ff22998cfd0>
Out[11]:
In order to find the best parameter set, one generally maximizes the likelihood function
using an optimization algorithm. Because optimization algorithms generally minimize
functions, they effectively minimize the log-likelihood, which comes out to be the same
as maximizing the likelihood itself.
4 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [12]:
logmin = -1e16
class PSDLogLikelihood(object):
Parameters
----------
freq : iterable
x-coordinate of the data
power : iterable
y-coordinte of the data
m : int
1/2 of the degrees of freedom, i.e. the number of powers
that were averaged to obtain the power spectrum input into
this routine.
"""
Parameters
----------
pars : iterable
The list of parameters for which to evaluate the model.
Returns
-------
loglike : float
The log-likelihood of the model
"""
# raise an error if the length of the parameter array input into
# this method doesn't match the number of free parameters in the model
if np.size(pars) != self.npar:
raise Exception("Input parameters must" +
" match model parameters!")
5 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
if not np.isfinite(loglike):
loglike = logmin
if neg:
return -loglike
else:
return loglike
Let's make an object and see what it calculates if we put in different parameter sets.
First, we have to make our sample PSD into an actual Powerspectrum object:
In [13]:
from stingray import Powerspectrum
ps = Powerspectrum()
ps.freq = freq
ps.power = powers
ps.df = ps.freq[1] - ps.freq[0]
ps.m = 1
In [14]:
loglike = PSDLogLikelihood(ps.freq, ps.power, plc, m=ps.m)
In [15]:
test_pars = [1, 5, 100]
loglike(test_pars)
-4835.88214847462
Out[15]:
In [16]:
test_pars = [4.0, 10, 2.5]
loglike(test_pars)
-2869.5582486265116
Out[16]:
In [17]:
test_pars = [2.0, 5.0, 2.0]
loglike(test_pars)
-2375.704120812954
Out[17]:
6 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
Something close to the parameters we put in should yield the largest log-likelihood. Feel
free to play around with the test parameters to verify that this is true.
In [18]:
from stingray.modeling import PSDLogLikelihood
-2375.704120812954
Out[18]:
Now we can instantiate the PSDParEst (for PSD Parameter Estimation) object. This
can do more than simply optimize a single model, but we'll get to that later.
The PSDParEst object allows one to specify the fit method to use (however, this must
be one of the optimizers in scipy.optimize ). The parameter max_post allows for
doing maximum-a-posteriori fits on the Bayesian posterior rather than maximum
likelihood fits (see below for more details). We'll set it to False for now, since we
haven't defined any priors:
In [19]:
from stingray.modeling import PSDParEst
In [20]:
loglike = PSDLogLikelihood(ps.freq, ps.power, plc, m=ps.m)
In [21]:
loglike.model.parameters
In [22]:
loglike.npar
3
Out[22]:
In [23]:
starting_pars = [3.0, 1.0, 2.4]
res = parest.fit(loglike, starting_pars)
7 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
For example, here's the value of the likelihood function at the maximum the optimizer
found:
In [24]:
res.result
2183.789677035487
Out[24]:
Note: Optimizers routinely get stuck in local minima (corresponding to local maxima of
the likelihood function). It is usually useful to run an optimizer several times with
different starting parameters in order to get close to the global maximum.
Most useful are the estimates of the parameters at the maximum likelihood and their
uncertainties:
In [25]:
print(res.p_opt)
print(res.err)
Note: uncertainties are estimated here via the covariance matrix between parameters,
i.e. the inverse of the Hessian at the maximum. This only represents the true
uncertainties for specific assumptions about the likelihood function (Gaussianity), so
use with care!
It also computes Akaike Information Criterion (AIC) and the Bayesian Information
Criterion (BIC) for model comparison purposes:
In [26]:
print("AIC: " + str(res.aic))
print("BIC: " + str(res.bic))
AIC: 2189.789677035487
BIC: 2204.512942872433
Finally, it also produces the values of the mean function for the parameters at the
maximum. Let's plot that and compare with the power spectrum we put in:
In [27]:
plt.figure(figsize=(12,8))
plt.loglog(ps.freq, psd_shape, label="true power spectrum",lw=3)
plt.loglog(ps.freq, ps.power, label="simulated data")
plt.loglog(ps.freq, res.mfit, label="best fit", lw=3)
plt.legend()
<matplotlib.legend.Legend at 0x7ff259161910>
Out[27]:
8 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [28]:
res.print_summary(loglike)
Fitting statistics:
-- number of data points: 1000
-- Deviance [-2 log L] D = 4367.579354.3
-- The Akaike Information Criterion of the model is: 2189.789677035487.
-- The Bayesian Information Criterion of the model is: 2204.51294287243
3.
-- The figure-of-merit function for this model is: 1079.682849.5f and
the fit for 997 dof is 1.082932.3f
-- Summed Residuals S = 69267.121618.5f
-- Expected S ~ 6000.000000.5 +/- 109.544512.5
9 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
Likelihood Ratios
The parameter estimation code has more functionality than act as a simple wrapper
around scipy.optimize . For example, it allows for easy computation of likelihood
ratios. Likelihood ratios are a standard way to perform comparisons between two
models (though they are not always statistically meaningful, and should be used with
caution!).
In [29]:
# broken power law model
bpl = models.BrokenPowerLaw1D()
# add constant
bplc = bpl + c
In [30]:
bplc.param_names
In [31]:
# define starting parameters
bplc_start_pars = [2.0, 1.0, 3.0, 1.0, 2.5]
In [32]:
loglike_bplc = PSDLogLikelihood(ps.freq, ps.power, bplc, m=ps.m)
In [33]:
pval, plc_opt, bplc_opt = parest.compute_lrt(loglike, starting_pars, loglike_bplc
In [34]:
print("Likelihood Ratio: " + str(pval))
10 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
Since there are no universally accepted priors for a model (they depend on the problem
at hand and your physical knowledge about the system), they cannot be easily hard-
coded in stingray. Consequently, setting priors is slightly more complex.
In [35]:
from stingray.modeling import PSDPosterior
In [36]:
lpost = PSDPosterior(ps.freq, ps.power, plc, m=ps.m)
In [37]:
import scipy.stats
priors = {}
priors["alpha_0"] = p_alpha
priors["amplitude_0"] = p_amplitude
priors["amplitude_1"] = p_whitenoise
In [38]:
from stingray.modeling import set_logprior
In [39]:
lpost.logprior = set_logprior(lpost, priors)
You can also set the priors when you instantiate the posterior object:
11 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [40]:
lpost = PSDPosterior(ps.freq, ps.power, plc, priors=priors, m=ps.m)
Much like before with the log-likelihood, we can now also compute the log-posterior for
various test parameter sets:
In [41]:
test_pars = [1.0, 2.0, 4.0]
print("log-prior: " + str(lpost.logprior(test_pars)))
print("log-likelihood: " + str(lpost.loglikelihood(test_pars)))
print("log-posterior: " + str(lpost(test_pars)))
log-prior: -198.61635344021062
log-likelihood: -2412.2493594640564
log-posterior: -2610.865712904267
When the prior is zero (so the log-prior is -infinity), it automatically gets set to a very
small value in order to avoid problems when doing the optimization:
In [42]:
test_pars = [6, 6, 3.0]
print("log-prior: " + str(lpost.logprior(test_pars)))
print("log-likelihood: " + str(lpost.loglikelihood(test_pars)))
print("log-posterior: " + str(lpost(test_pars)))
log-prior: -1e+16
log-likelihood: -2534.0567826161864
log-posterior: -1e+16
In [43]:
test_pars = [5.0, 2.0, 2.0]
print("log-prior: " + str(lpost.logprior(test_pars)))
print("log-likelihood: " + str(lpost.loglikelihood(test_pars)))
print("log-posterior: " + str(lpost(test_pars)))
log-prior: 1.383646559789373
log-likelihood: -2184.6739536386162
log-posterior: -2183.290307078827
We can do the same parameter estimation as above, except now it's called maximum-
a-posteriori instead of maximum likelihood and includes the prior (notice we set
max_post=True ):
In [44]:
parest = PSDParEst(ps, fitmethod='BFGS', max_post=True)
res = parest.fit(lpost, starting_pars)
In [45]:
print("best-fit parameters:")
for p,e in zip(res.p_opt, res.err):
print("%.4f +/- %.4f"%(p,e))
best-fit parameters:
4.8949 +/- 0.0762
2.0690 +/- 0.0636
2.0547 +/- 0.0149
12 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [46]:
res.print_summary(lpost)
Fitting statistics:
-- number of data points: 1000
-- Deviance [-2 log L] D = 4367.845867.3
-- The Akaike Information Criterion of the model is: 2188.688941098666.
-- The Bayesian Information Criterion of the model is: 2203.41220693561
2.
-- The figure-of-merit function for this model is: 1104.686605.5f and
the fit for 997 dof is 1.108011.3f
-- Summed Residuals S = 75870.935552.5f
-- Expected S ~ 6000.000000.5 +/- 109.544512.5
Unlike in the maximum likelihood case, we can also sample from the posterior
probability distribution. The method sample uses the emcee package to do MCMC.
Important: Do not sample from the likelihood function. This is formally incorrect and
can lead to incorrect inferences about the problem, because there is no guarantee that
a posterior with improper (flat, infinite) priors will be bounded!
Important: emcee has had a major upgrade to version 3, which came with a number of
API changes. To ensure compatibility with stingray, please update emcee to the latest
version, if you haven't already.
Much like the optimizer, the sampling method requires a model and a set of starting
parameters t0 . Optionally, it can be useful to also input a covariance matrix, for
example from the output of the optimizer.
Finally, the user should specify the number of walkers as well as the number of steps to
use for both burn-in and sampling:
In [47]:
sample = parest.sample(lpost, res.p_opt, cov=res.cov, nwalkers=400,
niter=100, burnin=300, namestr="psd_modeling_test")
---------------------------------------------
13 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
The sampling method returns an object with various attributes that are useful for further
analysis, for example the acceptance fraction:
In [48]:
sample.acceptance
0.6402000000000001
Out[48]:
In [49]:
sample.mean
In [50]:
sample.ci
In [51]:
sample.print_results()
---------------------------------------------
In [52]:
fig = sample.plot_results(nsamples=1000, fig=None, save_plot=True,
filename="modeling_tutorial_mcmc_corner.pdf")
14 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
15 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
• the models are nested, i.e. the simpler model is a special case of the more complex
model and
• the parameter values that transform the complex model into the simple one do not
lie on the boundary of parameter space.
Imagine e.g. a simple model without a QPO, and a complex model with a QPO, where
in order to make the simpler model out of the more complex one you would set the QPO
amplitude to zero. However, the amplitude cannot go below zero, thus the critical
parameter value transforming the complex into the simple model lie on the boundary of
parameter space.
If these two conditions are not given, the observed likelihood ratio must be calibrated via
simulations of the simpler model. In general, one should not simulate from the best-fit
model alone: this ignores the uncertainty in the model parameters, and thus may
artificially inflate the significance of the result.
In the purely frequentist (maximum likelihood case), one does not know the shape of
the probability distribution for the parameters. A rough approximation can be obtained
by assuming the likelihood surface to be a multi-variate Gaussian, with covariances
given by the inverse Fisher information. One may sample from that distribution and then
simulate fake data sets using the sampled parameters. Each simulated data set will be
fit with both models to compute a likelihood ratio, which is then used to build a
distribution of likelihood ratios from the simpler model to compare the observed
likelihood ratio to.
In the Bayesian case, one may sample from the posterior for the parameters directly
and then use these samples as above to create fake data sets in order to derive a
posterior probability distribution for the likelihood ratios and thus a posterior predictive
p-value.
For the statistical background of much of this, see Protassov et al, 2002.
Below, we set up code that will do exactly that, for both the frequentist and Bayesian
case.
16 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [53]:
import copy
Parameters
----------
lpost : instance of a Posterior or LogLikelihood subclass
The object containing the relevant information about the
data and the model
pars : iterable
A list of parameters to be passed to lpost.model in oder
to generate a model data set.
Returns:
--------
model_data : numpy.ndarray
An array of model values for each bin in lpost.x
"""
# get the model
m = lpost.model
return model_data
Parameters:
----------
lpost : instance of a Posterior or LogLikelihood subclass
The object containing the relevant information about the
data and the model
pars : iterable
A list of parameters to be passed to lpost.model in oder
to generate a model data set.
Returns:
--------
sim_ps : stingray.Powerspectrum object
The simulated Powerspectrum object
"""
sim_ps = copy.copy(ps)
17 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
sim_ps.powers = model_powers
return sim_ps
Parameters
----------
obs_value : float
The observed value of the test statistic in question
sim: iterable
A list or array of simulated values for the test statistic
Returns
-------
pval : float [0, 1]
The p-value for the test statistic given the simulations.
"""
return pval
# sample parameters
s_all = mvn.rvs(size=nsim)
else:
if sample is None
18 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
if sample is None:
# sample the posterior using MCMC
sample = parest.sample(lpost, res1.p_opt, cov=res1.cov,
nwalkers=nwalker, niter=niter,
burnin=burnin, namestr=namestr)
lrt_sim = np.zeros(nsim)
# now I can loop over all simulated parameter sets to generate a PSD
for i,s in enumerate(s_all):
In [54]:
pval = calibrate_lrt(ps, loglike, starting_pars,
loglike_bplc, bplc_start_pars,
max_post=False, nsim=100)
In [55]:
print("The p-value for rejecting the simpler model is: " + str(pval))
19 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
As expected, the p-value for rejecting the powerlaw model is fairly large: since we
simulated from that model, we would be surprised if it generated a small p-value,
causing us to reject this model (note, however, that if the null hypothesis is true, the
p-value will be uniformely distributed between 0 and 1. By definition, then, you will get a
p-value smaller or equal to 0.01 in approximately one out of a hundred cases)
We can do the same with the Bayesian model, in which case the result is called a
posterior predictive p-value, which, in turn, is often used in posterior model checking
(not yet implemented!).
We have not yet defined a PSDPosterior object for the bent power law model, so
let's do that. First, let's define some priors:
In [56]:
import scipy.stats
priors = {}
priors["alpha_1_0"] = p_alpha
priors["alpha_2_0"] = p_alpha
priors["amplitude_0"] = p_amplitude
priors["amplitude_1"] = p_whitenoise
priors["x_break_0"] = p_x_break
In [57]:
lpost_bplc = PSDPosterior(ps.freq, ps.power, bplc, priors=priors, m=ps.m
In [58]:
lpost_bplc(bplc_start_pars)
-2230.14039643262
Out[58]:
And do the posterior predictive p-value. Since we've already sampled from the simple
model, we can pass that sample to the calibrate_lrt function, in order to cut
down on computation time (if the keyword sample is not given, it will automatically run
MCMC:
20 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [59]:
pval = calibrate_lrt(ps, lpost, starting_pars,
lpost_bplc, bplc_start_pars,
sample=sample.samples,
max_post=True, nsim=100)
In [60]:
print("The posterior predictive p-value is: p = " + str(pval))
Again, we find that the p-value does not suggest rejecting the powerlaw model.
In [61]:
from stingray.modeling import PSDParEst
In [62]:
parest = PSDParEst(ps, fitmethod="BFGS")
In [63]:
pval = parest.calibrate_lrt(lpost, starting_pars, lpost_bplc, bplc_start_pars
sample=sample.samples, nsim=100, max_post=True, seed=
In [64]:
print(pval)
0.2
21 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In Vaughan et al, 2010, a method was introduced to search for QPOs in the presence of
red noise (stochastic variability), and in Huppenkothen et al, 2013 it was extended to
magnetar bursts, and in Inglis et al, 2015 and Inglis et al, 2016 a similar approach was
used to find QPOs in solar flares.
Based on a model for the broadband spectral noise, the algorithm finds the highest
outlier in a test statistic based on the data-model residuals (under the assumption that if
the broadband model is correct, the test statistic TR = maxj(2Dj/mj) for j power
spectral bins with powers Dj and model powers mj will be distributed following a χ2
distribution with two degrees of freedom). The observed test statistic TR is then
compared to a theoretical distribution based on simulated power spectra without an
outlier in order to compute a posterior predictive p-value as above for the likelihood
ratio.
Since the concept is very similar to that above, we do not show the full code here.
Instead, the p-value can be calculated using the method
calibrate_highest_outlier , which belongs to the PSDParEst class:
In [65]:
# compute highest outlier in the data, and the frequency and index
# where that power occurs
max_power, max_freq, max_ind = parest._compute_highest_outlier(lpost, res
In [66]:
max_power
array([16.79715722])
Out[66]:
In [67]:
pval = parest.calibrate_highest_outlier(lpost, starting_pars, sample=sample
max_post=True,
nsim=100, niter=200, nwalkers=500,
burnin=200, namestr="test")
In [68]:
pval
0.15
Out[68]:
22 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
Convenience Functions
For convenience, we have implemented some simple functions to reduce overhead with
having to instantiate objects of the various classes.
Note that these convenience function use similar approaches and guesses in all cases;
this might work for some simple quicklook analysis, but when preparing publication-
ready results, one should approach the analysis with more care and make sure the
options chosen are appropriate for the problem at hand.
Please note that while this aims to use reasonable defaults, this is unlikely to produce
publication-ready results!
So let's fit a power law and a constant to some data, which we'll create below:
In [69]:
from stingray import Powerspectrum
m = 1
nfreq = 100000
freq = np.linspace(1, 1000, nfreq)
alpha_0 = 2.0
amplitude_0 = 100.0
amplitude_1 = 2.0
model.alpha_0 = alpha_0
model.amplitude_0 = amplitude_0
model.amplitude_1 = amplitude_1
p = model(freq)
power = noise * p
ps = Powerspectrum()
ps.freq = freq
ps.power = power
ps.m = m
ps.df = freq[1] - freq[0]
ps.norm = "leahy"
23 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [70]:
plt.figure()
plt.loglog(ps.freq, ps.power, ds="steps-mid", lw=2, color="black")
[<matplotlib.lines.Line2D at 0x7ff1f9f77b80>]
Out[70]:
In order to fit this, we'll write a convenience function that can take the power spectrum, a
model, some starting parameters and just run with it:
In [71]:
from stingray.modeling import PSDLogLikelihood, PSDPosterior, PSDParEst
if priors:
lpost = PSDPosterior(ps, model, priors=priors)
else:
lpost = PSDLogLikelihood(ps.freq, ps.power, model, m=ps.m)
Let's see if it works. We've already defined our model above, but to be explicit, let's
define it again:
In [72]:
model_to_test = models.PowerLaw1D() + models.Const1D()
model_to_test.x_0_0.fixed = True
In [73]:
t0 = [80, 1.5, 2.5]
In [74]:
parest, res = fit_powerspectrum(ps, model_to_test, t0)
In [75]:
res.p_opt
24 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [76]:
plt.figure()
plt.figure()
plt.loglog(ps.freq, ps.power, ds="steps-mid", lw=2, color="black")
plt.plot(ps.freq, res.mfit, lw=3, color="red")
[<matplotlib.lines.Line2D at 0x7ff22a4fe640>]
Out[76]:
<Figure size 432x288 with 0 Axes>
In [77]:
from stingray.modeling.scripts import fit_powerspectrum
In [78]:
parest, res = fit_powerspectrum(ps, model_to_test, t0)
res.p_opt
Fitting Lorentzians
Fitting Lorentzians to power spectra is a routine task for most astronomers working with
power spectra, hence there is a function that can produce either Maximum Likelihood or
Maximum-A-Posteriori fits of the data.
In [79]:
l = models.Lorentz1D
In [80]:
l.param_names
25 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [81]:
def fit_lorentzians(ps, nlor, starting_pars, fit_whitenoise=True, max_post
fitmethod="L-BFGS-B"):
model = models.Lorentz1D()
if nlor > 1:
for i in range(nlor-1):
model += models.Lorentz1D()
if fit_whitenoise:
model += models.Const1D()
In [82]:
np.random.seed(400)
nlor = 3
x_0_0 = 0.5
x_0_1 = 2.0
x_0_2 = 7.5
amplitude_0 = 150.0
amplitude_1 = 50.0
amplitude_2 = 15.0
fwhm_0 = 0.1
fwhm_1 = 1.0
fwhm_2 = 0.5
whitenoise = 2.0
p = model(ps.freq)
noise = np.random.exponential(size=len(ps.freq))
power = p*noise
plt.figure()
plt.loglog(ps.freq, power, lw=1, ds="steps-mid", c="black")
plt.loglog(ps.freq, p, lw=3, color="red")
[<matplotlib.lines.Line2D at 0x7ff2396417f0>]
Out[82]:
26 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [83]:
import copy
In [84]:
ps_new = copy.copy(ps)
In [85]:
ps_new.power = power
So now we can fit this model with our new function, but first, we need to define the
starting parameters for our fit. The starting parameters will be [amplitude, x_0,
fwhm] for each component plus the white noise component at the end:
In [86]:
t0 = [150, 0.4, 0.2, 50, 2.3, 0.6, 20, 8.0, 0.4, 2.1]
parest, res = fit_lorentzians(ps_new, nlor, t0)
In [87]:
res.p_opt
Cool, that seems to work! For convenience PSDParEst also has a plotting function:
In [88]:
parest.plotfits(res, save_plot=False, namestr="lorentzian_test")
27 of 28 10/11/22, 11:43
Notebooks https://github.com/StingraySoftware/notebooks/blob/main/Modeling...
In [89]:
from stingray.modeling import fit_lorentzians
In [90]:
parest, res = fit_lorentzians(ps_new, nlor, t0)
In [91]:
res.p_opt
28 of 28 10/11/22, 11:43