# -*- coding: utf-8 -*- """Routines for computing magnitudes. Planetary routines adapted from: https://arxiv.org/pdf/1808.01973.pdf Which links to: https://sourceforge.net/projects/planetary-magnitudes/ Which has directories with three successive versions of their magnitude computation, the most recent of which provides the files on which this Python code is based: Ap_Mag_V3.f90 Ap_Mag_Output_V3.txt Ap_Mag_Input_V3.txt * ``r`` planet’s distance from the Sun. * ``delta`` from Earth? * ``ph_ang`` illumination phase angle (degrees) """ from numpy import array, clip, exp, log10, nan, sin, where from .constants import RAD2DEG from .functions import angle_between, length_of from .naifcodes import _target_name # See "design/planet_tilts.py" in the Skyfield repository. _SATURN_POLE = array([0.08547883, 0.07323576, 0.99364475]) _SATURN_POLE_2D = _SATURN_POLE[:, None] _URANUS_POLE = array([-0.21199958, -0.94155916, -0.26176809]) _URANUS_POLE_2D = _URANUS_POLE[:, None] def planetary_magnitude(position): """Given the position of a planet, return its visual magnitude. >>> from skyfield.api import load >>> from skyfield.magnitudelib import planetary_magnitude >>> ts = load.timescale() >>> t = ts.utc(2020, 7, 31) >>> eph = load('de421.bsp') >>> astrometric = eph['earth'].at(t).observe(eph['jupiter barycenter']) >>> print('%.2f' % planetary_magnitude(astrometric)) -2.73 The formulae are from `Mallama and Hilton “Computing Apparent Planetary Magnitude for the Astronomical Almanac” (2018) `_. Two of the formulae have inherent limits: * Saturn’s magnitude is unknown and the function will return ``nan`` (the floating-point value “Not a Number”) if the “illumination phase angle” — the angle of the vertex observer-Saturn-Sun — exceeds 6.5°. * Neptune’s magnitude is unknown and will return ``nan`` if the illumination phase angle exceeds 1.9° and the position's date is before the year 2000. And one formula is not fully implemented (though contributions are welcome!): * Skyfield does not compute which features on Mars are facing the observer, which can introduce an error of ±0.06 magnitude. """ target = position.target function = _FUNCTIONS.get(target) if function is None: name = _target_name(target) raise ValueError('cannot compute the magnitude of target %s' % name) # Shamelessly treat the Sun as sitting at the Solar System Barycenter. sun_to_observer = position.center_barycentric.xyz.au observer_to_planet = position.xyz.au sun_to_planet = sun_to_observer + observer_to_planet r = length_of(sun_to_planet) delta = length_of(observer_to_planet) ph_ang = angle_between(sun_to_planet, observer_to_planet) * RAD2DEG if function is _saturn_magnitude: if len(sun_to_planet.shape) > 1: pole = _SATURN_POLE_2D else: pole = _SATURN_POLE a = angle_between(pole, sun_to_planet) sun_sub_lat = a * RAD2DEG - 90.0 a = angle_between(pole, observer_to_planet) observer_sub_lat = a * RAD2DEG - 90.0 return function(r, delta, ph_ang, sun_sub_lat, observer_sub_lat) if function is _uranus_magnitude: if len(sun_to_planet.shape) > 1: pole = _URANUS_POLE_2D else: pole = _URANUS_POLE a = angle_between(pole, sun_to_planet) sun_sub_lat = a * RAD2DEG - 90.0 a = angle_between(pole, observer_to_planet) observer_sub_lat = a * RAD2DEG - 90.0 return function(r, delta, ph_ang, sun_sub_lat, observer_sub_lat) if function is _neptune_magnitude: year = position.t.J return function(r, delta, ph_ang, year) return function(r, delta, ph_ang) def _mercury_magnitude(r, delta, ph_ang): distance_mag_factor = 5 * log10(r * delta) ph_ang_factor = ( 6.3280e-02 * ph_ang - 1.6336e-03 * ph_ang**2 + 3.3644e-05 * ph_ang**3 - 3.4265e-07 * ph_ang**4 + 1.6893e-09 * ph_ang**5 - 3.0334e-12 * ph_ang**6 ) return -0.613 + distance_mag_factor + ph_ang_factor def _venus_magnitude(r, delta, ph_ang): distance_mag_factor = 5 * log10(r * delta) condition = ph_ang < 163.7 a0 = where(condition, 0.0, 236.05828 + 4.384) a1 = where(condition, - 1.044E-03, - 2.81914E+00) a2 = where(condition, + 3.687E-04, + 8.39034E-03) a3 = where(condition, - 2.814E-06, 0.0) a4 = where(condition, + 8.938E-09, 0.0) ph_ang_factor = a4 for a in a3, a2, a1, a0: ph_ang_factor *= ph_ang ph_ang_factor += a return -4.384 + distance_mag_factor + ph_ang_factor def _earth_magnitude(r, delta, ph_ang): distance_mag_factor = 5 * log10(r * delta) ph_ang_factor = -1.060e-03 * ph_ang + 2.054e-04 * ph_ang**2 return -3.99 + distance_mag_factor + ph_ang_factor def _mars_magnitude(r, delta, ph_ang): r_mag_factor = 2.5 * log10(r * r) delta_mag_factor = 2.5 * log10(delta * delta) distance_mag_factor = r_mag_factor + delta_mag_factor geocentric_phase_angle_limit = 50.0 condition = ph_ang <= geocentric_phase_angle_limit a = where(condition, 2.267E-02, - 0.02573) b = where(condition, - 1.302E-04, 0.0003445) ph_ang_factor = a * ph_ang + b * ph_ang**2 # Compute the effective central meridian longitude # eff_CM = ( sub_earth_long + sub_sun_long ) / 2. # if ( abs ( sub_earth_long - sub_sun_long ) > 180. ): # Eff_CM = Eff_CM + 180. # if ( Eff_CM > 360. ): # Eff_CM = Eff_CM - 360. # ! Use Stirling interpolation to determine the magnitude correction # call Mars_Stirling ( 'R', eff_CM, mag_corr_rot ) # Convert the ecliptic longitude to Ls # Ls = h_ecl_long + Ls_offset # if ( Ls > 360. ) Ls = Ls - 360. # if ( Ls < 0. ) Ls = Ls + 360. # Use Stirling interpolation to determine the magnitude correction # call Mars_Stirling ( 'O', Ls, mag_corr_orb ) # Until effects from Mars rotation are written up: mag_corr_rot = 0.0 mag_corr_orb = 0.0 # Add factors to determine the apparent magnitude ap_mag = where(ph_ang <= geocentric_phase_angle_limit, -1.601, -0.367) ap_mag += distance_mag_factor + ph_ang_factor + mag_corr_rot + mag_corr_orb return ap_mag def _jupiter_magnitude(r, delta, ph_ang): distance_mag_factor = 5 * log10(r * delta) geocentric_phase_angle_limit = 12.0 ph_ang_pi = ph_ang / 180.0 ph_ang_factor = where( ph_ang <= geocentric_phase_angle_limit, (6.16E-04 * ph_ang - 3.7E-04) * ph_ang, -2.5 * log10( ((((- 1.876 * ph_ang_pi + 2.809) * ph_ang_pi - 0.062) * ph_ang_pi - 0.363) * ph_ang_pi - 1.507) * ph_ang_pi + 1.0 ), ) ap_mag = where( ph_ang <= geocentric_phase_angle_limit, -9.395 + distance_mag_factor + ph_ang_factor, -9.428 + distance_mag_factor + ph_ang_factor, ) return ap_mag def _saturn_magnitude(r, delta, ph_ang, sun_sub_lat, earth_sub_lat, rings=True): # Note that sun_sub_lat and earth_sub_lat should be saturnicentric # latitude, not saturnidetic. r_mag_factor = 2.5 * log10(r * r) delta_mag_factor = 2.5 * log10(delta * delta) distance_mag_factor = r_mag_factor + delta_mag_factor # Then take the square root of the product of the saturnicentric # latitude of the Sun and that of Earth but set to zero when the # signs are opposite product = sun_sub_lat * earth_sub_lat signs_same = product >= 0.0 square_root = product ** where(signs_same, 0.5, 0.0) # avoid sqrt(neg) sub_lat_geoc = where(signs_same, square_root, 0.0) # Compute the effect of phase angle and inclination geocentric_phase_angle_limit = 6.5 geocentric_inclination_limit = 27.0 is_within_geocentric_bounds = ( (ph_ang <= geocentric_phase_angle_limit) & (sub_lat_geoc <= geocentric_inclination_limit) ) ap_mag = where( is_within_geocentric_bounds, where( rings, # Use equation #10 for globe+rings and geocentric circumstances. -8.914 - 1.825 * sin(sub_lat_geoc / RAD2DEG) + 0.026 * ph_ang - 0.378 * sin(sub_lat_geoc / RAD2DEG) * exp(-2.25 * ph_ang), # Use equation #11 for globe-alone and geocentric circumstances -8.95 - 3.7e-4 * ph_ang + 6.16e-4 * ph_ang**2, ), where( (ph_ang > geocentric_phase_angle_limit) & _not(rings), # Use equation #12 for globe-alone beyond geocentric phase # angle limit -8.94 + 2.446e-4 * ph_ang + 2.672e-4 * ph_ang**2 - 1.506e-6 * ph_ang**3 +4.767e-9 * ph_ang**4, nan, ), ) ap_mag = ap_mag + distance_mag_factor return ap_mag def _not(b): return 1 - b # since ~b raises a DeprecationWarning in Python 3.13 def _uranus_magnitude(r, delta, ph_ang, sun_sub_lat_planetog, earth_sub_lat_planetog): distance_mag_factor = 5.0 * log10 (r * delta) sub_lat_planetog = (abs(sun_sub_lat_planetog) + abs(earth_sub_lat_planetog)) / 2.0 sub_lat_factor = -0.00084 * sub_lat_planetog geocentric_phase_angle_limit = 3.1 ap_mag = -7.110 + distance_mag_factor + sub_lat_factor ap_mag += where( ph_ang > geocentric_phase_angle_limit, (1.045e-4 * ph_ang + 6.587e-3) * ph_ang, 0.0, ) return ap_mag def _neptune_magnitude(r, delta, ph_ang, year): r_mag_factor = 2.5 * log10(r * r) delta_mag_factor = 2.5 * log10(delta * delta) distance_mag_factor = r_mag_factor + delta_mag_factor # Equation 16 compute the magnitude at unit distance as a function of time ap_mag = clip(-6.89 - 0.0054 * (year - 1980.0), -7.00, -6.89) ap_mag += distance_mag_factor geocentric_phase_angle_limit = 1.9 ap_mag = where( ph_ang > geocentric_phase_angle_limit, # Add phase angle factor from equation 17 # Check the year because equation 17 only pertains to t > 2000.0 where( year >= 2000.0, ap_mag + 7.944e-3 * ph_ang + 9.617e-5 * ph_ang**2, nan, ), # Otherwise leave the value unchanged. ap_mag, ) return ap_mag _FUNCTIONS = { 199: _mercury_magnitude, 299: _venus_magnitude, 399: _earth_magnitude, 499: _mars_magnitude, 599: _jupiter_magnitude, 699: _saturn_magnitude, 799: _uranus_magnitude, 899: _neptune_magnitude, # Some planets can be reasonably identified with their barycenter. 1: _mercury_magnitude, 2: _venus_magnitude, 4: _mars_magnitude, 5: _jupiter_magnitude, 6: _saturn_magnitude, 7: _uranus_magnitude, 8: _neptune_magnitude, }