Skip to content

Commit deb0d91

Browse files
add docs, refractor stingray.models
1 parent 8026b67 commit deb0d91

File tree

1 file changed

+122
-44
lines changed

1 file changed

+122
-44
lines changed

stingray/simulator/models.py

Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ class GeneralizedLorentz1D(Fittable1DModel):
2626
power_coeff : float
2727
power coefficient [n]
2828
29+
Notes
30+
-----
31+
Model formula (with :math:`V` for ``value``, :math:`x_0` for ``x_0``,
32+
:math:`w` for ``fwhm``, and :math:`p` for ``power_coeff``):
33+
34+
.. math::
35+
36+
f(x) = V \\cdot \\left( \\frac{w}{2} \\right)^p
37+
\\cdot \\frac{1}{\\left( |x - x_0|^p + \\left( \\frac{w}{2} \\right)^p \\right)}
38+
2939
Returns
3040
-------
3141
model: astropy.modeling.Model
@@ -49,7 +59,8 @@ def evaluate(x, x_0, fwhm, value, power_coeff):
4959
@staticmethod
5060
def fit_deriv(x, x_0, fwhm, value, power_coeff):
5161
"""
52-
Gaussian1D model function derivatives.
62+
Gaussian1D model function derivatives with respect
63+
to parameters.
5364
"""
5465
assert power_coeff > 0.0, "The power coefficient should be greater than zero."
5566
fwhm_pc = np.power(fwhm / 2, power_coeff)
@@ -58,24 +69,22 @@ def fit_deriv(x, x_0, fwhm, value, power_coeff):
5869
denom = mod_x_pc + fwhm_pc
5970
denom_sq = np.power(denom, 2)
6071

61-
del_func_x = (
62-
-1.0 * num / denom_sq * (power_coeff * mod_x_pc / np.abs(x - x_0)) * np.sign(x - x_0)
63-
)
64-
del_func_x_0 = -del_func_x
65-
del_func_value = fwhm_pc / denom
72+
d_x = -1.0 * num / denom_sq * (power_coeff * mod_x_pc / np.abs(x - x_0)) * np.sign(x - x_0)
73+
d_x_0 = -d_x
74+
d_value = fwhm_pc / denom
6675

6776
pre_compute = 1.0 / 2.0 * power_coeff * fwhm_pc / (fwhm / 2)
68-
del_func_fwhm = 1.0 / denom_sq * (denom * (value * pre_compute) - num * pre_compute)
77+
d_fwhm = 1.0 / denom_sq * (denom * (value * pre_compute) - num * pre_compute)
6978

