# 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