Apparent Magnitude Validation

[1]:
import pandas as pd
import numpy as np
import astropy.units as u
from astroquery.jplhorizons import Horizons
from sorcha.modules.PPCalculateApparentMagnitudeInFilter import PPCalculateApparentMagnitudeInFilter
import matplotlib.pyplot as plt

To test the calculation of the apparent magnitude in the code, we can compare them to the apparent magnitudes calculated by JPL Horizons.

First, let’s get the JPL Horizons ephemeris for a test object. PPCalculateApparentMagnitudeInFilter uses sbpy’s photometry module to calculate phase functions, and sbpy’s unit tests use 24 Themis as a test object. We will do the same.

[2]:
obj = Horizons(id='Themis', id_type='name', location='I11',

               epochs={'start':'2021-01-01', 'stop':'2023-01-01',

                       'step':'1d'})

eph = obj.ephemerides(quantities='9,19,20,43')
jpl_eph = eph.to_pandas()
jpl_eph
[2]:
targetname datetime_str datetime_jd H G solar_presence lunar_presence V surfbright r r_rate delta delta_rate alpha_true PABLon PABLat
0 24 Themis (A853 GA) 2021-Jan-01 00:00 2459215.5 7.22 0.19 C 13.298 7.092 3.275241 1.868767 4.212453 -4.641369 4.5918 262.7496 -0.4684
1 24 Themis (A853 GA) 2021-Jan-02 00:00 2459216.5 7.22 0.19 C 13.307 7.102 3.276319 1.866016 4.209518 -4.922596 4.7832 263.0067 -0.4700
2 24 Themis (A853 GA) 2021-Jan-03 00:00 2459217.5 7.22 0.19 C 13.315 7.112 3.277396 1.863252 4.206423 -5.203712 4.9741 263.2635 -0.4716
3 24 Themis (A853 GA) 2021-Jan-04 00:00 2459218.5 7.22 0.19 C 13.324 7.122 3.278471 1.860476 4.203166 -5.484782 5.1645 263.5200 -0.4732
4 24 Themis (A853 GA) 2021-Jan-05 00:00 2459219.5 7.22 0.19 C 13.332 7.132 3.279545 1.857687 4.199749 -5.765845 5.3543 263.7760 -0.4748
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
726 24 Themis (A853 GA) 2022-Dec-28 00:00 2459941.5 7.22 0.19 C m 13.460 7.606 3.407801 -1.337567 3.548593 23.738198 16.0868 357.7423 -0.3756
727 24 Themis (A853 GA) 2022-Dec-29 00:00 2459942.5 7.22 0.19 C m 13.465 7.604 3.407028 -1.341288 3.562140 23.633204 16.0210 357.9249 -0.3731
728 24 Themis (A853 GA) 2022-Dec-30 00:00 2459943.5 7.22 0.19 C m 13.471 7.601 3.406252 -1.345002 3.575623 23.523879 15.9522 358.1090 -0.3707
729 24 Themis (A853 GA) 2022-Dec-31 00:00 2459944.5 7.22 0.19 C m 13.476 7.599 3.405474 -1.348708 3.589039 23.410433 15.8807 358.2945 -0.3683
730 24 Themis (A853 GA) 2023-Jan-01 00:00 2459945.5 7.22 0.19 C m 13.481 7.596 3.404694 -1.352405 3.602385 23.293037 15.8063 358.4816 -0.3659

731 rows × 16 columns

This needs to be turned into a form the function can understand. Values for G1, G2 and G12 are from Muinonen et al. (2010).

[3]:
observations_df = pd.DataFrame({'MJD':jpl_eph['datetime_jd'] - 2_400_000.5,
                                'H_filter': jpl_eph['H'],
                                'GS': jpl_eph['G'],
                                'G1': np.zeros(len(jpl_eph['G'])) + 0.62,
                                'G2': np.zeros(len(jpl_eph['G'])) + 0.14,
                                'G12': np.zeros(len(jpl_eph['G'])) + 0.68,
                                'JPL_mag': jpl_eph['V'],
                                'Range_LTC_km': jpl_eph['r'] * 1.495978707e8,
                                'Obj_Sun_LTC_km': jpl_eph['delta'] * 1.495978707e8,
                                'phase_deg': jpl_eph['alpha_true']})