70-
del_func_p_coeff = (
79+
d_power_coeff = (
7180
1.0
7281
/ denom_sq
7382
* (
7483
denom * (value * np.log(fwhm / 2) * fwhm_pc)
7584
- num * (np.log(abs(x - x_0)) * mod_x_pc + np.log(fwhm / 2) * fwhm_pc)
7685
)
7786
)
78-
return [del_func_x, del_func_x_0, del_func_value, del_func_fwhm, del_func_p_coeff]
87+
return [d_x, d_x_0, d_value, d_fwhm, d_power_coeff]
7988

8089
def bounding_box(self, factor=25):
8190
"""Tuple defining the default ``bounding_box`` limits,
@@ -126,6 +135,17 @@ class SmoothBrokenPowerLaw(Fittable1DModel):
126135
break_freq: float
127136
break frequency
128137
138+
Notes
139+
-----
140+
Model formula (with :math:`N` for ``norm``, :math:`x_b` for
141+
``break_freq``, :math:`\\gamma_1` for ``gamma_low``,
142+
and :math:`\\gamma_2` for ``gamma_high``):
143+
144+
.. math::
145+
146+
f(x) = N \\cdot x^{-\\gamma_1}
147+
\\left( 1 + \\left( \\frac{x}{x_b} \\right)^2 \\right)^{\\frac{\\gamma_1 - \\gamma_2}{2}}
148+
129149
Returns
130150
-------
131151
model: astropy.modeling.Model
@@ -145,45 +165,103 @@ def _norm_validator(self, value):
145165

146166
@staticmethod
147167
def evaluate(x, norm, gamma_low, gamma_high, break_freq):
148-
norm_ = norm * x ** (-1 * gamma_low)
149-
if isinstance(norm_, Quantity):
150-
return_unit = norm_.unit
151-
norm = norm_.value
152-
else:
153-
return_unit = None
154-
155-
exp_factor = (gamma_low - gamma_high) / 2
156-
break_freq_invsq = 1.0 / np.power(break_freq, 2)
157-
f = (
158-
norm
159-
* np.power(x, -gamma_low)
160-
* np.power(1.0 + np.power(x, 2) * break_freq_invsq, exp_factor)
161-
)
162-
return Quantity(f, unit=return_unit, copy=False, subok=True)
168+
"""One dimensional smoothly broken power law model function."""
169+
# Pre-calculate `x/x_b`
170+
xx = x / break_freq
171+
172+
# Initialize the return value
173+
f = np.zeros_like(xx, subok=False)
174+
175+
# The quantity `t = (x / x_b)^(1 / 2)` can become quite
176+
# large. To avoid overflow errors we will start by calculating
177+
# its natural logarithm:
178+
logt = np.log(xx) / 2
179+
180+
# When `t >> 1` or `t << 1` we don't actually need to compute
181+
# the `t` value since the main formula (see docstring) can be
182+
# significantly simplified by neglecting `1` or `t`
183+
# respectively. In the following we will check whether `t` is
184+
# much greater, much smaller, or comparable to 1 by comparing
185+
# the `logt` value with an appropriate threshold.
186+
threshold = 30 # corresponding to exp(30) ~ 1e13
187+
i = logt > threshold
188+
if i.max():
189+
f[i] = norm * np.power(x, -gamma_low) * np.power(xx[i], (gamma_low - gamma_high))
190+
191+
i = logt < -threshold
192+
if i.max():
193+
f[i] = norm * np.power(x, -gamma_low)
194+
195+
i = np.abs(logt) <= threshold
196+
if i.max():
197+
# In this case the `t` value is "comparable" to 1, hence we
198+
# we will evaluate the whole formula.
199+
f[i] = (
200+
norm
201+
* np.power(x, -gamma_low)
202+
* np.power(1.0 + np.power(xx[i], 2), (gamma_low - gamma_high) / 2)
203+
)
204+
return f
163205

164206
@staticmethod
165207
def fit_deriv(x, norm, gamma_low, gamma_high, break_freq):
166-
exp_factor = (gamma_low - gamma_high) / 2
167-
x_g_low = np.power(x, -gamma_low)
168-
A = norm * x_g_low
169-
x_b_freq_sq = 1.0 + np.power(x / break_freq, 2)
170-
B = np.power(x_b_freq_sq, exp_factor)
171-
172-
del_func_x = B * norm * (-gamma_low * x_g_low / x) + A * B / x_b_freq_sq * (
173-
2 * np.power(x / break_freq, 2)
174-
)
175-
176-
del_func_norm = x_g_low * B
177-
178-
del_func_g_low = (
179-
B * norm * -1.0 * np.log(x) * x_g_low + A * (1.0 / 2.0) * np.log(x_b_freq_sq) * B
180-
)
208+
"""One dimensional smoothly broken power law derivative with respect
209+
to parameters.
210+
"""
211+
# Pre-calculate `x_b` and `x/x_b` and `logt` (see comments in
212+
# SmoothBrokenPowerLaw.evaluate)
213+
xx = x / break_freq
214+
logt = np.log(xx) / 2
215+
216+
# Initialize the return values
217+
f = np.zeros_like(xx)
218+
d_x = np.zeros_like(xx)
219+
d_norm = np.zeros_like(xx)
220+
d_gamma_low = np.zeros_like(xx)
221+
d_gamma_high = np.zeros_like(xx)
222+
d_break_freq = np.zeros_like(xx)
223+
224+
threshold = 30 # (see comments in SmoothBrokenPowerLaw.evaluate)
225+
i = logt > threshold
226+
if i.max():
227+
f[i] = norm * np.power(x, -gamma_low) * np.power(xx[i], (gamma_low - gamma_high))
228+
229+
d_x[i] = f[i] * -gamma_high / x
230+
d_norm[i] = f[i] / norm
231+
d_gamma_low[i] = f[i] * (-np.log(x) + np.log(xx[i]))
232+
d_gamma_high[i] = -f[i] * np.log(xx[i])
233+
d_break_freq[i] = f[i] * (gamma_high - gamma_low) / break_freq
234+
235+
i = logt < -threshold
236+
if i.max():
237+
f[i] = norm * np.power(x, -gamma_low)
238+
239+
d_x[i] = f[i] * -gamma_low / x
240+
d_norm[i] = f[i] / norm
241+
d_gamma_low[i] = -f[i] * np.log(x)
242+
d_gamma_high[i] = 0
243+
d_break_freq[i] = 0
244+
245+
i = np.abs(logt) <= threshold
246+
if i.max():
247+
# In this case the `t` value is "comparable" to 1, hence we
248+
# we will evaluate the whole formula.
249+
f[i] = (
250+
norm
251+
* np.power(x, -gamma_low)
252+
* np.power(1.0 + np.power(xx[i], 2), (gamma_low - gamma_high) / 2)
253+
)
254+
d_x[i] = f[i] * -gamma_low / x
255+
d_norm[i] = f[i] / norm
256+
d_gamma_low[i] = f[i] * (-np.log(x) + np.log(1.0 + np.power(xx[i], 2)) / 2)
257+
d_gamma_high[i] = -f[i] * np.log(1.0 + np.power(xx[i], 2)) / 2
258+
d_break_freq[i] = (
259+
f[i]
260+
* (np.power(x, 2) * (gamma_high - gamma_low))
261+
/ (break_freq * (np.power(break_freq, 2) + np.power(x, 2)))
262+
)
181263

182-
del_func_g_high = A * -1.0 / 2.0 * np.log(x_b_freq_sq) * B
183-
del_func_b_freq = (
184-
A * (exp_factor) * B / x_b_freq_sq * (np.pow(x, 2) * -2 / np.power(break_freq, 3))
185-
)
186-
return [del_func_x, del_func_norm, del_func_g_low, del_func_g_high, del_func_b_freq]
264+
return [d_x, d_norm, d_gamma_low, d_gamma_high, d_break_freq]
187265

188266
# NOTE:
189267
# In astropy 4.3 'Parameter' object has no attribute 'input_unit',

0 commit comments

Comments
 (0)