gotelem/py/pytelem/optimus.py
2023-09-19 14:17:22 -05:00

617 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# hyperspeed forward and backwards analytics engine
import numpy as np
from numba import jit
# import jax.numpy as np
# TODO: define 3d vector space - x,y,z oriented around car/world?
# solvers should not care about position but we should be able to convert
# transforms between car/world spaces.
# let's define x as the forward-backward axis (with forward being positive)
# y as the lateral axis, with right being positive
# and z being the vertical, with up being positive.
# for simplicities sake, the car only rotates on the y-axis (up and down hills).
# z rotation can be determined from distance along the route.
# some data (wind) is only a 2d vector at a given time point.
def fsolve_discrete():
"""Forward compute a route segment."""
def dist_to_pos(dist: float):
"""convert a distance along the race path to a position in 3d space"""
# All units are BASE SI (no prefix except for kilogram)
ATM_MOLAR_MASS = 0.0289644 # kg/mol
STANDARD_TEMP = 288.15 # K
STANDARD_PRES = 101325.0 # Pa
AIR_GAS_CONSTANT = 8.31432 # N*m/s^2
EARTH_TEMP_LAPSE = -0.0065
EARTH_GRAVITY = 9.80665 # m/s^2
EARTH_RADIUS = 6378140.0 # m
EARTH_AXIS_INCLINATION = 23.45 # degrees
# FIXME: use named constants here
@jit
def get_pressure_el(
el: float,
Ps=STANDARD_PRES,
Ts: float = STANDARD_TEMP,
T_lapse: float = EARTH_TEMP_LAPSE,
):
"""Gets the pressure at a point given eleveation - assumes
standard pressure,temperature, gas constants, etc"""
return Ps * (Ts / (Ts + T_lapse * el)) ** (
(ATM_MOLAR_MASS * EARTH_GRAVITY) / (AIR_GAS_CONSTANT / T_lapse)
)
@jit
def estimate_temp(el: float, Ts: float = STANDARD_TEMP, T_lapse=EARTH_TEMP_LAPSE):
return Ts + el * T_lapse
def make_cubic(a, b, c, d):
"""returns a simple cubic function"""
def poly(x):
return a + b * x + c * (x ** 2) + (x ** 3) / d
return jit(poly)
@jit
def get_radiation_direct(yday, altitude_deg):
"""Calculate the direct radiation at a given day of the year given the angle of the sun
from the horizon."""
flux = 1160 + (75 * np.sin(2 * np.pi / 365 * (yday - 275)))
optical_depth = 0.174 + (0.035 * np.sin(2 * np.pi / 365 * (yday - 100)))
air_mass_ratio = 1 / np.sin(np.radians(altitude_deg))
# from Masters, p. 412
return flux * np.exp(-1 * optical_depth * air_mass_ratio) * (altitude_deg > 0)
# We start by defining MANY constants.
# to skip this, Ctrl-F to END COEFF
# START COEFF
# heliocentric longitude, latitude, radius (section 3.2) coefficients
# heliocentric longitude coefficients
L0_TABLE = np.array(
[
[175347046.0, 0.0, 0.0],
[3341656.0, 4.6692568, 6283.07585],
[34894.0, 4.6261, 12566.1517],
[3497.0, 2.7441, 5753.3849],
[3418.0, 2.8289, 3.5231],
[3136.0, 3.6277, 77713.7715],
[2676.0, 4.4181, 7860.4194],
[2343.0, 6.1352, 3930.2097],
[1324.0, 0.7425, 11506.7698],
[1273.0, 2.0371, 529.691],
[1199.0, 1.1096, 1577.3435],
[990.0, 5.233, 5884.927],
[902.0, 2.045, 26.298],
[857.0, 3.508, 398.149],
[780.0, 1.179, 5223.694],
[753.0, 2.533, 5507.553],
[505.0, 4.583, 18849.228],
[492.0, 4.205, 775.523],
[357.0, 2.92, 0.067],
[317.0, 5.849, 11790.629],
[284.0, 1.899, 796.298],
[271.0, 0.315, 10977.079],
[243.0, 0.345, 5486.778],
[206.0, 4.806, 2544.314],
[205.0, 1.869, 5573.143],
[202.0, 2.458, 6069.777],
[156.0, 0.833, 213.299],
[132.0, 3.411, 2942.463],
[126.0, 1.083, 20.775],
[115.0, 0.645, 0.98],
[103.0, 0.636, 4694.003],
[102.0, 0.976, 15720.839],
[102.0, 4.267, 7.114],
[99.0, 6.21, 2146.17],
[98.0, 0.68, 155.42],
[86.0, 5.98, 161000.69],
[85.0, 1.3, 6275.96],
[85.0, 3.67, 71430.7],
[80.0, 1.81, 17260.15],
[79.0, 3.04, 12036.46],
[75.0, 1.76, 5088.63],
[74.0, 3.5, 3154.69],
[74.0, 4.68, 801.82],
[70.0, 0.83, 9437.76],
[62.0, 3.98, 8827.39],
[61.0, 1.82, 7084.9],
[57.0, 2.78, 6286.6],
[56.0, 4.39, 14143.5],
[56.0, 3.47, 6279.55],
[52.0, 0.19, 12139.55],
[52.0, 1.33, 1748.02],
[51.0, 0.28, 5856.48],
[49.0, 0.49, 1194.45],
[41.0, 5.37, 8429.24],
[41.0, 2.4, 19651.05],
[39.0, 6.17, 10447.39],
[37.0, 6.04, 10213.29],
[37.0, 2.57, 1059.38],
[36.0, 1.71, 2352.87],
[36.0, 1.78, 6812.77],
[33.0, 0.59, 17789.85],
[30.0, 0.44, 83996.85],
[30.0, 2.74, 1349.87],
[25.0, 3.16, 4690.48],
]
)
L1_TABLE = np.array(
[
[628331966747.0, 0.0, 0.0],
[206059.0, 2.678235, 6283.07585],
[4303.0, 2.6351, 12566.1517],
[425.0, 1.59, 3.523],
[119.0, 5.796, 26.298],
[109.0, 2.966, 1577.344],
[93.0, 2.59, 18849.23],
[72.0, 1.14, 529.69],
[68.0, 1.87, 398.15],
[67.0, 4.41, 5507.55],
[59.0, 2.89, 5223.69],
[56.0, 2.17, 155.42],
[45.0, 0.4, 796.3],
[36.0, 0.47, 775.52],
[29.0, 2.65, 7.11],
[21.0, 5.34, 0.98],
[19.0, 1.85, 5486.78],
[19.0, 4.97, 213.3],
[17.0, 2.99, 6275.96],
[16.0, 0.03, 2544.31],
[16.0, 1.43, 2146.17],
[15.0, 1.21, 10977.08],
[12.0, 2.83, 1748.02],
[12.0, 3.26, 5088.63],
[12.0, 5.27, 1194.45],
[12.0, 2.08, 4694.0],
[11.0, 0.77, 553.57],
[10.0, 1.3, 6286.6],
[10.0, 4.24, 1349.87],
[9.0, 2.7, 242.73],
[9.0, 5.64, 951.72],
[8.0, 5.3, 2352.87],
[6.0, 2.65, 9437.76],
[6.0, 4.67, 4690.48],
]
)
L2_TABLE = np.array(
[
[52919.0, 0.0, 0.0],
[8720.0, 1.0721, 6283.0758],
[309.0, 0.867, 12566.152],
[27.0, 0.05, 3.52],
[16.0, 5.19, 26.3],
[16.0, 3.68, 155.42],
[10.0, 0.76, 18849.23],
[9.0, 2.06, 77713.77],
[7.0, 0.83, 775.52],
[5.0, 4.66, 1577.34],
[4.0, 1.03, 7.11],
[4.0, 3.44, 5573.14],
[3.0, 5.14, 796.3],
[3.0, 6.05, 5507.55],
[3.0, 1.19, 242.73],
[3.0, 6.12, 529.69],
[3.0, 0.31, 398.15],
[3.0, 2.28, 553.57],
[2.0, 4.38, 5223.69],
[2.0, 3.75, 0.98],
]
)
L3_TABLE = np.array(
[
[289.0, 5.844, 6283.076],
[35.0, 0.0, 0.0],
[17.0, 5.49, 12566.15],
[3.0, 5.2, 155.42],
[1.0, 4.72, 3.52],
[1.0, 5.3, 18849.23],
[1.0, 5.97, 242.73],
]
)
L4_TABLE = np.array([[114.0, 3.142, 0.0], [8.0, 4.13, 6283.08], [1.0, 3.84, 12566.15]])
L5_TABLE = np.array([[1.0, 3.14, 0.0]])
HELIO_L = [L0_TABLE, L1_TABLE, L2_TABLE, L3_TABLE, L4_TABLE, L5_TABLE]
# heliocentric latitude coefficients
B0_TABLE = np.array(
[
[280.0, 3.199, 84334.662],
[102.0, 5.422, 5507.553],
[80.0, 3.88, 5223.69],
[44.0, 3.7, 2352.87],
[32.0, 4.0, 1577.34],
]
)
B1_TABLE = np.array([[9.0, 3.9, 5507.55], [6.0, 1.73, 5223.69]])
HELIO_B = [B0_TABLE, B1_TABLE]
# heliocentric radius coefficients
R0_TABLE = np.array(
[
[100013989.0, 0.0, 0.0],
[1670700.0, 3.0984635, 6283.07585],
[13956.0, 3.05525, 12566.1517],
[3084.0, 5.1985, 77713.7715],
[1628.0, 1.1739, 5753.3849],
[1576.0, 2.8469, 7860.4194],
[925.0, 5.453, 11506.77],
[542.0, 4.564, 3930.21],
[472.0, 3.661, 5884.927],
[346.0, 0.964, 5507.553],
[329.0, 5.9, 5223.694],
[307.0, 0.299, 5573.143],
[243.0, 4.273, 11790.629],
[212.0, 5.847, 1577.344],
[186.0, 5.022, 10977.079],
[175.0, 3.012, 18849.228],
[110.0, 5.055, 5486.778],
[98.0, 0.89, 6069.78],
[86.0, 5.69, 15720.84],
[86.0, 1.27, 161000.69],
[65.0, 0.27, 17260.15],
[63.0, 0.92, 529.69],
[57.0, 2.01, 83996.85],
[56.0, 5.24, 71430.7],
[49.0, 3.25, 2544.31],
[47.0, 2.58, 775.52],
[45.0, 5.54, 9437.76],
[43.0, 6.01, 6275.96],
[39.0, 5.36, 4694.0],
[38.0, 2.39, 8827.39],
[37.0, 0.83, 19651.05],
[37.0, 4.9, 12139.55],
[36.0, 1.67, 12036.46],
[35.0, 1.84, 2942.46],
[33.0, 0.24, 7084.9],
[32.0, 0.18, 5088.63],
[32.0, 1.78, 398.15],
[28.0, 1.21, 6286.6],
[28.0, 1.9, 6279.55],
[26.0, 4.59, 10447.39],
]
)
R1_TABLE = np.array(
[
[103019.0, 1.10749, 6283.07585],
[1721.0, 1.0644, 12566.1517],
[702.0, 3.142, 0.0],
[32.0, 1.02, 18849.23],
[31.0, 2.84, 5507.55],
[25.0, 1.32, 5223.69],
[18.0, 1.42, 1577.34],
[10.0, 5.91, 10977.08],
[9.0, 1.42, 6275.96],
[9.0, 0.27, 5486.78],
]
)
R2_TABLE = np.array(
[
[4359.0, 5.7846, 6283.0758],
[124.0, 5.579, 12566.152],
[12.0, 3.14, 0.0],
[9.0, 3.63, 77713.77],
[6.0, 1.87, 5573.14],
[3.0, 5.47, 18849.23],
]
)
R3_TABLE = np.array([[145.0, 4.273, 6283.076], [7.0, 3.92, 12566.15]])
R4_TABLE = np.array([[4.0, 2.56, 6283.08]])
HELIO_R = [R0_TABLE, R1_TABLE, R2_TABLE, R3_TABLE, R4_TABLE]
# longitude and obliquity nutation coefficients
NUTATION_ABCD_ARRAY = np.array(
[
[-171996, -174.2, 92025, 8.9],
[-13187, -1.6, 5736, -3.1],
[-2274, -0.2, 977, -0.5],
[2062, 0.2, -895, 0.5],
[1426, -3.4, 54, -0.1],
[712, 0.1, -7, 0],
[-517, 1.2, 224, -0.6],
[-386, -0.4, 200, 0],
[-301, 0, 129, -0.1],
[217, -0.5, -95, 0.3],
[-158, 0, 0, 0],
[129, 0.1, -70, 0],
[123, 0, -53, 0],
[63, 0, 0, 0],
[63, 0.1, -33, 0],
[-59, 0, 26, 0],
[-58, -0.1, 32, 0],
[-51, 0, 27, 0],
[48, 0, 0, 0],
[46, 0, -24, 0],
[-38, 0, 16, 0],
[-31, 0, 13, 0],
[29, 0, 0, 0],
[29, 0, -12, 0],
[26, 0, 0, 0],
[-22, 0, 0, 0],
[21, 0, -10, 0],
[17, -0.1, 0, 0],
[16, 0, -8, 0],
[-16, 0.1, 7, 0],
[-15, 0, 9, 0],
[-13, 0, 7, 0],
[-12, 0, 6, 0],
[11, 0, 0, 0],
[-10, 0, 5, 0],
[-8, 0, 3, 0],
[7, 0, -3, 0],
[-7, 0, 0, 0],
[-7, 0, 3, 0],
[-7, 0, 3, 0],
[6, 0, 0, 0],
[6, 0, -3, 0],
[6, 0, -3, 0],
[-6, 0, 3, 0],
[-6, 0, 3, 0],
[5, 0, 0, 0],
[-5, 0, 3, 0],
[-5, 0, 3, 0],
[-5, 0, 3, 0],
[4, 0, 0, 0],
[4, 0, 0, 0],
[4, 0, 0, 0],
[-4, 0, 0, 0],
[-4, 0, 0, 0],
[-4, 0, 0, 0],
[3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
[-3, 0, 0, 0],
]
)
NUTATION_YTERM_ARRAY = np.array(
[
[0, 0, 0, 0, 1],
[-2, 0, 0, 2, 2],
[0, 0, 0, 2, 2],
[0, 0, 0, 0, 2],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[-2, 1, 0, 2, 2],
[0, 0, 0, 2, 1],
[0, 0, 1, 2, 2],
[-2, -1, 0, 2, 2],
[-2, 0, 1, 0, 0],
[-2, 0, 0, 2, 1],
[0, 0, -1, 2, 2],
[2, 0, 0, 0, 0],
[0, 0, 1, 0, 1],
[2, 0, -1, 2, 2],
[0, 0, -1, 0, 1],
[0, 0, 1, 2, 1],
[-2, 0, 2, 0, 0],
[0, 0, -2, 2, 1],
[2, 0, 0, 2, 2],
[0, 0, 2, 2, 2],
[0, 0, 2, 0, 0],
[-2, 0, 1, 2, 2],
[0, 0, 0, 2, 0],
[-2, 0, 0, 2, 0],
[0, 0, -1, 2, 1],
[0, 2, 0, 0, 0],
[2, 0, -1, 0, 1],
[-2, 2, 0, 2, 2],
[0, 1, 0, 0, 1],
[-2, 0, 1, 0, 1],
[0, -1, 0, 0, 1],
[0, 0, 2, -2, 0],
[2, 0, -1, 2, 1],
[2, 0, 1, 2, 2],
[0, 1, 0, 2, 2],
[-2, 1, 1, 0, 0],
[0, -1, 0, 2, 2],
[2, 0, 0, 2, 1],
[2, 0, 1, 0, 0],
[-2, 0, 2, 2, 2],
[-2, 0, 1, 2, 1],
[2, 0, -2, 0, 1],
[2, 0, 0, 0, 1],
[0, -1, 1, 0, 0],
[-2, -1, 0, 2, 1],
[-2, 0, 0, 0, 1],
[0, 0, 2, 2, 1],
[-2, 0, 2, 0, 1],
[-2, 1, 0, 2, 1],
[0, 0, 1, -2, 0],
[-1, 0, 1, 0, 0],
[-2, 1, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 0, 1, 2, 0],
[0, 0, -2, 2, 2],
[-1, -1, 1, 0, 0],
[0, 1, 1, 0, 0],
[0, -1, 1, 2, 2],
[2, -1, -1, 2, 2],
[0, 0, 3, 2, 2],
[2, -1, 0, 2, 2],
]
)
# END COEFF
# now, we write to the actual code
DELTA_T = 67
@jit
def helio_vector(vec, jme):
"""This function calculates equation 9 across the vector"""
return np.sum(
vec[:, 0] * np.cos(vec[:, 1] + vec[:, 2] * jme[..., np.newaxis]), axis=-1
)
def solar_position(timestamp, latitude, longitude, elevation):
"""Calculate the position of the sun at a given location and time.
Args:
timestamp (array-like): The timestamp(s) at each point.
latitude (array-like): The latitude(s) of each point.
longitude (array-like): The longitude(s) of each point.
elevation (array-like): The elevation of each point.
Returns:
ndarray: An array containing the altitude and azimuth for each point.
"""
jd = timestamp / 86400.0 + 2440587.5
jc = (jd - 2451545) / 36525
jde = jd + DELTA_T / 86400.0
jce = (jde - 2451545) / 36525
jm = jc / 10
jme = jce / 10
# todo: make more elegant?
# TODO: vectorize later? it's kinda complex
# heliocentric longitude
l_rad = np.zeros_like(timestamp)
for idx, vec in enumerate(HELIO_L):
l_rad = l_rad + helio_vector(vec, jme) * jme ** idx
l_rad = l_rad / 10e8
l_deg = np.rad2deg(l_rad) % 360
# heliocentric latitude
b_rad = np.zeros_like(timestamp)
for idx, vec in enumerate(HELIO_B):
b_rad = b_rad + helio_vector(vec, jme) * jme ** idx
b_rad = b_rad / 10e8
b_deg = np.rad2deg(b_rad) % 360
# heliocentric radius
r_rad = np.zeros_like(timestamp)
for idx, vec in enumerate(HELIO_R):
r_rad = r_rad + helio_vector(vec, jme) * jme ** idx
r_rad = r_rad / 10e8
r_deg = np.rad2deg(r_rad) % 360
theta = (l_deg + 180) % 360
beta = -1 * b_deg
def cubic_poly(a, b, c, d):
return a + b * jce + c * jce ** 2 + (jce ** 3) / d
X0 = cubic_poly(297.85036, 445267.111480, -0.0019142, 189474)
X1 = cubic_poly(357.52772, 35999.050340, -0.0001603, -300000)
X2 = cubic_poly(134.96298, 477198.867398, 0.0086972, 56250)
X3 = cubic_poly(93.27191, 483202.017538, -0.0036825, 327270)
X4 = cubic_poly(125.04452, 1934.136261, 0.0020708, 450000)
X = np.vstack([X0, X1, X2, X3, X4]).T
nut = NUTATION_ABCD_ARRAY
# TODO: these are gross - use loops instead of broadcasting?
# FIXME: use guvectorize, treat jce as a scalar.
d_psi = (nut[:, 0] + jce[..., np.newaxis] * nut[:, 1]) * np.sin(
np.sum(X[:, np.newaxis, :] * NUTATION_YTERM_ARRAY[np.newaxis, ...], axis=2)
)
d_psi = np.sum(d_psi, axis=-1) / 36, 000, 000
d_epsilon = (nut[:, 2] + jce[..., np.newaxis] * nut[:, 3]) * np.cos(
np.sum(X[:, np.newaxis, :] * NUTATION_YTERM_ARRAY[np.newaxis, ...], axis=2)
)
d_epsilon = np.sum(d_epsilon, axis=-1) / 36, 000, 000
u = jme[:, np.newaxis] / 10 * np.arange(0, 10).reshape((1, -1))
epsilon_0 = np.array(
[
84381.448,
-4680.93,
1.55,
1999.25,
-51.38,
-249.67,
-39.05,
7.12,
27.87,
5.79,
2.45,
]
)
epsilon = np.sum(u * epsilon_0, axis=-1) / 3600 + d_epsilon
d_tau = -20.4898 / (3600 * r_deg)
sun_longitude = theta + d_psi + d_tau
v_0 = 280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * jc ** 2 - jc ** 3 / 38710000
v_0 = v_0 % 360
v = v_0 + d_psi * np.cos(np.deg2rad(epsilon))
alpha = np.arctan2(np.sin(np.deg2rad(sun_longitude)) *
np.cos(np.deg2rad(epsilon)) -
np.tan(np.deg2rad(beta)) *
np.sin(np.deg2rad(epsilon)),
np.cos(np.deg2rad(sun_longitude)))
alpha_deg = np.rad2deg(alpha) % 360
delta = np.arcsin(
np.sin(np.deg2rad(beta)) *
np.cos(np.deg2rad(epsilon)) +
np.cos(np.deg2rad(beta)) *
np.sin(np.deg2rad(epsilon)) *
np.cos(np.deg2rad(sun_longitude))
)
delta_deg = np.rad2deg(delta) % 360
h = v + latitude - alpha_deg
xi_deg = 8.794 / (3600 * r_deg)
u = np.arctan(0.99664719 * np.tan(latitude))
x = np.cos(u) + elevation / 6378140 * np.cos(latitude)
y = 0.99664719 * np.sin(u) + elevation / 6378140 * np.sin(latitude)
d_alpha = np.arctan2(-1 * x * np.sin(np.radians(xi_deg)) * np.sin(np.radians(h)), np.cos(delta))
d_alpha = np.rad2deg(d_alpha)
alpha_prime = alpha_deg + d_alpha
delta_prime = np.arctan2((np.sin(delta) - y * np.sin(np.radians(xi_deg))) * np.cos(np.radians(d_alpha)),
np.cos(delta) - x * np.sin(np.radians(xi_deg)) * np.cos(np.radians(h)))
h_prime = h - d_alpha
e_0 = np.arcsin(np.sin(latitude) * np.sin(delta) + np.cos(latitude) * np.cos(delta_prime))