diff --git a/setup.py b/setup.py index e5fa20a..a59c67d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open('HISTORY.rst') as history_file: history = history_file.read() -requirements = ['pandas', 'numpy'] +requirements = ['pandas', 'numpy', 'matplotlib'] setup_requirements = [] diff --git a/space/coordinates/coordinates.py b/space/coordinates/coordinates.py index b9c9849..ac1bed3 100644 --- a/space/coordinates/coordinates.py +++ b/space/coordinates/coordinates.py @@ -10,7 +10,7 @@ def spherical_to_cartesian(R, theta, phi): def cartesian_to_spherical(X,Y,Z): r = np.sqrt(X**2+Y**2+Z**2) - theta = np.arccos(X/R) + theta = np.arccos(X/r) phi = np.arctan2(Z,Y) return r,theta,phi diff --git a/space/models/planetary.py b/space/models/planetary.py index 344218c..6674aac 100644 --- a/space/models/planetary.py +++ b/space/models/planetary.py @@ -5,15 +5,14 @@ sys.path.append('.') from .. import utils from ..coordinates import coordinates as coords +from ..smath import resolve_poly2 -def checking_angles(theta, phi): - theta = utils.listify(theta) - phi = utils.listify(phi) - if (len(np.shape(theta)) == 1) & (len(np.shape(phi)) == 1) & (len(theta) > 1) & (len(phi) > 1): - theta, phi = np.meshgrid(theta, phi) - print('theta and phi are both 1D array : applying meshgrid to do a 3D boundaries') +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 @@ -33,7 +32,7 @@ def _formisano1979(theta, phi, **kwargs): def formisano1979(theta, phi, **kwargs): ''' - Formisano 2005 magnetopause model. Give the default position of the magnetopause. + Formisano 1979 magnetopause model. Give the default position of the magnetopause. function's arguments : - theta : angle in radiant, can be int, float or array (1D or 2D) - phi : angle in radiant, can be int, float or array (1D or 2D) @@ -41,7 +40,6 @@ def formisano1979(theta, phi, **kwargs): - boundary : "magnetopause", "bow_shock" - base : 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 the boundary is wanted one of the two angle must be an array and the other one must be @@ -59,17 +57,25 @@ def formisano1979(theta, phi, **kwargs): else: raise ValueError("boundary: {} not allowed".format(kwargs["boundary"])) - theta, phi = checking_angles(theta, phi) + 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) + return coords.spherical_to_cartesian(r, theta, phi) elif base == "spherical": return r, theta, phi raise ValueError("unknown base '{}'".format(kwargs["base"])) +def mp_formisano1979(theta, phi, **kwargs): + return formisano1979(theta, phi, boundary="magnetopause", **kwargs) + +def bs_formisano1979(theta, phi, **kwargs): + return formisano1979(theta, phi, boundary="bow_shock", **kwargs) + + + def Fairfield1971(x, args): @@ -105,56 +111,11 @@ def Fairfield1971(x, args): return pos.dropna() -def Formisano1979(x, args): - - ''' - Formisano 1979 : Magnetopause and Bow shock models. Give positions of the boudaries in plans (XY) with Z=0 and (XZ) with Y=0. - function's arguments : - - x : X axis (array) in Re (earth radii) - - args : coefficients Aij are determined from many boundary crossings and they depend on upstream conditions. - - --> Default parameter for the bow shock and the magnetopause respectively are : - default_bs_formisano = [0.52,1,1.05,0.13,-0.16,-0.08,47.53,-0.42,0.67,-613] - default_mp_formisano = [0.65,1,1.16,0.03,-0.28,-0.11,21.41,0.46,-0.36,-221] - - return : DataFrame (Pandas) with the position (X,Y,Z) in Re of the wanted boudary to plot (XY) and (XZ) plans. - ''' - - - a11,a22,a33,a12,a13,a23,a14,a24,a34,a44 = args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8],args[9] - - a_y = a22 - b_y = a12*x + a24 - c_y = a11*x**2 + a14*x + a44 - - delta_y =(b_y**2-4*a_y*c_y) - - - ym = (-b_y - np.sqrt(delta_y))/(2*a_y) - yp = (-b_y + np.sqrt(delta_y))/(2*a_y) - - a_z = a33 - b_z = a13*x + a34 - c_z = a11*x**2 + a14*x + a44 - - delta_z =(b_z**2-4*a_z*c_z) - - zm = (-b_z - np.sqrt(delta_z))/(2*a_z) - zp = (-b_z + np.sqrt(delta_z))/(2*a_z) - - - pos=pd.DataFrame({'X' : np.concatenate([x, x[::-1]]), - 'Y' : np.concatenate([yp, ym[::-1]]), - 'Z' : np.concatenate([zp, zm[::-1]]),}) - - return pos.dropna() - - -def BS_Jerab2005( Np, V, Ma, B, gamma=2.15 ): +def bs_Jerab2005( Np, V, Ma, B, gamma=2.15 ): ''' Jerab 2005 Bow shock model. Give positions of the box shock in plans (XY) with Z=0 and (XZ) with Y=0 as a function of the upstream solar wind. @@ -215,7 +176,7 @@ def make_Rav(theta,phi): return pos.sort_values('Y') -def shue1998(theta, phi, **kwargs): +def mp_shue1998(theta, phi, **kwargs): ''' Shue 1998 Magnetopause model. @@ -351,3 +312,60 @@ def quad(i, s): return r+Q +_models = {"mp_shue": mp_shue1998, + "mp_formisano1979": mp_formisano1979, + "bs_formisano1979": bs_formisano1979, + "bs_jerab": bs_Jerab2005} + + +class Magnetosheath: + def __init__(self, **kwargs): + self.magnetopause = _models[kwargs.get("magnetopause", "shue")] + self.bow_shock = _models[kwargs.get("bow_shock", "jerab")] + + def boundaries(self, theta, phi, **kwargs): + return self.magnetopause(theta, phi, **kwargs), self.bow_shock(theta, phi, **kwargs) + + +def _interest_points(model, **kwargs): + dup = kwargs.copy() + dup["base"] = "cartesian" + x = model(0, 0, **dup)[0] + y = model(np.pi / 2, 0, **dup)[1] + xf = x - y ** 2 / (4 * x) + return x, y, xf + + +def _parabolic_approx(theta, phi, x, xf, **kwargs): + theta, phi = _checking_angles(theta, phi) + 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.BaseChoice(kwargs.get("base", "cartesian"), r, theta, phi) + + +class ParabolicMagnetosheath: + def __init__(self, **kwargs): + self._magnetopause = _models[kwargs.get("magnetopause", "shue")] + self._bow_shock = _models[kwargs.get("bow_shock", "jerab")] + + def magnetopause(self, theta, phi, **kwargs): + return self._parabolize(theta, phi, **kwargs)[0] + + def bow_shock(self, theta, phi, **kwargs): + return self._parabolize(theta, phi, **kwargs)[1] + + def _parabolize(self, theta, phi, **kwargs): + xmp, y, xfmp = _interest_points(self._magnetopause, **kwargs) + xbs, y, xfbs = _interest_points(self._bow_shock, **kwargs) + if kwargs.get("confocal", False) is True: + xfmp = xmp / 2 + xfbs = xmp / 2 + mp_coords = _parabolic_approx(theta, phi, xmp, xfmp, **kwargs) + bs_coords = _parabolic_approx(theta, phi, xbs, xfbs, **kwargs) + return mp_coords, bs_coords + + def boundaries(self, theta, phi, **kwargs): + return self._parabolize(theta, phi, **kwargs) diff --git a/space/plot/planet_env.py b/space/plot/planet_env.py new file mode 100644 index 0000000..5810ffb --- /dev/null +++ b/space/plot/planet_env.py @@ -0,0 +1,59 @@ + +import matplotlib.pyplot as plt + +def layout_EarthEnv_3planes(**kwargs): + #figsize=(15,4.5),xlim=(-30,30),ylim=(-30,30),zlim =(-30,30),alpha=0.5): + + figsize = kwargs.get("kwargs", (15, 4.5)) + + 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_boudaries(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 new file mode 100644 index 0000000..20352ad --- /dev/null +++ b/space/smath.py @@ -0,0 +1,28 @@ +import numpy as np + +def norm(u, v, w): + return np.sqrt(u**2 + v**2 + w**2) + + +def resolve_poly2(a, b, c): + if isinstance(a, np.ndarray): + r1 = np.zeros_like(a) + r2 = np.zeros_like(a) + a_null = np.where(np.abs(a) < 1e-6)[0] + + delta = b ** 2 - 4 * a * c + np.testing.assert_array_less(-delta, 0) + + r1, r2 = (-b + np.sqrt(delta)) / (2 * a), (-b + np.sqrt(delta)) / (2 * a) + if isinstance(c, np.ndarray): + c = c[a_null] + if isinstance(b, np.ndarray): + b = b[a_null] + r1[a_null] = r2[a_null] = -c / b + else: + delta = b ** 2 - 4 * a * c + np.testing.assert_array_less(-delta, 0) + r1, r2 = (-b + np.sqrt(delta)) / (2 * a), (-b - np.sqrt(delta)) / (2 * a) + if np.abs(a) < 1e-6: + r1 = r2 = -c / b + return r1, r2