Spaces:
Runtime error
Runtime error
# backmapping.py | |
import streamlit as st | |
import math | |
from copy import deepcopy | |
import matplotlib.patches as mpatches | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import pandas as pd | |
import scipy.constants as const | |
from matplotlib.legend_handler import HandlerPatch | |
from sunpy import log | |
from sunpy.coordinates import frames | |
from sunpy.coordinates import get_horizons_coord | |
from apps.extras.selected_bodies import body_dict | |
plt.rcParams['axes.linewidth'] = 1.5 | |
plt.rcParams['font.size'] = 15 | |
plt.rcParams['agg.path.chunksize'] = 20000 | |
pd.options.display.max_rows = None | |
pd.options.display.float_format = '{:.1f}'.format | |
# disable unnecessary logging | |
log.setLevel('WARNING') | |
def print_body_list(): | |
""" | |
prints a selection of body keys and the corresponding body names which may be provided to the | |
HeliosphericConstellation class | |
""" | |
# print('Please visit https://ssd.jpl.nasa.gov/horizons.cgi?s_target=1#top for a complete list of available bodies') | |
data = pd.DataFrame\ | |
.from_dict(body_dict, orient='index', columns=['ID', 'Body', 'Color'])\ | |
.drop(['ID', 'Color'], 'columns')\ | |
.drop_duplicates() | |
data.index.name = 'Key' | |
return data | |
class HeliosphericConstellation(): | |
""" | |
Class which handles the selected bodies | |
Parameters | |
---------- | |
date: str | |
body_list: list | |
list of body keys to be used. Keys can be string of int. | |
vsw_list: list, optional | |
list of solar wind speeds at the position of the different bodies. Must have the same length as body_list. | |
Default is an epmty list leading to vsw=400km/s used for every body. | |
reference_long: float, optional | |
Carrington longitute of reference position at the Sun | |
reference_lat: float, optional | |
Heliographic latitude of referene position at the Sun | |
""" | |
def __init__(self, date, body_list, vsw_list=[], reference_long=None, reference_lat=None): | |
body_list = list(dict.fromkeys(body_list)) | |
bodies = deepcopy(body_dict) | |
self.date = date | |
self.reference_long = reference_long | |
self.reference_lat = reference_lat | |
pos_E = get_horizons_coord(399, self.date, 'id') # (lon, lat, radius) in (deg, deg, AU) | |
self.pos_E = pos_E.transform_to(frames.HeliographicCarrington(observer='Sun')) | |
if len(vsw_list) == 0: | |
vsw_list = np.zeros(len(body_list)) + 400 | |
random_cols = ['forestgreen', 'mediumblue', 'm', 'saddlebrown', 'tomato', 'olive', 'steelblue', 'darkmagenta', | |
'c', 'darkslategray', 'yellow', 'darkolivegreen'] | |
body_lon_list = [] | |
body_lat_list = [] | |
body_dist_list = [] | |
longsep_E_list = [] | |
latsep_E_list = [] | |
body_vsw_list = [] | |
footp_long_list = [] | |
longsep_list = [] | |
latsep_list = [] | |
footp_longsep_list = [] | |
for i, body in enumerate(body_list.copy()): | |
if body in bodies: | |
body_id = bodies[body][0] | |
body_lab = bodies[body][1] | |
body_color = bodies[body][2] | |
else: | |
body_id = body | |
body_lab = str(body) | |
body_color = random_cols[i] | |
bodies.update(dict.fromkeys([body_id], [body_id, body_lab, body_color])) | |
try: | |
pos = get_horizons_coord(body_id, date, 'id') # (lon, lat, radius) in (deg, deg, AU) | |
pos = pos.transform_to(frames.HeliographicCarrington(observer='Sun')) | |
bodies[body_id].append(pos) | |
bodies[body_id].append(vsw_list[i]) | |
longsep_E = pos.lon.value - self.pos_E.lon.value | |
if longsep_E > 180: | |
longsep_E = longsep_E - 360. | |
latsep_E = pos.lat.value - self.pos_E.lat.value | |
body_lon_list.append(pos.lon.value) | |
body_lat_list.append(pos.lat.value) | |
body_dist_list.append(pos.radius.value) | |
longsep_E_list.append(longsep_E) | |
latsep_E_list.append(latsep_E) | |
body_vsw_list.append(vsw_list[i]) | |
sep, alpha = self.backmapping(pos, date, reference_long, vsw=vsw_list[i]) | |
bodies[body_id].append(sep) | |
body_footp_long = pos.lon.value + alpha | |
if body_footp_long > 360: | |
body_footp_long = body_footp_long - 360 | |
footp_long_list.append(body_footp_long) | |
if self.reference_long is not None: | |
bodies[body_id].append(sep) | |
long_sep = pos.lon.value - self.reference_long | |
if long_sep > 180: | |
long_sep = long_sep - 360. | |
longsep_list.append(long_sep) | |
footp_longsep_list.append(sep) | |
if self.reference_lat is not None: | |
lat_sep = pos.lat.value - self.reference_lat | |
latsep_list.append(lat_sep) | |
except ValueError: | |
print('') | |
print('!!! No ephemeris for target "' + str(body) + '" for date ' + self.date) | |
st.warning('No ephemeris for target "' + str(body) + '" for date ' + self.date) | |
body_list.remove(body) | |
body_dict_short = {sel_key: bodies[sel_key] for sel_key in body_list} | |
self.body_dict = body_dict_short | |
self.max_dist = np.max(body_dist_list) | |
self.coord_table = pd.DataFrame( | |
{'Spacecraft/Body': list(self.body_dict.keys()), 'Carrington Longitude (°)': body_lon_list, | |
'Latitude (°)': body_lat_list, 'Heliocentric Distance (AU)': body_dist_list, | |
"Longitudinal separation to Earth's longitude": longsep_E_list, | |
"Latitudinal separation to Earth's latitude": latsep_E_list, 'Vsw': body_vsw_list, | |
'Magnetic footpoint longitude (Carrington)': footp_long_list}) | |
if self.reference_long is not None: | |
self.coord_table['Longitudinal separation between body and reference_long'] = longsep_list | |
self.coord_table[ | |
"Longitudinal separation between body's mangetic footpoint and reference_long"] = footp_longsep_list | |
if self.reference_lat is not None: | |
self.coord_table['Latitudinal separation between body and reference_lat'] = latsep_list | |
pass | |
self.coord_table.style.set_properties(**{'text-align': 'left'}) | |
def backmapping(self, body_pos, date, reference_long, vsw=400): | |
""" | |
Determine the longitudinal separation angle of a given spacecraft and a given reference longitude | |
Parameters | |
---------- | |
body_pos : astropy.coordinates.sky_coordinate.SkyCoord | |
coordinate of the body in Carrington coordinates | |
date: str | |
e.g., '2020-03-22 12:30' | |
reference_long: float | |
Carrington longitude of reference point at Sun to which we determine the longitudinal separation | |
vsw: float | |
solar wind speed (km/s) used to determine the position of the magnetic footpoint of the body. Default is 400. | |
out: | |
sep: float | |
longitudinal separation of body magnetic footpoint and reference longitude in degrees | |
alpha: float | |
backmapping angle | |
""" | |
AU = const.au / 1000 # km | |
pos = body_pos | |
lon = pos.lon.value | |
dist = pos.radius.value | |
omega = math.radians(360. / (25.38 * 24 * 60 * 60)) # rot-angle in rad/sec, sidereal period | |
tt = dist * AU / vsw | |
alpha = math.degrees(omega * tt) | |
if reference_long is not None: | |
sep = (lon + alpha) - reference_long | |
if sep > 180.: | |
sep = sep - 360 | |
if sep < -180.: | |
sep = 360 - abs(sep) | |
else: | |
sep = np.nan | |
return sep, alpha | |
def plot(self, plot_spirals=True, plot_sun_body_line=False, show_earth_centered_coord=True, reference_vsw=400, transparent=False, outfile=''): | |
""" | |
Make a polar plot showing the Sun in the center (view from North) and the positions of the selected bodies | |
Parameters | |
---------- | |
plot_spirals: bool | |
if True, the magnetic field lines connecting the bodies with the Sun are plotted | |
plot_sun_body_line: bool | |
if True, straight lines connecting the bodies with the Sun are plotted | |
show_earth_centered_coord: bool | |
if True, additional longitudinal tickmarks are shown with Earth at longitude 0 | |
reference_vsw: int | |
if defined, defines solar wind speed for reference. if not defined, 400 km/s is used | |
outfile: string | |
if provided, the plot is saved with outfile as filename | |
""" | |
import pylab as pl | |
AU = const.au / 1000 # km | |
fig, ax = plt.subplots(subplot_kw=dict(projection='polar'), figsize=(12, 8)) | |
self.ax = ax | |
r = np.arange(0.007, self.max_dist + 0.3, 0.001) | |
omega = np.radians(360. / (25.38 * 24 * 60 * 60)) # solar rot-angle in rad/sec, sidereal period | |
for i, body_id in enumerate(self.body_dict): | |
body_lab = self.body_dict[body_id][1] | |
body_color = self.body_dict[body_id][2] | |
body_vsw = self.body_dict[body_id][4] | |
body_pos = self.body_dict[body_id][3] | |
pos = body_pos | |
dist_body = pos.radius.value | |
body_long = pos.lon.value | |
E_long = self.pos_E.lon.value | |
dist_e = self.pos_E.radius.value | |
# plot body positions | |
ax.plot(np.deg2rad(body_long), dist_body, 's', color=body_color, label=body_lab) | |
if plot_sun_body_line: | |
# ax.plot(alpha_ref[0], 0.01, 0) | |
ax.plot([np.deg2rad(body_long), np.deg2rad(body_long)], [0.01, dist_body], ':', color=body_color) | |
# plot the spirals | |
if plot_spirals: | |
tt = dist_body * AU / body_vsw | |
alpha = np.degrees(omega * tt) | |
alpha_body = np.deg2rad(body_long) + omega / (body_vsw / AU) * (dist_body - r) | |
ax.plot(alpha_body, r, color=body_color) | |
if self.reference_long is not None: | |
delta_ref = self.reference_long | |
if delta_ref < 0.: | |
delta_ref = delta_ref + 360. | |
alpha_ref = np.deg2rad(delta_ref) + omega / (reference_vsw / AU) * (dist_e / AU - r) - ( | |
omega / (reference_vsw / AU) * (dist_e / AU)) | |
# old arrow style: | |
# arrow_dist = min([self.max_dist + 0.1, 2.]) | |
# ref_arr = plt.arrow(alpha_ref[0], 0.01, 0, arrow_dist, head_width=0.12, head_length=0.11, edgecolor='black', | |
# facecolor='black', lw=2, zorder=5, overhang=0.2) | |
arrow_dist = min([self.max_dist/3.2, 2.]) | |
ref_arr = plt.arrow(alpha_ref[0], 0.01, 0, arrow_dist, head_width=0.2, head_length=0.07, edgecolor='black', | |
facecolor='black', lw=1.8, zorder=5, overhang=0.2) | |
if plot_spirals: | |
ax.plot(alpha_ref, r, '--k', label=f'field line connecting to\nref. long. (vsw={reference_vsw} km/s)') | |
leg1 = ax.legend(loc=(1.2, 0.7), fontsize=13) | |
if self.reference_long is not None: | |
def legend_arrow(width, height, **_): | |
return mpatches.FancyArrow(0, 0.5 * height, width, 0, length_includes_head=True, | |
head_width=0.75 * height) | |
leg2 = ax.legend([ref_arr], ['reference long.'], loc=(1.2, 0.6), | |
handler_map={mpatches.FancyArrow: HandlerPatch(patch_func=legend_arrow), }, | |
fontsize=13) | |
ax.add_artist(leg1) | |
ax.set_rlabel_position(E_long + 120) | |
ax.set_theta_offset(np.deg2rad(270 - E_long)) | |
ax.set_rmax(self.max_dist + 0.3) | |
ax.set_rmin(0.01) | |
ax.yaxis.get_major_locator().base.set_params(nbins=4) | |
circle = plt.Circle((0., 0.), self.max_dist + 0.29, transform=ax.transData._b, edgecolor="k", facecolor=None, | |
fill=False, lw=2) | |
ax.add_patch(circle) | |
# manually plot r-grid lines with different resolution depending on maximum distance bodyz | |
# st.sidebar.info(self.max_dist) | |
if self.max_dist < 2: | |
ax.set_rgrids(np.arange(0, self.max_dist + 0.29, 0.5)[1:], angle=22.5) | |
# st.sidebar.info(str(np.arange(0, self.max_dist + 0.29, 0.5))) | |
else: | |
if self.max_dist < 10: | |
ax.set_rgrids(np.arange(0, self.max_dist + 0.29, 1.0)[1:], angle=22.5) | |
# st.sidebar.info(str(np.arange(0, self.max_dist + 0.29, 1.0))) | |
ax.set_title(self.date + '\n', pad=60) | |
plt.tight_layout() | |
plt.subplots_adjust(bottom=0.15) | |
if show_earth_centered_coord: | |
pos1 = ax.get_position() # get the original position of the polar plot | |
offset = 0.12 | |
pos2 = [pos1.x0 - offset / 2, pos1.y0 - offset / 2, pos1.width + offset, pos1.height + offset] | |
ax2 = self._polar_twin(ax, E_long, pos2) | |
ax.tick_params(axis='x', pad=10) | |
ax.text(0.94, 0.16, 'Solar-MACH', | |
fontfamily='DejaVu Serif', fontsize=28, | |
ha='right', va='bottom', transform=fig.transFigure) | |
ax.text(0.94, 0.12, 'https://solar-mach.github.io', | |
fontfamily='DejaVu Sans', fontsize=18, | |
ha='right', va='bottom', transform=fig.transFigure) | |
if transparent: | |
fig.patch.set_alpha(0.0) | |
if outfile != '': | |
plt.savefig(outfile) | |
st.pyplot(fig) | |
def _polar_twin(self, ax, E_long, position): | |
""" | |
add an additional axes which is needed to plot additional longitudinal tickmarks with Earth at longitude 0 | |
""" | |
ax2 = ax.figure.add_axes(position, projection='polar', | |
label='twin', frameon=False, | |
theta_direction=ax.get_theta_direction(), | |
theta_offset=E_long) | |
ax2.set_rmax(self.max_dist + 0.3) | |
ax2.yaxis.set_visible(False) | |
ax2.set_theta_zero_location("S") | |
ax2.tick_params(axis='x', colors='darkgreen', pad=10) | |
gridlines = ax2.xaxis.get_gridlines() | |
for xax in gridlines: | |
xax.set_color('darkgreen') | |
return ax2 |