From d887aa07e043d6fba190bdbaeed9926ebd588a94 Mon Sep 17 00:00:00 2001 From: michotte Date: Fri, 27 Nov 2020 15:03:47 +0100 Subject: [PATCH] fix Jerab2005 change the resolve_poly2 function and adapte the code of _parabolic_appox, fix a bug in mp_liu2015 fix a bug in to_swi plot earth boundaries with axis slice import root_scalar in planet_env import root in planet_env add add_columns_to_df function change index_isin function change formisano coeff correction _interest_points add color background plot change color background plot fix Z axis label add jelineck models add mp_lin2010 change base = cylindrical to spherical add pressure dependance to formisano model change default values shue1998 add Nguyen2020 model change gamma in jerab model change coordinate system add tangents bs_jelinek2012 and new methode in Magnetosheath class import smath.norm in planetary corrections methode tangents in Magnetosheath class add normal and tangent methode in Magnetosheath class for mp_shue1998 and bs_jelinek2012 fix bug in mp_shue1998_normal fix bug in magnetopause normal and tangents magnetopause and bow_shock method return also tangents and normal add earth tilt functions add mp_shue1997 model correction kwargs setUp function --- space/coordinates/coordinates.py | 100 +++--- space/models/planetary.py | 523 +++++++++++++++++++++++-------- space/plot/planet_env.py | 289 +++++++++++++---- space/smath.py | 46 ++- space/utils.py | 30 +- tests/test_space.py | 8 +- 6 files changed, 730 insertions(+), 266 deletions(-) diff --git a/space/coordinates/coordinates.py b/space/coordinates/coordinates.py index 124f905..3be0c83 100644 --- a/space/coordinates/coordinates.py +++ b/space/coordinates/coordinates.py @@ -2,45 +2,40 @@ import pandas as pd -def spherical_to_cartesian(R, theta, phi): - x = R * np.cos(theta) - y = R * np.sin(theta) * np.cos(phi) - z = R * np.sin(theta) * np.sin(phi) +def spherical_to_cartesian(r, theta, phi): + x = r * np.cos(theta) + y = r * np.sin(theta) * np.sin(phi) + z = r * np.sin(theta) * np.cos(phi) return x, y, z +def cylindrical_to_cartesian(r, theta): + x = r * np.cos(theta) + y = r * np.sin(theta) + z = r * np.sin(theta) + return x, y, z -def cartesian_to_spherical(X, Y, Z): - r = np.sqrt(X ** 2 + Y ** 2 + Z ** 2) - theta = np.arccos(X / r) - phi = np.arctan2(Z, Y) +def cartesian_to_spherical(x, y, z): + r = np.sqrt(x ** 2 + y ** 2 + z ** 2) + theta = np.arccos(x / r) + phi = np.arctan2(y, z) + phi[z==0]=np.sign(y)*np.pi/2 return r, theta, phi -def base_choice(base, R, theta, phi): - if base == 'cartesian': +def choice_coordinate_system(R, theta, phi, **kwargs): + coord_sys = kwargs.get('coord_sys','cartesian') + if coord_sys == 'cartesian': return spherical_to_cartesian(R, theta, phi) - elif base == 'spherical': + elif coord_sys == 'spherical': return R, theta, phi else: - print('Error : base parameter must be set to "cartesian" or "spherical" ') - + print('Error : coord_sys parameter must be set to "cartesian" or "spherical" ') -def check_base(pos): - if ((hasattr(pos, 'R')) | (hasattr(pos, 'r'))) & (hasattr(pos, 'X')) | (hasattr(pos, 'x')): - base = 'spherical&cartesian' - elif (hasattr(pos, 'R')) | (hasattr(pos, 'r')): - base = 'spherical' - elif (hasattr(pos, 'X')) | (hasattr(pos, 'x')): - base = 'cartesian' - else: - print('must check the name of the variables : cartesian = (X,Y,Z) and spherical = (R,theta,phi)') - return base -def add_cst_radiuus(pos, cst): - base = check_base(pos) - if base == 'cartesian': +def add_cst_radiuus(pos, cst,coord_sys): + if coord_sys == 'cartesian': r, theta, phi = cartesian_to_spherical(pos.X, pos.Y, pos.Z) else: r, theta, phi = pos.R, pos.theta, pos.phi @@ -71,30 +66,29 @@ def to_swi(omni_data, msh_data, pos_msh): o_data = omni_data.copy() pos = pos_msh.copy() - o_data['Vx'] = X_swi[:, 0] * omni_data['Vx'] + X_swi[:, 1] * omni_data['Vy'] + X_swi[:, 2] * omni_data['Vz'] - o_data['Vy'] = Y_swi[:, 0] * omni_data['Vx'] + Y_swi[:, 1] * omni_data['Vy'] + Y_swi[:, 2] * omni_data['Vz'] - o_data['Vz'] = Z_swi[:, 0] * omni_data['Vx'] + Z_swi[:, 1] * omni_data['Vy'] + Z_swi[:, 2] * omni_data['Vz'] - - o_data['Bx'] = np.sign(omni_data['COA']) * ( - X_swi[:, 0] * omni_data['Bx'] + X_swi[:, 1] * omni_data['By'] + X_swi[:, 2] * omni_data['Bz']) - o_data['By'] = np.sign(omni_data['COA']) * ( - Y_swi[:, 0] * omni_data['Bx'] + Y_swi[:, 1] * omni_data['By'] + Y_swi[:, 2] * omni_data['Bz']) - o_data['Bz'] = np.sign(omni_data['COA']) * ( - Z_swi[:, 0] * omni_data['Bx'] + Z_swi[:, 1] * omni_data['By'] + Z_swi[:, 2] * omni_data['Bz']) - - data['Vx'] = X_swi[:, 0] * msh_data['Vx'] + X_swi[:, 1] * msh_data['Vy'] + X_swi[:, 2] * msh_data['Vz'] - data['Vy'] = Y_swi[:, 0] * msh_data['Vx'] + Y_swi[:, 1] * msh_data['Vy'] + Y_swi[:, 2] * msh_data['Vz'] - data['Vz'] = Z_swi[:, 0] * msh_data['Vx'] + Z_swi[:, 1] * msh_data['Vy'] + Z_swi[:, 2] * msh_data['Vz'] - - data['Bx'] = np.sign(omni_data['COA']) * ( - X_swi[:, 0] * msh_data['Bx'] + X_swi[:, 1] * msh_data['By'] + X_swi[:, 2] * msh_data['Bz']) - data['By'] = np.sign(omni_data['COA']) * ( - Y_swi[:, 0] * msh_data['Bx'] + Y_swi[:, 1] * msh_data['By'] + Y_swi[:, 2] * msh_data['Bz']) - data['Bz'] = np.sign(omni_data['COA']) * ( - Z_swi[:, 0] * msh_data['Bx'] + Z_swi[:, 1] * msh_data['By'] + Z_swi[:, 2] * msh_data['Bz']) - - pos['X'] = X_swi[:, 0] * pos_msh['X'] + X_swi[:, 1] * pos_msh['Y'] + X_swi[:, 2] * pos_msh['Z'] - pos['Y'] = Y_swi[:, 0] * pos_msh['X'] + Y_swi[:, 1] * pos_msh['Y'] + Y_swi[:, 2] * pos_msh['Z'] - pos['Z'] = Z_swi[:, 0] * pos_msh['X'] + Z_swi[:, 1] * pos_msh['Y'] + Z_swi[:, 2] * pos_msh['Z'] - - return data, pos, o_data + + + o_data['Vx'] = X_swi[:,0]*omni_data['Vx']+X_swi[:,1]*(omni_data['Vy']+29.8)+X_swi[:,2]*omni_data['Vz'] + o_data['Vy'] = Y_swi[:,0]*omni_data['Vx']+Y_swi[:,1]*(omni_data['Vy']+29.8)+Y_swi[:,2]*omni_data['Vz'] + o_data['Vz'] = Z_swi[:,0]*omni_data['Vx']+Z_swi[:,1]*(omni_data['Vy']+29.8)+Z_swi[:,2]*omni_data['Vz'] + + o_data['Bx'] = np.sign(omni_data['COA'])*(X_swi[:,0]*omni_data['Bx']+X_swi[:,1]*omni_data['By']+X_swi[:,2]*omni_data['Bz']) + o_data['By'] = np.sign(omni_data['COA'])*(Y_swi[:,0]*omni_data['Bx']+Y_swi[:,1]*omni_data['By']+Y_swi[:,2]*omni_data['Bz']) + o_data['Bz'] = np.sign(omni_data['COA'])*(Z_swi[:,0]*omni_data['Bx']+Z_swi[:,1]*omni_data['By']+Z_swi[:,2]*omni_data['Bz']) + + data['Vx'] = X_swi[:,0]*msh_data['Vx']+X_swi[:,1]*msh_data['Vy']+X_swi[:,2]*msh_data['Vz'] + data['Vy'] = Y_swi[:,0]*msh_data['Vx']+Y_swi[:,1]*msh_data['Vy']+Y_swi[:,2]*msh_data['Vz'] + data['Vz'] = Z_swi[:,0]*msh_data['Vx']+Z_swi[:,1]*msh_data['Vy']+Z_swi[:,2]*msh_data['Vz'] + + data['Bx'] = np.sign(omni_data['COA'])*(X_swi[:,0]*msh_data['Bx']+X_swi[:,1]*msh_data['By']+X_swi[:,2]*msh_data['Bz']) + data['By'] = np.sign(omni_data['COA'])*(Y_swi[:,0]*msh_data['Bx']+Y_swi[:,1]*msh_data['By']+Y_swi[:,2]*msh_data['Bz']) + data['Bz'] = np.sign(omni_data['COA'])*(Z_swi[:,0]*msh_data['Bx']+Z_swi[:,1]*msh_data['By']+Z_swi[:,2]*msh_data['Bz']) + + + pos['X'] = X_swi[:,0]*pos_msh['X']+X_swi[:,1]*pos_msh['Y']+X_swi[:,2]*pos_msh['Z'] + pos['Y'] = Y_swi[:,0]*pos_msh['X']+Y_swi[:,1]*pos_msh['Y']+Y_swi[:,2]*pos_msh['Z'] + pos['Z'] = Z_swi[:,0]*pos_msh['X']+Z_swi[:,1]*pos_msh['Y']+Z_swi[:,2]*pos_msh['Z'] + + + return data,pos,o_data + diff --git a/space/models/planetary.py b/space/models/planetary.py index 8943366..eaa39fa 100644 --- a/space/models/planetary.py +++ b/space/models/planetary.py @@ -1,34 +1,70 @@ import numpy as np import pandas as pd from scipy import constants as cst +from datetime import timedelta, datetime import sys sys.path.append('.') from ..coordinates import coordinates as coords -from ..smath import resolve_poly2 +from ..smath import resolve_poly2_real_roots +from ..smath import norm as mnorm +from ..utils import listify -def _checking_angles(theta, phi): - if isinstance(theta, np.ndarray) and isinstance(theta, np.ndarray) and len(theta.shape) > 1 and len(phi.shape) > 1: - return np.meshgrid(theta, phi) - return theta, phi + + +def tilt_earth_normal_year(): + tilts = np.zeros(365)+np.nan + tilts[:79] = np.linspace(-34,0,89)[10:] + tilts[79:172] = np.linspace(0,34,93) + tilts[172:265] = np.linspace(34,0,93) + tilts[265:355] = np.linspace(0,-34,90) + tilts[355:] = np.linspace(-34,0,89)[:10] + return tilts*np.pi/180 + + +def tilt_earth_bissextile_year(): + tilts = np.zeros(366)+np.nan + tilts[:80] = np.linspace(-34,0,90)[10:] + tilts[80:173] = np.linspace(0,34,93) + tilts[173:266] = np.linspace(34,0,93) + tilts[266:356] = np.linspace(0,-34,90) + tilts[356:] = np.linspace(-34,0,90)[:10] + return tilts*np.pi/180 + + +def get_tilt(date): + years = pd.DatetimeIndex(date).year + days = pd.to_numeric(pd.DatetimeIndex(date).strftime('%j'))-1 + n_days_year = ((pd.to_datetime((years+1).astype(str))-pd.to_datetime(years.astype(str)))/timedelta(days=1)).astype(int).values + year_tilts = tilt_earth_normal_year() + year_bis_tilts =tilt_earth_bissextile_year() + tilts = np.zeros(len(date)) + tilts[n_days_year==366]= year_bis_tilts[days[n_days_year==366]] + tilts[n_days_year==365]= year_tilts[days[n_days_year==365]] + return tilts + def _formisano1979(theta, phi, **kwargs): a11, a22, a33, a12, a13, a23, a14, a24, a34, a44 = kwargs["coefs"] - ct = np.cos(theta) - st = np.sin(theta) - cp = np.cos(phi) - sp = np.sin(phi) - A = a11 * ct ** 2 + st ** 2 * (a22 * cp ** 2 + a33 * sp ** 2) \ - + ct * st * (a12 * cp + a13 * sp) + a23 * st ** 2 * cp * sp - B = a14 * ct + st * (a24 * cp + a34 * sp) + + if 'Pd' in kwargs: + a14 = a14 * (2.056 / kwargs['Pd']) ** (1 / 6) + a44 = a44 * (2.056 / kwargs['Pd']) ** (1 / 3) + x = np.cos(theta) + y = np.sin(theta)*np.sin(phi) + z = np.sin(theta)*np.cos(phi) + A = a11 * x ** 2 + a22 * y ** 2 + a33 * z ** 2 + a12 * x * y + a13 * x * z + a23 * y * z + B = a14 * x + a24 * y + a34 * z C = a44 D = B ** 2 - 4 * A * C return (-B + np.sqrt(D)) / (2 * A) + + def formisano1979(theta, phi, **kwargs): ''' Formisano 1979 magnetopause model. Give the default position of the magnetopause. @@ -37,7 +73,7 @@ def formisano1979(theta, phi, **kwargs): - phi : angle in radiant, can be int, float or array (1D or 2D) kwargs: - boundary : "magnetopause", "bow_shock" - - base : can be "cartesian" (default) or "spherical" + - coord_sys : can be "cartesian" (default) or "spherical" information : to get a particular point theta and phi must be an int or a float (ex : the nose of the boundary is given with the input theta=0 and phi=0). If a plan (2D) of @@ -46,25 +82,22 @@ def formisano1979(theta, phi, **kwargs): theta=np.linespace(-np.pi/2,np.pi/2,100) and phi=0). To get the 3D boundary theta and phi must be given an array, if it is two 1D array a meshgrid will be performed to obtain a two 2D array. - return : X,Y,Z (base="cartesian")or R,theta,phi (base="spherical") depending on the chosen base + return : X,Y,Z (coord_sys="cartesian")or R,theta,phi (coord_sys="spherical") depending on the chosen coord_sys ''' if kwargs["boundary"] == "magnetopause": - coefs = [0.65, 1, 1.16, 0.03, -0.28, -0.11, 21.41, 0.46, -0.36, -221] + coefs = [0.65, 1, 1.16, 0.03, -0.28, -0.11, 21.41, 0.46, -0.36, -221] # coeff magnetopause Romashets 2019 with aberration, take into account earth's rotation + #coefs = [0.66, 1, 1.16, 0.08, -0.29, -0.09, 21.47, -0.97, -0.36, -222] # coeff magnetopause Romashets 2019 without aberation elif kwargs["boundary"] == "bow_shock": - coefs = [0.52, 1, 1.05, 0.13, -0.16, -0.08, 47.53, -0.42, 0.67, -613] + coefs = [0.52, 1, 1.05, 0.13, -0.16, -0.08, 47.53, -0.42, 0.67, -613] # coeff bow shock Romashets 2019 with aberration, take into account earth's rotation + #coefs = [0.54,1,1.06,0.19,-0.17,-0.07,47.90,-3.62,0.68,-619] # coeff bow shock Romashets 2019 without aberation else: raise ValueError("boundary: {} not allowed".format(kwargs["boundary"])) - theta, phi = _checking_angles(theta, phi) - r = _formisano1979(theta, phi, coefs=coefs) - base = kwargs.get("base", "cartesian") - if base == "cartesian": - return coords.spherical_to_cartesian(r, theta, phi) - elif base == "spherical": - return r, theta, phi - raise ValueError("unknown base '{}'".format(kwargs["base"])) + r = _formisano1979(theta, phi, coefs=coefs,**kwargs) + + return coords.choice_coordinate_system(r, theta, phi, **kwargs) def mp_formisano1979(theta, phi, **kwargs): return formisano1979(theta, phi, boundary="magnetopause", **kwargs) @@ -131,8 +164,12 @@ def make_Rav(theta, phi): a34 = -0.6 a44 = -618 - a = a11 * np.cos(theta) ** 2 + np.sin(theta) ** 2 * (a22 * np.cos(phi) ** 2 + a33 * np.sin(phi) ** 2) - b = a14 * np.cos(theta) + np.sin(theta) * (a24 * np.cos(phi) + a34 * np.sin(phi)) + x = np.cos(theta) + y = np.sin(theta) * np.sin(phi) + z = np.sin(theta) * np.cos(phi) + + a = a11 * x ** 2 + a22 * y ** 2 + a33 * z ** 2 + a12 * x * y + b = a14 * x + a24 * y + a34 * z c = a44 delta = b ** 2 - 4 * a * c @@ -143,7 +180,7 @@ def make_Rav(theta, phi): Np = kwargs.get('Np', 6.025) V = kwargs.get('V', 427.496) B = kwargs.get('B', 5.554) - gamma = kwargs.get('gamma', 2.15) + gamma = kwargs.get('gamma', 5./3) Ma = V * 1e3 * np.sqrt(Np * 1e6 * cst.m_p * cst.mu_0) / (B * 1e-9) C = 91.55 @@ -154,16 +191,26 @@ def make_Rav(theta, phi): K = ((gamma - 1) * Ma ** 2 + 2) / ((gamma + 1) * (Ma ** 2 - 1)) r = (Rav / R0) * (C / (Np * V ** 2) ** (1 / 6)) * (1 + D * K) - base = kwargs.get('base', 'cartesian') + return coords.choice_coordinate_system(r, theta, phi, **kwargs) + + +def mp_shue1997(theta, phi, **kwargs): + Pd = kwargs.get("Pd", 2.056) + Bz = kwargs.get("Bz", -0.001) - if base == "cartesian": - x = r * np.cos(theta) - y = r * np.sin(theta) * np.cos(phi) - z = r * np.sin(theta) * np.sin(phi) - return x, y, z - elif base == "spherical": - return r, theta, phi - raise ValueError("unknown base '{}'".format(kwargs["base"])) + if isinstance(Bz, float) | isinstance(Bz, int): + if Bz >= 0: + r0 = (11.4 + 0.13 * Bz) * Pd ** (-1 / 6.6) + else: + r0 = (11.4 + 0.14 * Bz) * Pd ** (-1 / 6.6) + else: + if isinstance(Pd, float) | isinstance(Pd, int): + Pd = np.ones_like(Bz) * Pd + r0 = (11.4 + 0.13 * Bz) * Pd ** (-1 / 6.6) + r0[Bz < 0] = (11.4 + 0.14 * Bz[Bz < 0]) * Pd[Bz < 0] ** (-1 / 6.6) + a = (0.58 - 0.010 * Bz) * (1 + 0.010 * Pd) + r = r0 * (2. / (1 + np.cos(theta))) ** a + return coords.choice_coordinate_system(r, theta, phi, **kwargs) def mp_shue1998(theta, phi, **kwargs): @@ -183,25 +230,16 @@ def mp_shue1998(theta, phi, **kwargs): ''' - Pd = kwargs.get("pdyn", 2) - Bz = kwargs.get("Bz", 1) + Pd = kwargs.get("Pd", 2.056) + Bz = kwargs.get("Bz", -0.001) r0 = (10.22 + 1.29 * np.tanh(0.184 * (Bz + 8.14))) * Pd ** (-1. / 6.6) a = (0.58 - 0.007 * Bz) * (1 + 0.024 * np.log(Pd)) r = r0 * (2. / (1 + np.cos(theta))) ** a - - base = kwargs.get("base", "cartesian") - if base == "cartesian": - x = r * np.cos(theta) - y = r * np.sin(theta) - z = r * np.sin(theta) - return x, y, z - elif base == "cylindrical": - return r, theta - raise ValueError("unknown base '{}'".format(kwargs["base"])) + return coords.choice_coordinate_system(r, theta, phi, **kwargs) -def MP_Lin2010(phi_in, th_in, Pd, Pm, Bz, tilt=0.): +def mp_lin2010(theta, phi, **kwargs): ''' The Lin 2010 Magnetopause model. Returns the MP distance for a given azimuth (phi), zenith (th), solar wind dynamic and magnetic pressures (nPa) and Bz (in nT). @@ -211,54 +249,37 @@ def MP_Lin2010(phi_in, th_in, Pd, Pm, Bz, tilt=0.): * Pm: Solar wind magnetic pressure in nPa * tilt: Dipole tilt ''' - a = [12.544, - -0.194, - 0.305, - 0.0573, - 2.178, - 0.0571, - -0.999, - 16.473, - 0.00152, - 0.382, - 0.0431, - -0.00763, - -0.210, - 0.0405, - -4.430, - -0.636, - -2.600, - 0.832, - -5.328, - 1.103, - -0.907, - 1.450] - - arr = type(np.array([])) - - if (type(th_in) == arr): - th = th_in.copy() - else: - th = th_in + a = [12.544, -0.194, 0.305, 0.0573, 2.178, 0.0571, -0.999, 16.473, 0.00152, 0.382, 0.0431, -0.00763, -0.210, 0.0405, + -4.430, -0.636, -2.600, 0.832, -5.328, 1.103, -0.907, 1.450] - if (type(phi_in) == arr): - phi = phi_in.copy() + if isinstance(theta, np.ndarray) | isinstance(theta, pd.Series): + t = np.array(theta).copy() + idx = np.where(t < 0)[0] + if isinstance(phi, np.ndarray) | isinstance(phi, pd.Series): + p = np.array(phi).copy() + p[idx] = p[idx] + np.pi + else: + p = phi * np.ones(t.shape) + p[idx] = p[idx] + np.pi else: - phi = phi_in + t = theta + p = phi + (t < 0) * np.pi - el = th_in < 0. - if (type(el) == arr): - if (el.any()): - th[el] = -th[el] + t = np.sign(t) * t - if (type(phi) == type(arr)): - phi[el] = phi[el] + np.pi - else: - phi = phi * np.ones(th.shape) + np.pi * el + Pd = kwargs.get('Pd', 2.056) + Bx = kwargs.get('Bx', 0.032) + By = kwargs.get('By', -0.015) + Bz = kwargs.get('Bz', -0.001) + tilt = kwargs.get('tilt', 0.) + + B_param = [('Bx' in kwargs), ('By' in kwargs), ('Bz' in kwargs)] + if all(B_param): + Pm = (Bx ** 2 + By ** 2 + Bz ** 2) * 1e-18 / (2 * cst.mu_0) * 1e9 + elif not any(B_param): + Pm = 0.016 else: - if (el): - th = -th - phi = phi + np.pi + raise ValueError('None or all Bx, By, Bz parameters must be set') P = Pd + Pm @@ -275,8 +296,8 @@ def quad(i, s): quad(11, [1, 0]), a[13]] - f = np.cos(0.5 * th) + a[5] * np.sin(2 * th) * (1 - np.exp(-th)) - s = beta[0] + beta[1] * np.sin(phi) + beta[2] * np.cos(phi) + beta[3] * np.cos(phi) ** 2 + f = np.cos(0.5 * t) + a[5] * np.sin(2 * t) * (1 - np.exp(-t)) + s = beta[0] + beta[1] * np.sin(p) + beta[2] * np.cos(p) + beta[3] * np.cos(p) ** 2 f = f ** (s) c = {} @@ -288,8 +309,8 @@ def quad(i, s): c[i] = a[14] * P ** a[15] d[i] = quad(16, [s, 1]) TH[i] = quad(19, [s, 0]) - PHI[i] = np.cos(th) * np.cos(TH[i]) - PHI[i] = PHI[i] + np.sin(th) * np.sin(TH[i]) * np.cos(phi - (1 - s) * 0.5 * np.pi) + PHI[i] = np.cos(t) * np.cos(TH[i]) + PHI[i] = PHI[i] + np.sin(t) * np.sin(TH[i]) * np.cos(p - (1 - s) * 0.5 * np.pi) PHI[i] = np.arccos(PHI[i]) e[i] = a[21] r = f * r0 @@ -297,27 +318,32 @@ def quad(i, s): Q = c['n'] * np.exp(d['n'] * PHI['n'] ** e['n']) Q = Q + c['s'] * np.exp(d['s'] * PHI['s'] ** e['s']) - return r + Q + r = r + Q + + return coords.choice_coordinate_system(r, theta, phi, **kwargs) def mp_liu2015(theta, phi, **kwargs): if isinstance(theta, np.ndarray) | isinstance(theta, pd.Series): - idx = np.where(theta < 0)[0] + t = np.array(theta).copy() + idx = np.where(t < 0)[0] if isinstance(phi, np.ndarray) | isinstance(phi, pd.Series): - phi[idx] = phi[idx] + np.pi + p = np.array(phi).copy() + p[idx] = p[idx] + np.pi else: - phi = phi * np.ones(theta.shape) - phi[idx] = phi[idx] + np.pi + p = phi * np.ones(t.shape) + p[idx] = p[idx] + np.pi else: - phi = phi + (theta < 0) * np.pi + t = theta + p = phi + (t < 0) * np.pi - theta = np.sign(theta) * theta + t = np.sign(t) * t Pd = kwargs.get('Pd', 2.056) Bx = kwargs.get('Bx', 0.032) By = kwargs.get('By', -0.015) Bz = kwargs.get('Bz', -0.001) - tilt = kwargs.get('tilt', 0) + tilt = kwargs.get('tilt', 0.) B_param = [('Bx' in kwargs), ('By' in kwargs), ('Bz' in kwargs)] if all(B_param): Pm = (Bx ** 2 + By ** 2 + Bz ** 2) * 1e-18 / (2 * cst.mu_0) * 1e9 @@ -334,7 +360,7 @@ def mp_liu2015(theta, phi, **kwargs): alpha_z = 0.06263 * np.tanh(0.0251 * tilt) - alpha_phi = (0.06354 + 0.07764 * np.tanh(0.07217 * (abs(Bz) + 4.851))) * (1 + 0.01182 * np.log(Pd)) + alpha_p = (0.06354 + 0.07764 * np.tanh(0.07217 * (abs(Bz) + 4.851))) * (1 + 0.01182 * np.log(Pd)) delta_alpha = 0.02582 * np.tanh(0.0667 * Bx) * np.sign(Bx) @@ -351,35 +377,224 @@ def mp_liu2015(theta, phi, **kwargs): else: omega = np.sign(By) * np.pi / 2 - alpha = alpha_0 + alpha_z * np.cos(phi) + (alpha_phi + delta_alpha * np.sign(np.cos(phi))) * np.cos( - 2 * (phi - omega)) + alpha = alpha_0 + alpha_z * np.cos(p) + (alpha_p + delta_alpha * np.sign(np.cos(p))) * np.cos( + 2 * (p - omega)) l_n = (0.822 + 0.2921 * np.tanh(0.08792 * (Bz + 10.12))) * (1 - 0.01278 * tilt) l_s = (0.822 + 0.2921 * np.tanh(0.08792 * (Bz + 10.12))) * (1 + 0.01278 * tilt) w = (0.2382 + 0.005806 * np.log(Pd)) * (1 + 0.0002335 * tilt ** 2) - C = np.exp(-abs(theta - l_n) / w) * (1 + np.sign(np.cos(phi))) + np.exp(-abs(theta - l_s) / w) * ( - 1 + np.sign(-np.cos(phi))) + C = np.exp(-abs(t - l_n) / w) * (1 + np.sign(np.cos(p))) + np.exp(-abs(t - l_s) / w) * ( + 1 + np.sign(-np.cos(p))) + + r = (r0 * (2 / (1 + np.cos(t))) ** alpha) * (1 - 0.1 * C * np.cos(p) ** 2) + + return coords.choice_coordinate_system(r, theta, phi, **kwargs) + + + +def mp_jelinek2012(theta, phi, **kwargs): + lamb = 1.54 + R = 12.82 + epsilon = 5.26 + Pd = kwargs.get('Pd', 2.056) + + R0 = 2 * R * Pd ** (-1 / epsilon) + r = R0 / (np.cos(theta) + np.sqrt(np.cos(theta) ** 2 + np.sin(theta) * np.sin(theta) * lamb ** 2)) + return coords.choice_coordinate_system(r, theta, phi, **kwargs) + - r = (r0 * (2 / (1 + np.cos(theta))) ** alpha) * (1 - 0.1 * C * np.cos(phi) ** 2) +def bs_jelinek2012(theta, phi, **kwargs): + lamb = 1.17 + R = 15.02 + epsilon = 6.55 + Pd = kwargs.get('Pd', 2.056) - base = kwargs.get('base', 'cartesian') - if base == "cartesian": - x = r * np.cos(theta) - y = r * np.sin(theta) * np.cos(phi) - z = r * np.sin(theta) * np.sin(phi) - return x, y, z - elif base == "spherical": - return r, theta, phi - raise ValueError("unknown base '{}'".format(kwargs["base"])) + R0 = 2 * R * Pd ** (-1 / epsilon) + r = R0 / (np.cos(theta) + np.sqrt(np.cos(theta) ** 2 + np.sin(theta) * np.sin(theta) * lamb ** 2)) + return coords.choice_coordinate_system(r, theta, phi, **kwargs) +def mp_nguyen2020(theta, phi, **kwargs): + def inv_cos(t): + return 2 / (1 + np.cos(t)) -_models = {"mp_shue1998": mp_shue1998, - "mp_formisano1979": mp_formisano1979, - "mp_liu2015": mp_liu2015, - "bs_formisano1979": bs_formisano1979, - "bs_jerab2005": bs_Jerab2005} + ## return an array of phi and theta in the right ranges of values + if isinstance(theta, np.ndarray) | isinstance(theta, pd.Series): + t = np.array(theta).copy() + idx = np.where(t < 0)[0] + if isinstance(phi, np.ndarray) | isinstance(phi, pd.Series): + p = np.array(phi).copy() + p[idx] = p[idx] + np.pi + else: + p = phi * np.ones(t.shape) + p[idx] = p[idx] + np.pi + else: + t = theta + p = phi + (t < 0) * np.pi + + t = np.sign(t) * t + + # coefficients + a = [10.61, + -0.150, + 0.027, + 0.207, + 1.62, + 0.558, + 0.135, + 0.0150, + -0.0839] + Pd = kwargs.get('Pd', 2.056) + Bx = kwargs.get('Bx', 0.032) + By = kwargs.get('By', -0.015) + Bz = kwargs.get('Bz', -0.001) + gamma = kwargs.get('tilt', 0.) + + B_param = [('Bx' in kwargs), ('By' in kwargs), ('Bz' in kwargs)] + if all(B_param): + Pm = (Bx ** 2 + By ** 2 + Bz ** 2) * 1e-18 / (2 * cst.mu_0) * 1e9 + elif not any(B_param): + Pm = 0.016 + else: + raise ValueError('None or all Bx, By, Bz parameters must be set') + + omega = np.sign(By) * np.arccos(Bz / np.sqrt(By ** 2 + Bz ** 2)) + P = Pd + Pm + r0 = a[0] * (1 + a[2] * np.tanh(a[3] * Bz + a[4])) * P ** (a[1]) + + alpha0 = a[5] + alpha1 = a[6] * gamma + alpha2 = a[7] * np.cos(omega) + alpha3 = a[8] * np.cos(omega) + + alpha = alpha0 + alpha1 * np.cos(p) + alpha2 * np.sin(p) ** 2 + alpha3 * np.cos(p) ** 2 + r = r0 * inv_cos(t) ** alpha + return coords.choice_coordinate_system(r, theta, phi, **kwargs) + + +def mp_shue1998_tangents(theta, phi, **kwargs): + Pd = kwargs.get("Pd", 2.056) + Bz = kwargs.get("Bz", -0.001) + + r0 = (10.22 + 1.29 * np.tanh(0.184 * (Bz + 8.14))) * Pd ** (-1. / 6.6) + a = (0.58 - 0.007 * Bz) * (1 + 0.024 * np.log(Pd)) + r = r0 * (2. / (1 + np.cos(theta))) ** a + drdt = r0 * a * (2 ** a) * np.sin(theta) / (1 + np.cos(theta)) ** (a + 1) + dxdt = drdt * np.cos(theta) - r * np.sin(theta) + dydt = drdt * np.sin(theta) * np.sin(phi) + r * np.cos(theta) * np.sin(phi) + dzdt = drdt * np.sin(theta) * np.cos(phi) + r * np.cos(theta) * np.cos(phi) + + normt = mnorm(dxdt, dydt, dzdt) + normt[normt == 0] = 1 + + dxdp = 0 + dydp = r * np.sin(theta) * np.cos(phi) + dzdp = -r * np.sin(theta) * np.sin(phi) + + normp = mnorm(dxdp, dydp, dzdp) + normp[normp == 0] = 1 + + return [dxdt / normt, dydt / normt, dzdt / normt], [dxdp / normp, dydp / normp, dzdp / normp] + + +def bs_jelinek2012_tangents(theta, phi, **kwargs): + lamb = 1.17 + R = 15.02 + epsilon = 6.55 + Pd = kwargs.get('Pd', 2.056) + + R0 = 2 * R * Pd ** (-1 / epsilon) + r = R0 / (np.cos(theta) + np.sqrt(np.cos(theta) ** 2 + (lamb * np.sin(theta)) ** 2)) + test = R0 * np.sin(theta) * ( + np.cos(theta) * (1 - lamb ** 2) / np.sqrt(np.cos(theta) ** 2 + (lamb * np.sin(theta)) ** 2)) / ( + np.cos(theta) + np.sqrt(np.cos(theta) ** 2 + (lamb * np.sin(theta)) ** 2)) ** 2 + + u0 = R0 + v0 = np.cos(theta) + np.sqrt(np.cos(theta) ** 2 + (lamb * np.sin(theta)) ** 2) + v0p = -np.sin(theta) + (np.cos(theta) * (-np.sin(theta)) + lamb ** 2 * np.sin(theta) * np.cos(theta)) / np.sqrt( + np.cos(theta) ** 2 + (lamb * np.sin(theta)) ** 2) + + drdt = -u0 * v0p / v0 ** 2 + + dxdt = drdt * np.cos(theta) - r * np.sin(theta) + dydt = drdt * np.sin(theta) * np.sin(phi) + r * np.cos(theta) * np.sin(phi) + dzdt = drdt * np.sin(theta) * np.cos(phi) + r * np.cos(theta) * np.cos(phi) + + normt = mnorm(dxdt, dydt, dzdt) + normt[normt == 0] = 1 + + dxdp = 0 + dydp = r * np.sin(theta) * np.cos(phi) + dzdp = -r * np.sin(theta) * np.sin(phi) + + normp = mnorm(dxdp, dydp, dzdp) + normp[normp == 0] = 1 + + return [dxdt / normt, dydt / normt, dzdt / normt], [dxdp / normp, dydp / normp, dzdp / normp] + + +def mp_shue1998_normal(theta, phi, **kwargs): + [dxdt, dydt, dzdt], [dxdp, dydp, dzdp] = mp_shue1998_tangents(theta, phi, **kwargs) + + pvx = dzdt * dydp - dydt * dzdp + pvy = dxdt * dzdp - dzdt * dxdp + pvz = dydt * dxdp - dxdt * dydp + + norm = mnorm(pvx, pvy, pvz) + + pvx[norm == 0] = 1 + norm[norm == 0] = 1 + + return (pvx / norm, pvy / norm, pvz / norm) + + +def bs_jelinek2012_normal(theta, phi, **kwargs): + [dxdt, dydt, dzdt], [dxdp, dydp, dzdp] = bs_jelinek2012_tangents(theta, phi, **kwargs) + pvx = dzdt * dydp - dydt * dzdp + pvy = dxdt * dzdp - dzdt * dxdp + pvz = dydt * dxdp - dxdt * dydp + + norm = mnorm(pvx, pvy, pvz) + + pvx[norm == 0] = 1 + norm[norm == 0] = 1 + + return (pvx / norm, pvy / norm, pvz / norm) + + +_models = { "mp_shue1998": mp_shue1998, + "mp_shue1997": mp_shue1997, + "mp_formisano1979": mp_formisano1979, + "mp_liu2015" : mp_liu2015, + "mp_jelinek2012" : mp_jelinek2012, + "mp_lin2010" : mp_lin2010, + "mp_nguyen2020" : mp_nguyen2020, + "bs_formisano1979": bs_formisano1979, + "bs_jerab2005": bs_Jerab2005, + "bs_jelinek2012": bs_jelinek2012} +_tangents = {"mp_shue1998" : mp_shue1998_tangents, + "bs_jelinek2012" : bs_jelinek2012_tangents, + "mp_shue1997": None, + "mp_formisano1979": None, + "mp_liu2015": None, + "mp_jelinek2012": None, + "mp_lin2010": None, + "mp_nguyen2020": None, + "bs_formisano1979": None, + "bs_jerab2005": None} + +_normal = {"mp_shue1998" : mp_shue1998_normal, + "bs_jelinek2012" : bs_jelinek2012_normal, + "mp_shue1997": None, + "mp_formisano1979": None, + "mp_liu2015": None, + "mp_jelinek2012": None, + "mp_lin2010": None, + "mp_nguyen2020": None, + "bs_formisano1979": None, + "bs_jerab2005": None + } def available(model): if model == "magnetopause": @@ -389,23 +604,24 @@ def available(model): raise ValueError("invalid model type") + + def _interest_points(model, **kwargs): dup = kwargs.copy() - dup["base"] = "cartesian" + dup["coord_sys"] = "cartesian" x = model(0, 0, **dup)[0] - y = model(np.pi / 2, 0, **dup)[1] + y = ( abs(model(np.pi / 2, np.pi/2, **dup)[1]) + abs(model(np.pi / 2, -np.pi/2, **dup)[1]))/2 xf = x - y ** 2 / (4 * x) return x, y, xf - -def _parabolic_approx(theta, phi, x, xf, **kwargs): - theta, phi = _checking_angles(theta, phi) +def _parabolic_approx(theta, phi, x, xf, **kwargs): # x= nose boundary , xf= focal point K = x - xf a = np.sin(theta) ** 2 b = 4 * K * np.cos(theta) c = -4 * K * x - r = resolve_poly2(a, b, c, 0) - return coords.base_choice(kwargs.get("base", "cartesian"), r, theta, phi) + r = resolve_poly2_real_roots(a, b, c)[0] + return coords.choice_coordinate_system(r, theta, phi, **kwargs) + def check_parabconfoc(func): @@ -423,7 +639,7 @@ class Magnetosheath: def __init__(self, **kwargs): kwargs["magnetopause"] = kwargs.get("magnetopause", "mp_shue1998") - kwargs["bow_shock"] = kwargs.get("bow_shock", "bs_jerab2005") + kwargs["bow_shock"] = kwargs.get("bow_shock", "bs_jelinek2012") if not kwargs["magnetopause"].startswith("mp_") \ or not kwargs["bow_shock"].startswith("bs_"): @@ -433,20 +649,45 @@ def __init__(self, **kwargs): self._bow_shock = _models[kwargs["bow_shock"]] self.model_magnetopause = kwargs["magnetopause"] self.model_bow_shock = kwargs["bow_shock"] + self._tangents_magnetopause = _tangents[kwargs["magnetopause"]] + self._normal_magnetopause = _normal[kwargs["magnetopause"]] + self._tangents_bow_shock = _tangents[kwargs["bow_shock"]] + self._normal_bow_shock = _normal[kwargs["bow_shock"]] + + @check_parabconfoc def magnetopause(self, theta, phi, **kwargs): if kwargs["parabolic"]: return self._parabolize(theta, phi, **kwargs)[0] else: - return self._magnetopause(theta, phi, **kwargs) + if kwargs.get('normal', False) or kwargs.get('tangents', False) : + ret_vectors = [] + ret_vectors.append(self._magnetopause(theta, phi, **kwargs)) + if kwargs.get('normal', False): + ret_vectors.append(self._normal_magnetopause(listify(theta), listify(phi), **kwargs)) + if kwargs.get('tangents', False): + ret_vectors.append(self._tangents_magnetopause(listify(theta), listify(phi), **kwargs)) + return ret_vectors + else : + return self._magnetopause(theta, phi, **kwargs) @check_parabconfoc def bow_shock(self, theta, phi, **kwargs): if kwargs["parabolic"]: return self._parabolize(theta, phi, **kwargs)[1] else: - return self._bow_shock(theta, phi, **kwargs) + if kwargs.get('normal', False) or kwargs.get('tangents', False): + ret_vectors = [] + ret_vectors.append(self._bow_shock(theta, phi, **kwargs)) + if kwargs.get('normal',False): + ret_vectors.append(self._normal_bow_shock(listify(theta), listify(phi), **kwargs)) + if kwargs.get('tangents',False): + ret_vectors.append(self._tangents_bow_shock(listify(theta), listify(phi), **kwargs)) + return ret_vectors + else : + return self._bow_shock(theta, phi, **kwargs) + @check_parabconfoc def boundaries(self, theta, phi, **kwargs): @@ -456,6 +697,20 @@ def boundaries(self, theta, phi, **kwargs): return self._magnetopause(theta, phi, **kwargs), \ self._bow_shock(theta, phi, **kwargs) + def tangents_magnetopause(self, theta, phi, **kwargs): + return self._tangents_magnetopause(listify(theta), listify(phi), **kwargs) + + def normal_magnetopause(self, theta, phi, **kwargs): + return self._normal_magnetopause(listify(theta), listify(phi), **kwargs) + + + def tangents_bow_shock(self, theta, phi,**kwargs): + return self._tangents_bow_shock(listify(theta), listify(phi), **kwargs) + + def normal_bow_shock(self, theta, phi,**kwargs): + return self._normal_bow_shock(listify(theta), listify(phi), **kwargs) + + def _parabolize(self, theta, phi, **kwargs): xmp, y, xfmp = _interest_points(self._magnetopause, **kwargs) xbs, y, xfbs = _interest_points(self._bow_shock, **kwargs) diff --git a/space/plot/planet_env.py b/space/plot/planet_env.py index 026494d..979aae4 100644 --- a/space/plot/planet_env.py +++ b/space/plot/planet_env.py @@ -1,63 +1,232 @@ import matplotlib.pyplot as plt import numpy as np +from scipy.optimize import root_scalar, root + + + +def _make_figure(**kwargs): + ncols = np.sum([1 for arg in ['x_slice', 'y_slice', 'z_slice'] if arg in kwargs]) + if ncols == 0: + ncols = 1 + figsize = kwargs.get('figsize', (5 * ncols, 4.5)) + fig, ax = plt.subplots(nrows=1, ncols=ncols, figsize=figsize, constrained_layout=True) + if 'color_background' in kwargs: + if isinstance(ax,np.ndarray) : + for i in range(len(ax)) : + ax[i].set_facecolor('xkcd:{}'.format(kwargs['color_background'])) + else : + ax.set_facecolor('xkcd:{}'.format(kwargs['color_background'])) + + return fig, ax + + +def _set_infos_earth_env(fig, ax, **kwargs): + if isinstance(ax, np.ndarray) is False: + ax = [ax] + c = 0 + if 'z_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('x_label', 'X (Re)')) + ax[c].set_ylabel(kwargs.get('y_label', 'Y (Re)')) + ax[c].set_xlim(kwargs.get('x_lim', (-20, 15))) + ax[c].set_ylim(kwargs.get('y_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + c += 1 + if 'y_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('x_label', 'X (Re)')) + ax[c].set_ylabel(kwargs.get('z_label', 'Z (Re)')) + ax[c].set_xlim(kwargs.get('x_lim', (-20, 15))) + ax[c].set_ylim(kwargs.get('z_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + c += 1 + if 'x_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('y_label', 'Y (Re)')) + ax[c].set_ylabel(kwargs.get('z_label', 'Z (Re)')) + ax[c].set_xlim(kwargs.get('y_lim', (-30, 30))) + ax[c].set_ylim(kwargs.get('z_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + if 'title' in kwargs: + fig.suptitle(kwargs.get('title')) + return fig, ax + + +def _make_figure(**kwargs): + ncols = np.sum([1 for arg in ['x_slice', 'y_slice', 'z_slice'] if arg in kwargs]) + if ncols == 0: + ncols = 1 + figsize = kwargs.get('figsize', (5 * ncols, 4.5)) + fig, ax = plt.subplots(nrows=1, ncols=ncols, figsize=figsize, constrained_layout=True) + return fig, ax + + +def _set_infos_earth_env(fig, ax, **kwargs): + if isinstance(ax, np.ndarray) is False: + ax = [ax] + c = 0 + if 'z_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('x_label', 'X (Re)')) + ax[c].set_ylabel(kwargs.get('y_label', 'Y (Re)')) + ax[c].set_xlim(kwargs.get('x_lim', (-20, 15))) + ax[c].set_ylim(kwargs.get('y_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + c += 1 + if 'y_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('x_label', 'X (Re)')) + ax[c].set_ylabel(kwargs.get('z_label', 'Z (Re)')) + ax[c].set_xlim(kwargs.get('x_lim', (-20, 15))) + ax[c].set_ylim(kwargs.get('z_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + c += 1 + if 'x_slice' in kwargs: + ax[c].set_xlabel(kwargs.get('y_label', 'Y (Re)')) + ax[c].set_ylabel(kwargs.get('z_label', 'Z (Re)')) + ax[c].set_xlim(kwargs.get('y_lim', (-30, 30))) + ax[c].set_ylim(kwargs.get('z_lim', (-30, 30))) + ax[c].axhline(0, color='k', ls='dotted', alpha=0.4) + ax[c].axvline(0, color='k', ls='dotted', alpha=0.4) + if 'title' in kwargs: + fig.suptitle(kwargs.get('title')) + return fig, ax + + +def _find_theta_for_x_slice(boundary_model, phi, **kwargs): + def eq(t, x): + return boundary_model(t, x[0], coord_sys='spherical')[0] * np.cos(t) - x[1] + + n_pts = kwargs.get('n_pts', 300) + xs = np.ones(n_pts) * kwargs.get('x_slice', 0) + x0 = np.ones( + n_pts) * np.pi / 3 # initial guess of theta found empiricly, will enable to find a positive value of theta to make the x_slice with phi between [0;2pi] + return root(eq, args=[phi, xs], x0=x0, jac=False, method='lm').x + + +def _find_phi_for_z_slice(boundary_model, theta, **kwargs): + def eq(p, x): + if (p <= np.pi) & ( + p >= 0): # cos(phi) must be positive because y=r*sin(theta)*cos(phi) and it's easier to get z positive or negative with theta than with phi (multiple roots with cos(roots) positive or negative) + return boundary_model(x[0], p, coord_sys='cartesian', **kwargs)[2] - x[1] + else: + return 1000 + + xs = kwargs.get('z_slice', 0) + phi = np.array([root_scalar(eq, args=[t, xs], x0=np.pi / 3, x1=np.pi / 4, method='secant').root for t in + theta]) # x0, x1 initial guesses of phi that will enable to find a cos(phi) positive. -sign(theta) will allow to have an initial guess closer to the wanted root. Found empirically + return phi + + +def _find_phi_for_y_slice(boundary_model, theta, **kwargs): + def eq(p, x): + if (p <= np.pi / 2) & ( + p >= -np.pi / 2): # sin(phi) must be positive because z=r*sin(theta)*sin(phi) and it's simplier to get z positive or negative with theta than with phi (multiple roots with sin(roots) positive or negative) + return boundary_model(x[0], p, coord_sys='cartesian', **kwargs)[1] - x[1] + else: + return 1000 + + xs = kwargs.get('y_slice', 0) + phi = np.array( + [root_scalar(eq, args=[t, xs], x0=-np.sign(t) * np.pi / 3, x1=-np.sign(t) * np.pi / 4, method='secant').root for + t in theta]) # x0, x1 initial guesses of phi that will enable to find a sin(phi) positive, found empirically + return phi + + +def _find_theta_lim_y_slice(boundary_model, + **kwargs): # find the smallest value of theta allowed in the considerate y_slice + def eq(t, a): + return boundary_model(t, a[0], coord_sys='cartesian', **kwargs)[1] - a[1] + + phi_p = np.pi / 2 # phi for y positive : r*cos(theta) - y_slice = 0 + phi_n = -np.pi / 2 # phi for y negative : -r*cos(theta) - y_slice = 0 + t_lim1 = root_scalar(eq, args=[phi_p, kwargs['y_slice']], x0=0.01, x1=np.pi / 3, + method='secant').root # x0, x1 initial guesses of theta that will enable to find the smallest theta giving an y positive + t_lim2 = root_scalar(eq, args=[phi_n, kwargs['y_slice']], x0=0.01, x1=np.pi / 3, + method='secant').root # x0, x1 initial guesses of theta that will enable to find the smallest theta giving an y negative + return t_lim1, t_lim2 + + +def _find_theta_lim_z_slice(boundary_model, + **kwargs): # find the smallest value of theta allowed in the considerate z_slice + def eq(t, a): + return boundary_model(t, a[0], coord_sys='cartesian', **kwargs)[2] - a[1] + + phi_p = 0 # phi for z positive : r*cos(theta) - z_slice = 0 + phi_n = np.pi # phi for z negative : -r*cos(theta) - z_slice = 0 + t_lim1 = root_scalar(eq, args=[phi_p, kwargs['z_slice']], x0=0.01, x1=np.pi / 3, + method='secant').root # x0, x1 initial guesses of theta that will enable to find the smallest theta giving a z positive + t_lim2 = root_scalar(eq, args=[phi_n, kwargs['z_slice']], x0=0.01, x1=np.pi / 3, + method='secant').root # x0, x1 initial guesses of theta that will enable to find the smallest theta giving a z negative + return t_lim1, t_lim2 + + +def make_theta_and_phi(boundary_model, fct_theta, fct_phi, **kwargs): + n_pts = kwargs.get('n_pts', 300) + if fct_phi is not None: + theta_lim = np.array(fct_theta(boundary_model, **kwargs)) + theta_p = np.linspace(0.8 * np.pi, max(theta_lim), int(np.ceil(n_pts / 2))) + theta_n = np.linspace(min(theta_lim), -0.8 * np.pi, int(np.floor(n_pts / 2))) + theta = np.concatenate((theta_p, theta_n), axis=0) + phi = fct_phi(boundary_model, theta, **kwargs) + + else: + phi = np.linspace(0, 2 * np.pi, n_pts) + theta = fct_theta(boundary_model, phi, **kwargs) + return theta, phi + + +def check_validity_of_asked_slice(boundary_model, **kwargs): + theta0 = np.pi / 2 # theta to have x=0 (terminator) + if 'z_slice' in kwargs: + range_phi = np.linspace(-np.pi / 4, np.pi / 4, 100) # range of phi containing the greatest value of z for x=0 + z_max = np.max(boundary_model(theta0, range_phi, **kwargs)[2]) + z_min = np.min(boundary_model(-theta0, range_phi, **kwargs)[2]) + if (kwargs['z_slice'] > z_max) | (kwargs['z_slice'] < z_min): + raise ValueError(f"z_slice value must be between [{round(z_min, 2)},{round(z_max, 2)}] to be able to plot the boudary") + + if 'y_slice' in kwargs: + range_phi = np.linspace(np.pi / 4, 3 * np.pi / 4, 100) # range of phi containing the greatest value of y for x=0 + y_max = np.max(boundary_model(theta0, range_phi, **kwargs)[1]) + y_min = np.min(boundary_model(-theta0, range_phi, **kwargs)[1]) + if (kwargs['y_slice'] > y_max) | (kwargs['y_slice'] < y_min): + raise ValueError(f" y_slice value must be between [{round(y_min, 2)},{round(y_max, 2)}] to be able to plot the boundary") + + + +def plot_boundary(boundary_model, fig, ax, **kwargs): + c = 0 + check_validity_of_asked_slice(boundary_model, **kwargs) + if 'z_slice' in kwargs: # cut z axis to plot xy plane + theta, phi = make_theta_and_phi(boundary_model, _find_theta_lim_z_slice, _find_phi_for_z_slice, **kwargs) + x, y = boundary_model(theta, phi, **kwargs)[:2] + ax[c].plot(x, y, kwargs['style'], alpha= kwargs['alpha']) + c += 1 + + if 'y_slice' in kwargs: # cut y axis to plot xz plane + theta, phi = make_theta_and_phi(boundary_model, _find_theta_lim_y_slice, _find_phi_for_y_slice, **kwargs) + x, z = boundary_model(theta, phi, **kwargs)[::2] + ax[c].plot(x, z, kwargs['style'], alpha= kwargs['alpha']) + c += 1 + + if 'x_slice' in kwargs: # cut x axis to plot yz plane + theta, phi = make_theta_and_phi(boundary_model, _find_theta_for_x_slice, None, **kwargs) + y, z = boundary_model(theta, phi, **kwargs)[1:] + ax[c].plot(y, z, kwargs['style'], alpha= kwargs['alpha']) + + return fig, ax + + +def layout_earth_env(magnetosheath, **kwargs): + fig, ax = _make_figure(**kwargs) + fig, ax = _set_infos_earth_env(fig, ax, **kwargs) + if kwargs.get('magnetopause', True) is True: + kwargs['style'] = kwargs.get('style_mp', '-k') + kwargs['alpha'] = kwargs.get('alpha_mp', 1) + fig, ax = plot_boundary(magnetosheath.magnetopause, fig, ax, **kwargs) + if kwargs.get('bow_shock', True) is True: + kwargs['style'] = kwargs.get('style_bs', '-k') + kwargs['alpha'] = kwargs.get('alpha_bs', 1) + fig, ax = plot_boundary(magnetosheath.bow_shock, fig, ax, **kwargs) + return fig, ax - -def layout_EarthEnv_3planes(**kwargs): - # figsize=(15,4.5), - # to be fixed! - xlim = (-30, 30) - ylim = (-30, 30) - zlim = (-30, 30) - alpha = 0.5 - - figsize = kwargs.get("kwargs", (15, 4.5)) # kwargs?! - - fig, (ax0, ax1, ax2) = plt.subplots(ncols=3, figsize=figsize, constrained_layout=True) - - ax0.set_xlabel('X (Re)') - ax0.set_ylabel('Y (Re)') - ax1.set_xlabel('X (Re)') - ax1.set_ylabel('Z (Re)') - ax2.set_xlabel('Y (Re)') - ax2.set_ylabel('Z (Re)') - ax0.axhline(0, color='k', ls='dotted', alpha=alpha) - ax0.axvline(0, color='k', ls='dotted', alpha=alpha) - ax1.axhline(0, color='k', ls='dotted', alpha=alpha) - ax1.axvline(0, color='k', ls='dotted', alpha=alpha) - ax2.axhline(0, color='k', ls='dotted', alpha=alpha) - ax2.axvline(0, color='k', ls='dotted', alpha=alpha) - ax0.set_xlim(xlim) - ax0.set_ylim(ylim) - ax1.set_xlim(xlim) - ax1.set_ylim(zlim) - ax2.set_xlim(ylim) - ax2.set_ylim(zlim) - - return ax0, ax1, ax2 - - -# plot_boundaries(MP, BS, slice_x=22, slice_y=24, slice_z=0) - - -def make_YZ_plan(pos): - a = np.linspace(0, 2 * np.pi, 100) - r = abs(pos[(pos.X ** 2).argmin(): (pos.X ** 2).argmin() + 1].Y.values) - return r * np.cos(a), r * np.sin(a) - - -def plot_boundaries(MP, BS, **kwargs): - style = kwargs.get("style", ['--k', '--k']) - alpha = kwargs.get("alpha", 0.6) - axes = kwargs.get("axes", layout_EarthEnv_3planes(**kwargs)) - - if "slice_x" in kwargs: - axes[0].plot() - - axes[0].plot(MP.X, MP.Y, style[0], alpha=alpha) - axes[0].plot(BS.X, BS.Y, style[1], alpha=alpha) - - axes[1].plot(MP.X, MP.Z, style[0], alpha=alpha) - axes[1].plot(BS.X, BS.Z, style[1], alpha=alpha) - - axes[2].plot(make_YZ_plan(MP)[0], make_YZ_plan(MP)[1], style[0], alpha=alpha) - axes[2].plot(make_YZ_plan(BS)[0], make_YZ_plan(BS)[1], style[1], alpha=alpha) diff --git a/space/smath.py b/space/smath.py index afc8d24..1031ab1 100644 --- a/space/smath.py +++ b/space/smath.py @@ -4,11 +4,41 @@ def norm(u, v, w): return np.sqrt(u**2 + v**2 + w**2) -def resolve_poly2(a, b, c, roots=None): - def fun(a, b, c): - if roots is None: - return np.roots([a, b, c]) - return np.roots([a,b,c])[roots] - - vfun = np.vectorize(fun, otypes=(np.ndarray,)) - return vfun(a, b, c) +def resolve_poly1(a, b, epsilon=1e-7): + + if isinstance(a, np.ndarray) | isinstance(a, pd.Series): + b = np.ones(len(a)) * b + r = np.zeros(len(a)) + r[(abs(a) >= epsilon)] = -b[ (abs(a) >= epsilon)] / a[ (abs(a) >= epsilon)] + + else : + if abs(a) >= epsilon : + r=-b/a + else : + r=0 + return r + +def resolve_poly2_real_roots(a, b, c, epsilon=1e-7): + delta = b ** 2 - 4 * a * c + if isinstance(delta, np.ndarray) | isinstance(delta, pd.Series): + delta[delta < 0] = np.nan + a = np.ones(len(delta)) * a + b = np.ones(len(delta)) * b + c = np.ones(len(delta)) * c + r1 = np.zeros(len(delta)) + r2 = np.zeros(len(delta)) + r1[abs(a) >= epsilon] = (-b[abs(a) >= epsilon] + np.sqrt(delta[abs(a) >= epsilon])) / (2 * a[abs(a) >= epsilon]) + r2[abs(a) >= epsilon] = (-b[abs(a) >= epsilon] - np.sqrt(delta[abs(a) >= epsilon])) / (2 * a[abs(a) >= epsilon]) + r1[abs(a) <= epsilon ] = r2[(abs(a) <= epsilon)] = resolve_poly1(b[abs(a) <= epsilon ], c[abs(a) <= epsilon ] , epsilon=epsilon) + + else: + if delta < 0: + r1 = r2 = np.nan + elif (abs(a) <= epsilon): + r1 = r2 = resolve_poly1(b, c, epsilon=epsilon) + else : + r1 = (-b + np.sqrt(delta)) / (2 * a) + r2 = (-b - np.sqrt(delta)) / (2 * a) + + + return r1, r2 diff --git a/space/utils.py b/space/utils.py index 09094c0..b0ec8a8 100644 --- a/space/utils.py +++ b/space/utils.py @@ -1,4 +1,5 @@ import numpy as np +import pandas as pd def listify(arg): @@ -6,22 +7,37 @@ def listify(arg): return [arg] else: return arg - + + def none_iterable(*args): """ return true if none of the arguments are either lists or tuples """ - return all([not isinstance(arg, list) and not isinstance(arg, tuple) and not isinstance(arg, np.ndarray) for arg in args]) + return all([not isinstance(arg, list) and not isinstance(arg, tuple) and not isinstance(arg,np.ndarray) and not isinstance(arg, pd.Series) for arg in args]) + +def index_isin(df1, df2): + if isinstance(df1, list): + return [df[df.index.isin(df2.index)] for df in df1] + else: + return df1[df1.index.isin(df2.index)] + -def index_isin(df1,df2): - return df1[df1.index.isin(df2.index)] - - def same_index(a,b): a = index_isin(a,b) b = index_isin(b,a) return a,b -def eliminateNoneValidValues(df): +def eliminate_none_valid_values(df): return df.replace([np.inf, -np.inf], np.nan).dropna() +def add_columns_to_df(dataframe,var,name): + df = dataframe.copy() + for n,v in zip(name,var): + df[n]=v + return df + +def select_data_with_condition(data,cond): + if isinstance(data, list): + return [d[cond] for d in data] + else : + return data[cond] diff --git a/tests/test_space.py b/tests/test_space.py index 6e9d406..d75c388 100644 --- a/tests/test_space.py +++ b/tests/test_space.py @@ -15,10 +15,10 @@ class TestMagnetosheath(unittest.TestCase): def setUp(self): """Set up test fixtures, if any.""" - self.typical_msh = Magnetosheath(magnetopause="mp_formisano1979", bow_shock="bs_formisano1979") - theta = np.arange(0, np.pi/2, 0.1*np.pi) - self.typical_mp_sph, self.typical_bs_sph = self.typical_msh.boundaries(theta, 0, base = "spherical") - self.typical_mp_cart, self.typical_bs_cart = self.typical_msh.boundaries(theta, 0, base = "cartesian") + self.typical_msh = Magnetosheath(magnetopause="mp_shue1998", bow_shock="bs_jelinek2012") + theta = np.arange(0, np.pi/2, 0.01*np.pi) + self.typical_mp_sph, self.typical_bs_sph = self.typical_msh.boundaries(theta, 0, coord_sys = "spherical") + self.typical_mp_cart, self.typical_bs_cart = self.typical_msh.boundaries(theta, 0, coord_sys = "cartesian") def tearDown(self): """Tear down test fixtures, if any."""