[4]:
observations_df
[4]:
MJD H_filter GS G1 G2 G12 JPL_mag Range_LTC_km Obj_Sun_LTC_km phase_deg
0 59215.0 7.22 0.19 0.62 0.14 0.68 13.298 4.899690e+08 6.301739e+08 4.5918
1 59216.0 7.22 0.19 0.62 0.14 0.68 13.307 4.901304e+08 6.297350e+08 4.7832
2 59217.0 7.22 0.19 0.62 0.14 0.68 13.315 4.902915e+08 6.292719e+08 4.9741
3 59218.0 7.22 0.19 0.62 0.14 0.68 13.324 4.904523e+08 6.287847e+08 5.1645
4 59219.0 7.22 0.19 0.62 0.14 0.68 13.332 4.906130e+08 6.282735e+08 5.3543
... ... ... ... ... ... ... ... ... ... ...
726 59941.0 7.22 0.19 0.62 0.14 0.68 13.460 5.097998e+08 5.308620e+08 16.0868
727 59942.0 7.22 0.19 0.62 0.14 0.68 13.465 5.096841e+08 5.328886e+08 16.0210
728 59943.0 7.22 0.19 0.62 0.14 0.68 13.471 5.095680e+08 5.349056e+08 15.9522
729 59944.0 7.22 0.19 0.62 0.14 0.68 13.476 5.094517e+08 5.369125e+08 15.8807
730 59945.0 7.22 0.19 0.62 0.14 0.68 13.481 5.093350e+08 5.389091e+08 15.8063

731 rows × 10 columns

Now we calculate the magnitude using the various models in PPCalculateApparentMagnitudeInFilter.

[5]:
observations_df = PPCalculateApparentMagnitudeInFilter(observations_df.copy(), 'HG', 'r', 'HG_mag')
observations_df = PPCalculateApparentMagnitudeInFilter(observations_df.copy(), 'HG12', 'r', 'HG12_mag')
observations_df = PPCalculateApparentMagnitudeInFilter(observations_df.copy(), 'HG1G2', 'r', 'HG1G2_mag')
[6]:
observations_df
[6]:
MJD H_filter GS G1 G2 G12 JPL_mag Range_LTC_km Obj_Sun_LTC_km phase_deg HG_mag HG12_mag HG1G2_mag
0 59215.0 7.22 0.19 0.62 0.14 0.68 13.298 4.899690e+08 6.301739e+08 4.5918 13.311578 13.307267 13.296821
1 59216.0 7.22 0.19 0.62 0.14 0.68 13.307 4.901304e+08 6.297350e+08 4.7832 13.321366 13.316490 13.306087
2 59217.0 7.22 0.19 0.62 0.14 0.68 13.315 4.902915e+08 6.292719e+08 4.9741 13.330778 13.325410 13.315072
3 59218.0 7.22 0.19 0.62 0.14 0.68 13.324 4.904523e+08 6.287847e+08 5.1645 13.339831 13.334037 13.323783
4 59219.0 7.22 0.19 0.62 0.14 0.68 13.332 4.906130e+08 6.282735e+08 5.3543 13.348535 13.342378 13.332227
... ... ... ... ... ... ... ... ... ... ... ... ... ...
726 59941.0 7.22 0.19 0.62 0.14 0.68 13.460 5.097998e+08 5.308620e+08 16.0868 13.461646 13.498557 13.502578
727 59942.0 7.22 0.19 0.62 0.14 0.68 13.465 5.096841e+08 5.328886e+08 16.0210 13.467347 13.503931 13.507875
728 59943.0 7.22 0.19 0.62 0.14 0.68 13.471 5.095680e+08 5.349056e+08 15.9522 13.472879 13.509119 13.512982
729 59944.0 7.22 0.19 0.62 0.14 0.68 13.476 5.094517e+08 5.369125e+08 15.8807 13.478251 13.514132 13.517910
730 59945.0 7.22 0.19 0.62 0.14 0.68 13.481 5.093350e+08 5.389091e+08 15.8063 13.483454 13.518962 13.522651

731 rows × 13 columns

Now we can plot the magnitudes and compare them.

Note that we do not expect any of the calculated magnitudes to match JPL Horizons exactly. JPL Horizons uses the IAU simplification of the HG model to calculate apparent magnitude, while sbpy uses the original HG formulation from Bowell et al. (1989). However, they should all be a close match.

[7]:
fig, ax = plt.subplots(figsize=(10,8))
ax.plot(observations_df["MJD"] - 59000, observations_df["JPL_mag"], linestyle="", marker="x", label="JPL")
ax.plot(observations_df["MJD"] - 59000, observations_df["HG_mag"], label="HG")
ax.plot(observations_df["MJD"] - 59000, observations_df["HG12_mag"], label="HG12")
ax.plot(observations_df["MJD"] - 59000, observations_df["HG1G2_mag"], label="HG1G2")
ax.legend()
ax.set_xlabel("MJD - 59000")
ax.set_ylabel("apparent magnitude")
ax.set_ylim((11.75, 14))
plt.show()
../_images/notebooks_demo_ApparentMagnitudeValidation_11_0.png

