2from collections
import namedtuple
3from scipy.interpolate
import CubicSpline
4from scipy.optimize
import leastsq, least_squares
7HG12FitResult = namedtuple(
9 [
"H",
"G12",
"H_err",
"G12_err",
"HG_cov",
"chi2dof",
"nobs"],
20alpha_12 = np.deg2rad([7.5, 30.0, 60, 90, 120, 150])
22phi_1_sp = [7.5e-1, 3.3486016e-1, 1.3410560e-1, 5.1104756e-2, 2.1465687e-2, 3.6396989e-3]
23phi_1_derivs = [-1.9098593, -9.1328612e-2]
25phi_2_sp = [9.25e-1, 6.2884169e-1, 3.1755495e-1, 1.2716367e-1, 2.2373903e-2, 1.6505689e-4]
26phi_2_derivs = [-5.7295780e-1, -8.6573138e-8]
28alpha_3 = np.deg2rad([0.0, 0.3, 1.0, 2.0, 4.0, 8.0, 12.0, 20.0, 30.0])
42phi_3_derivs = [-1.0630097, 0]
45phi_1 = CubicSpline(alpha_12, phi_1_sp, bc_type=((1, phi_1_derivs[0]), (1, phi_1_derivs[1])))
46phi_2 = CubicSpline(alpha_12, phi_2_sp, bc_type=((1, phi_2_derivs[0]), (1, phi_2_derivs[1])))
47phi_3 = CubicSpline(alpha_3, phi_3_sp, bc_type=((1, phi_3_derivs[0]), (1, phi_3_derivs[1])))
52 tan_ah = np.tan(phase / 2)
54 W = np.exp(-90.56 * tan_ah * tan_ah)
55 scale_sina = sin_a / (0.119 + 1.341 * sin_a - 0.754 * sin_a * sin_a)
57 phi_1_S = 1 - C[0] * scale_sina
58 phi_2_S = 1 - C[1] * scale_sina
60 phi_1_L = np.exp(-A[0] * np.power(tan_ah, B[0]))
61 phi_2_L = np.exp(-A[1] * np.power(tan_ah, B[1]))
63 phi_1 = W * phi_1_S + (1 - W) * phi_1_L
64 phi_2 = W * phi_2_S + (1 - W) * phi_2_L
65 return params[0] - 2.5 * np.log10((1 - params[1]) * phi_1 + (params[1]) * phi_2)
69 phi_1_ev =
phi_1(phase)
70 phi_2_ev =
phi_2(phase)
71 phi_3_ev =
phi_3(phase)
73 msk = phase < 7.5 * np.pi / 180
75 phi_1_ev[msk] = 1 - 6 * phase[msk] / np.pi
76 phi_2_ev[msk] = 1 - 9 * phase[msk] / (5 * np.pi)
78 phi_3_ev[phase > np.pi / 6] = 0
80 return params[0] - 2.5 * np.log10(
81 params[1] * phi_1_ev + params[2] * phi_2_ev + (1 - params[1] - params[2]) * phi_3_ev
87 G1 = +0.9529 * params[1] + 0.02162
88 G2 = -0.6125 * params[1] + 0.5572
90 G1 = +0.7527 * params[1] + 0.06164
91 G2 = -0.9612 * params[1] + 0.6270
97 G1 = 0 + params[1] * 0.84293649
98 G2 = 0.53513350 - params[1] * 0.53513350
103def chi2(params, mag, phase, mag_err, model):
104 pred = model(phase, params)
105 return (mag - pred) / mag_err
108def fit(mag, phase, sigma, model=HG12_model, params=[0.1]):
109 phase = np.deg2rad(phase)
111 sol = leastsq(chi2, [mag[0]] + params, (mag, phase, sigma, model), full_output=
True)
117 mag, magSigma, phaseAngle, tdist, rdist,
118 fixedG12=None, magSigmaFloor=0.0, nSigmaClip=None,
120 """Fit the HG12 phase curve model (Muinonen et al. 2010).
122 Fits absolute magnitude H (and optionally the slope parameter
123 G12) to apparent magnitude observations at known phase angles
130 magSigma : array_like
131 Magnitude uncertainties (1-sigma).
132 phaseAngle : array_like
133 Phase angles in degrees.
135 Topocentric (observer-target) distances in AU.
137 Heliocentric (sun-target) distances in AU.
138 fixedG12 : float or None, optional
139 If set, fix G12 to this value and only fit H.
140 If None (default), both H and G12 are fit.
141 magSigmaFloor : float, optional
142 Systematic error floor (mag) added in quadrature to
143 ``magSigma`` before fitting. Default is 0.0.
144 nSigmaClip : float or None, optional
145 If set, perform outlier rejection: an initial robust fit
146 (soft_l1 loss) followed by sigma clipping at this
147 threshold, then a final linear least-squares refit on the
148 clipped data. If None (default), no clipping is performed.
152 result : `HG12FitResult`
153 Named tuple with fields:
156 Best-fit absolute magnitude.
158 Best-fit (or fixed) slope parameter.
160 Uncertainty on H from the covariance matrix.
162 Uncertainty on G12 (NaN if ``fixedG12`` is set).
164 H-G12 covariance (NaN if ``fixedG12`` is set).
166 Reduced chi-squared of the fit.
168 Number of observations used (after clipping).
170 On failure, all float fields are NaN and ``nobs`` is 0.
178 (mag, magSigma, phaseAngle, tdist, rdist) = map(
179 np.asarray, (mag, magSigma, phaseAngle, tdist, rdist)
183 if magSigmaFloor > 0:
184 magSigma = np.sqrt(magSigma**2 + magSigmaFloor**2)
188 np.isfinite(mag) & np.isfinite(magSigma)
192 magSigma = magSigma[good]
193 phaseAngle = phaseAngle[good]
202 dmag = -5.0 * np.log10(tdist * rdist)
205 if fixedG12
is not None:
206 def model(phase, params):
207 return HG12_model(phase, [params[0], fixedG12])
214 phase_rad = np.deg2rad(phaseAngle)
216 [mag[0]] + ([]
if fixedG12
is not None else [0.1])
219 def residuals(params):
220 return (mag - model(phase_rad, params)) / magSigma
223 with warnings.catch_warnings():
224 warnings.simplefilter(
"ignore")
226 if nSigmaClip
is not None and nobsv > nparams + 1:
228 sol_robust = least_squares(
229 residuals, x0, loss=
'soft_l1', f_scale=1.0,
231 if not sol_robust.success:
235 resid = residuals(sol_robust.x)
236 keep = np.abs(resid) < nSigmaClip
238 magSigma = magSigma[keep]
239 phase_rad = phase_rad[keep]
246 def residuals(params):
248 (mag - model(phase_rad, params)) / magSigma
254 sol = least_squares(residuals, x0, loss=
'linear')
260 chi2_total = np.sum(sol.fun ** 2)
265 cov = np.linalg.inv(J.T @ J)
266 except np.linalg.LinAlgError:
270 H_err = np.sqrt(cov[0, 0])
272 if fixedG12
is not None:
278 G_err = np.sqrt(cov[1, 1])
282 H=H, G12=G, H_err=H_err, G12_err=G_err,
284 chi2dof=chi2_total / (nobsv - nparams),
294 Compute phase angle (Sun–Object–Observer) in degrees.
298 r_obj_sun : array, shape (3,) or (3, N)
299 Object position vector wrt Sun (Sun → object).
300 r_obs_sun : array, shape (3,) or (3, N)
301 Observer position vector wrt Sun (Sun → observer).
306 Phase angle(s) in degrees, in [0, 180].
308 r_obj_sun = np.asarray(r_obj_sun)
309 r_obs_sun = np.asarray(r_obs_sun)
313 v_obs = r_obs_sun - r_obj_sun
316 dot = np.sum(v_sun * v_obs, axis=0)
317 norm_sun = np.linalg.norm(v_sun, axis=0)
318 norm_obs = np.linalg.norm(v_obs, axis=0)
320 cosang = dot / (norm_sun * norm_obs)
321 cosang = np.clip(cosang, -1.0, 1.0)
323 return np.degrees(np.arccos(cosang))
328 Compute apparent V magnitude from the IAU H–G system.
333 Absolute magnitude (V-band).
337 Heliocentric distance in AU.
338 delta : float or ndarray
339 Observer distance (Δ) in AU.
340 phase_deg : float or ndarray
341 Phase angle in degrees.
343 a = np.radians(phase_deg) / 2.0
346 phi1 = np.exp(-3.33 * np.tan(a) ** 0.63)
347 phi2 = np.exp(-1.87 * np.tan(a) ** 1.22)
349 phi = (1 - G) * phi1 + G * phi2
351 return H + 5 * np.log10(r * delta) - 2.5 * np.log10(phi)
HG12star_model(phase, params)
chi2(params, mag, phase, mag_err, model)
hg_V_mag(H, G, r, delta, phase_deg)
HG1G2_model(phase, params)
phase_angle_deg(r_obj_sun, r_obs_sun)
fitHG12(mag, magSigma, phaseAngle, tdist, rdist, fixedG12=None, magSigmaFloor=0.0, nSigmaClip=None)
fit(mag, phase, sigma, model=HG12_model, params=[0.1])
HG12_model(phase, params)