@@ -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