To test the linear phase function model, we simply define a slope. We will use the same values for Themis and arbitrarily choose S to be 0.04.

[8]:
H = observations_df['H_filter'].values
alpha = observations_df['phase_deg'].values
r = observations_df['Range_LTC_km'].values / 1.495978707e8
delta = observations_df['Obj_Sun_LTC_km'].values / 1.495978707e8
S = np.zeros(len(H)) + 0.04

observations_df["S"] = S

The expected apparent magnitude will thus take the form:

\(m = H + 5 \log_{10}(\Delta) + 5 \log_{10}(r) + S\alpha\)

[9]:
linear_mag_calc = 5. * np.log10(delta) + 5. * np.log10(r) + H + (S * alpha)

Calculating using the linear phase function model in PPCalculateApparentMagnitudeInFilter…

[10]:
observations_df = PPCalculateApparentMagnitudeInFilter(observations_df.copy(), 'linear', 'r', 'linear_mag')
[11]:
fig, ax = plt.subplots(figsize=(10,8))
ax.plot(observations_df["MJD"] - 59000, linear_mag_calc, linestyle="", marker="x", label="calculated")
ax.plot(observations_df["MJD"] - 59000, observations_df["linear_mag"], label="function")
ax.legend()
ax.set_xlabel("MJD - 59000")
ax.set_ylabel("apparent magnitude")
ax.set_ylim((11.75, 14))
plt.show()
../_images/notebooks_demo_ApparentMagnitudeValidation_18_0.png

Note that there is some offset (20-40 mmag) between the sorcha predicted magnitudes using the HG/HG12/HG1G2 phase curves and the JPL magnitudes. This is not a bug in sorcha, but rather something in JPL (that is presumably using a more sophisticated model). To show that this is the case, we’ll test these values out using an independent implementation of the three phase curve models.

[12]:
from scipy.interpolate import CubicSpline

A = [3.332, 1.862]
B = [0.631, 1.218]
C = [0.986, 0.238]

alpha_12 = np.deg2rad([7.5, 30., 60, 90, 120, 150])

phi_1_sp = [7.5e-1, 3.3486016e-1, 1.3410560e-1, 5.1104756e-2, 2.1465687e-2, 3.6396989e-3]
phi_1_derivs = [-1.9098593, -9.1328612e-2]

phi_2_sp = [9.25e-1, 6.2884169e-1, 3.1755495e-1, 1.2716367e-1, 2.2373903e-2, 1.6505689e-4]
phi_2_derivs = [-5.7295780e-1, -8.6573138e-8]

alpha_3 = np.deg2rad([0.0, 0.3, 1., 2., 4., 8., 12., 20., 30.])

phi_3_sp = [1., 8.3381185e-1, 5.7735424e-1, 4.2144772e-1, 2.3174230e-1, 1.0348178e-1, 6.1733473e-2, 1.6107006e-2, 0.]
phi_3_derivs = [-1.0630097, 0]


phi_1 = CubicSpline(alpha_12, phi_1_sp, bc_type=((1,phi_1_derivs[0]),(1,phi_1_derivs[1])))
phi_2 = CubicSpline(alpha_12, phi_2_sp, bc_type=((1,phi_2_derivs[0]),(1,phi_2_derivs[1])))
phi_3 = CubicSpline(alpha_3, phi_3_sp, bc_type=((1,phi_3_derivs[0]),(1,phi_3_derivs[1])))

def HG_model(phase, params):
    sin_a = np.sin(phase)
    tan_ah = np.tan(phase/2)

    W = np.exp(-90.56 * tan_ah * tan_ah)
    scale_sina = sin_a/(0.119 + 1.341*sin_a - 0.754*sin_a*sin_a)

    phi_1_S = 1 - C[0] * scale_sina
    phi_2_S = 1 - C[1] * scale_sina

    phi_1_L = np.exp(-A[0] * np.power(tan_ah, B[0]))
    phi_2_L = np.exp(-A[1] * np.power(tan_ah, B[1]))

    phi_1 = W * phi_1_S + (1-W) * phi_1_L
    phi_2 = W * phi_2_S + (1-W) * phi_2_L
    return params[0] - 2.5*np.log10((1-params[1])* phi_1 + (params[1]) * phi_2)


def HG1G2_model(phase, params):

    tan_ah = np.tan(phase/2)

    phi_1_ev = phi_1(phase)
    phi_2_ev = phi_2(phase)
    phi_3_ev = phi_3(phase)

    msk = phase < 7.5 * np.pi/180

    phi_1_ev[msk] = 1-6*phase[msk]/np.pi
    phi_2_ev[msk] = 1- 9 * phase[msk]/(5*np.pi)

    phi_3_ev[phase > np.pi/6] = 0


    return params[0] - 2.5 * np.log10(params[1] * phi_1_ev + params[2] * phi_2_ev + (1-params[1]-params[2]) * phi_3_ev)

def HG12_model(phase, params):
    if params[1] >= 0.2:
        G1 = +0.9529*params[1] + 0.02162
        G2 = -0.6125*params[1] + 0.5572
    else:
        G1 = +0.7527*params[1] + 0.06164
        G2 = -0.9612*params[1] + 0.6270

    return HG1G2_model(phase, [params[0], G1, G2])

def HG12star_model(phase, params):
    G1 = 0 + params[1] * 0.84293649
    G2 = 0.53513350 - params[1] * 0.53513350

    return HG1G2_model(phase, [params[0], G1, G2])
[13]:
observations_df['independent_HG'] = observations_df['H_filter'] + 5 * np.log10(observations_df['Range_LTC_km'] * observations_df['Obj_Sun_LTC_km']/(1.495978707e8**2)) + HG_model(observations_df['phase_deg'] * np.pi/180, [0,0.19])
observations_df['independent_HG12'] = observations_df['H_filter'] + 5 * np.log10(observations_df['Range_LTC_km'] * observations_df['Obj_Sun_LTC_km']/(1.495978707e8**2)) + HG12_model(observations_df['phase_deg'] * np.pi/180, [0,0.68])
observations_df['independent_HG12star'] = observations_df['H_filter'] + 5 * np.log10(observations_df['Range_LTC_km'] * observations_df['Obj_Sun_LTC_km']/(1.495978707e8**2)) + HG12star_model(observations_df['phase_deg'] * np.pi/180, [0,0.68])
observations_df['independent_HG1G2'] = observations_df['H_filter'] + 5 * np.log10(observations_df['Range_LTC_km'] * observations_df['Obj_Sun_LTC_km']/(1.495978707e8**2)) + HG1G2_model(observations_df['phase_deg'] * np.pi/180, [0,0.62, 0.14])
[14]:
plt.hist(observations_df['JPL_mag'] - observations_df['HG_mag'], bins=30, histtype='step', label='HG')
plt.hist(observations_df['JPL_mag'] - observations_df['HG12_mag'], bins=30, histtype='step', label='HG12 (Penttila)')
plt.hist(observations_df['JPL_mag'] - observations_df['HG1G2_mag'], bins=30, histtype='step', label='HG1G2')
plt.hist(observations_df['JPL_mag'] - observations_df['independent_HG12'], bins=30, histtype='step', label='HG12 (Muinonen)')

plt.legend(ncol=2)
plt.xlabel('JPL - sorcha (mag)')
plt.show()


plt.hist(observations_df['HG12_mag'] - observations_df['independent_HG12star'], bins=30, histtype='step', label='HG12')
plt.hist(observations_df['HG_mag'] - observations_df['independent_HG'], bins=30, histtype='step', label='HG')
plt.hist(observations_df['HG1G2_mag'] - observations_df['independent_HG1G2'], bins=30, histtype='step', label='HG1G2 ')
plt.xlabel('Independent - sorcha (mag)')

plt.legend()
plt.tight_layout()
plt.show()
../_images/notebooks_demo_ApparentMagnitudeValidation_22_0.png
../_images/notebooks_demo_ApparentMagnitudeValidation_22_1.png

So the differences are \(< 10^{-9}\) between the two implementations.

Let’s look at this plot again with a logarithmic y-axis so you can see the residuals clearer

[15]:
plt.yscale('log',base=10)
plt.hist(observations_df['HG12_mag'] - observations_df['independent_HG12star'], bins=30, histtype='step', label='HG12')
plt.hist(observations_df['HG_mag'] - observations_df['independent_HG'], bins=30, histtype='step', label='HG')
plt.hist(observations_df['HG1G2_mag'] - observations_df['independent_HG1G2'], bins=30, histtype='step', label='HG1G2 ')
plt.xlabel('Independent - sorcha (mag)')
plt.legend()
plt.tight_layout()
plt.show()
../_images/notebooks_demo_ApparentMagnitudeValidation_25_0.png