metablate 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metablate/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ '''
2
+ # Meteoroid Ablation Models
3
+ '''
4
+
5
+ from .models import *
6
+
7
+ from . import physics
8
+ from . import models
9
+ from . import atmosphere
10
+ from . import material
11
+
12
+ from .atmosphere import Atmosphere
13
+ from .ablation_model import AblationModel
14
+ from .ode import ScipyODESolve
15
+
16
+ from .version import __version__
@@ -0,0 +1,40 @@
1
+ """The basic class structures for implementing a ablation model.
2
+
3
+ """
4
+
5
+ from abc import abstractmethod
6
+ import logging
7
+ from configparser import ConfigParser
8
+ from typing import Union
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AblationModel:
15
+ """Base-class for ablation models
16
+
17
+ The `run` method is expected to return the results from the ablation modelling
18
+ """
19
+
20
+ DEFAULT_CONFIG = {}
21
+
22
+ def __init__(self, config: Union[dict, str, Path, ConfigParser]):
23
+ self.config = ConfigParser(allow_no_value=True)
24
+ self.config.read_dict(self.DEFAULT_CONFIG)
25
+
26
+ if isinstance(config, ConfigParser):
27
+ for section in config.sections():
28
+ config.read_dict({
29
+ section: {key: val for key, val in config.items(section=section)}
30
+ })
31
+ elif isinstance(config, dict):
32
+ self.config.read_dict(config)
33
+ elif isinstance(config, (str, Path)):
34
+ self.config.read_file(config)
35
+ else:
36
+ raise TypeError(f"Unexpected config type: {type(config)}")
37
+
38
+ @abstractmethod
39
+ def run(self, *args, **kwargs):
40
+ pass
@@ -0,0 +1,8 @@
1
+ '''
2
+ Meteoroid Ablation Models - Atmospheric models
3
+ ===============================================
4
+
5
+ '''
6
+ from .atmosphere import Atmosphere
7
+ from .atm_msise00 import AtmMSISE00
8
+ from .atm_pymsis import AtmPymsis
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python
2
+
3
+ """msis00
4
+
5
+ """
6
+ import scipy.constants
7
+ from .atmosphere import Atmosphere
8
+ import numpy.typing as npt
9
+
10
+ try:
11
+ import msise00
12
+ except ImportError:
13
+ msise00 = None
14
+
15
+
16
+ class AtmMSISE00(Atmosphere):
17
+ def __init__(self):
18
+ super().__init__(
19
+ supported_species=["He", "O", "N2", "O2", "Ar", "H", "N"],
20
+ )
21
+
22
+ def density(
23
+ self,
24
+ time: npt.ArrayLike,
25
+ lat: npt.ArrayLike,
26
+ lon: npt.ArrayLike,
27
+ alt: npt.ArrayLike,
28
+ f107: float = 80.0,
29
+ f107s: float = 80.0,
30
+ Ap: float = 4.0,
31
+ mass_densities: bool = False,
32
+ **kwargs
33
+ ):
34
+ """TODO: Write docstring
35
+
36
+ returns density in [m^-3]
37
+
38
+ """
39
+ if msise00 is None:
40
+ raise ImportError("msis00 is not installed")
41
+
42
+ result = msise00.run(
43
+ time=time,
44
+ altkm=alt * 1e-3,
45
+ glat=lat,
46
+ glon=lon,
47
+ indices=dict(
48
+ f107=f107,
49
+ f107s=f107s,
50
+ Ap=Ap,
51
+ ),
52
+ **kwargs
53
+ )
54
+ result["alt_km"] = result["alt_km"]*1e3
55
+ result = result.rename({
56
+ "alt_km": "alt",
57
+ "Tn": "Temperature",
58
+ })
59
+ result = result.transpose("time", "lon", "lat", "alt")
60
+ result.attrs["mass_densities"] = mass_densities
61
+
62
+ if mass_densities:
63
+ for symbol, s in self.species.items():
64
+ weight = scipy.constants.u * s.A
65
+ result[symbol] = result[symbol] * weight
66
+
67
+ return result
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python
2
+
3
+ """pymsis
4
+
5
+ """
6
+ from typing import Union
7
+ import scipy.constants
8
+ from .atmosphere import Atmosphere
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ import xarray as xr
12
+
13
+ try:
14
+ from pymsis import msis
15
+ except ImportError:
16
+ msis = None
17
+
18
+
19
+ class AtmPymsis(Atmosphere):
20
+ """
21
+ # TODO: docstring
22
+
23
+ ndarray (ndates, nlons, nlats, nalts, 11) or (ndates, 11) |
24
+ The data calculated at each grid point:
25
+ | [Total mass density (kg/m3) | N2 # density (m-3), | O2 # density (m-3), |
26
+ O # density (m-3), | He # density (m-3), | H # density (m-3), |
27
+ Ar # density (m-3), | N # density (m-3), |
28
+ Anomalous oxygen # density (m-3), | NO # density (m-3),
29
+ | Temperature (K)]
30
+ """
31
+ def __init__(self):
32
+ super().__init__(
33
+ supported_species=["He", "O", "N2", "O2", "Ar", "H", "N"],
34
+ )
35
+
36
+ def density(
37
+ self,
38
+ time: npt.ArrayLike,
39
+ lat: npt.ArrayLike,
40
+ lon: npt.ArrayLike,
41
+ alt: npt.ArrayLike,
42
+ f107: npt.ArrayLike = None,
43
+ f107s: npt.ArrayLike = None,
44
+ aps: npt.ArrayLike = None,
45
+ mass_densities: bool = False,
46
+ version: Union[float, str] = 2.1,
47
+ **kwargs
48
+ ):
49
+ """TODO: Write docstring
50
+
51
+ returns density in [m^-3]
52
+
53
+ """
54
+ if msis is None:
55
+ raise ImportError("pymsis is not installed")
56
+
57
+ time = np.array(time) if not isinstance(time, np.ndarray) else time
58
+ lon = np.array(lon) if not isinstance(lon, np.ndarray) else lon
59
+ lat = np.array(lat) if not isinstance(lat, np.ndarray) else lat
60
+ alt = np.array(alt) if not isinstance(alt, np.ndarray) else alt
61
+
62
+ for inp in [time, lon, lat, alt]:
63
+ if len(inp.shape) == 0:
64
+ inp.shape = (1,)
65
+
66
+ result = msis.run(
67
+ dates=time,
68
+ lons=lon,
69
+ lats=lat,
70
+ alts=alt * 1e-3,
71
+ f107s=f107,
72
+ f107as=f107s,
73
+ aps=aps,
74
+ version=version,
75
+ **kwargs,
76
+ )
77
+ if len(result.shape) == 2:
78
+ result = result.reshape(result.shape[0], 1, 1, 1, result.shape[1])
79
+
80
+ result = xr.Dataset(
81
+ {
82
+ "Total": (["time", "lon", "lat", "alt"], result[:, :, :, :, 0]),
83
+ "N2": (["time", "lon", "lat", "alt"], result[:, :, :, :, 1]),
84
+ "O2": (["time", "lon", "lat", "alt"], result[:, :, :, :, 2]),
85
+ "O": (["time", "lon", "lat", "alt"], result[:, :, :, :, 3]),
86
+ "He": (["time", "lon", "lat", "alt"], result[:, :, :, :, 4]),
87
+ "H": (["time", "lon", "lat", "alt"], result[:, :, :, :, 5]),
88
+ "Ar": (["time", "lon", "lat", "alt"], result[:, :, :, :, 6]),
89
+ "N": (["time", "lon", "lat", "alt"], result[:, :, :, :, 7]),
90
+ "AnomalousO": (["time", "lon", "lat", "alt"], result[:, :, :, :, 8]),
91
+ "NO": (["time", "lon", "lat", "alt"], result[:, :, :, :, 9]),
92
+ "Temperature": (["time", "lon", "lat", "alt"], result[:, :, :, :, 10]),
93
+ },
94
+ coords={
95
+ "time": time,
96
+ "lon": lon,
97
+ "lat": lat,
98
+ "alt": alt,
99
+ },
100
+ attrs=dict(
101
+ f107 = f107,
102
+ f107s = f107s,
103
+ aps = aps,
104
+ mass_densities = mass_densities,
105
+ version = version,
106
+ )
107
+ )
108
+
109
+ if mass_densities:
110
+ for symbol, s in self.species.items():
111
+ weight = scipy.constants.u * s.A
112
+ result[symbol] = result[symbol] * weight
113
+
114
+ return result
@@ -0,0 +1,80 @@
1
+ from abc import abstractmethod
2
+ from dataclasses import dataclass
3
+ import logging
4
+ from copy import copy
5
+
6
+ import scipy.constants as constants
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class Species:
15
+ symbol: str
16
+ A: float
17
+ Z: float
18
+
19
+
20
+ SPECIES = {
21
+ "He": Species(
22
+ symbol="He",
23
+ A=4.002602,
24
+ Z=2,
25
+ ),
26
+ "O": Species(
27
+ symbol="O",
28
+ A=15.999,
29
+ Z=8,
30
+ ),
31
+ "N2": Species(
32
+ symbol="N2",
33
+ A=14.007 * 2,
34
+ Z=14,
35
+ ),
36
+ "O2": Species(
37
+ symbol="O2",
38
+ A=15.999 * 2,
39
+ Z=16,
40
+ ),
41
+ "Ar": Species(
42
+ symbol="Ar",
43
+ A=39.948,
44
+ Z=18,
45
+ ),
46
+ "H": Species(
47
+ symbol="H",
48
+ A=1.0079,
49
+ Z=1,
50
+ ),
51
+ "N": Species(
52
+ symbol="N",
53
+ A=14.007,
54
+ Z=7,
55
+ ),
56
+ }
57
+
58
+
59
+ # TODO: get a a hold of other atm models to figure out how to generalize interface better
60
+ class Atmosphere:
61
+ def __init__(self, supported_species):
62
+ self.species = {}
63
+ for s in supported_species:
64
+ if isinstance(s, Species):
65
+ self.species[s.symbol] = s
66
+ else:
67
+ self.species[s] = copy(SPECIES[s])
68
+
69
+ self.mean_mass = np.array([x.A for _, x in self.species.items()]).mean() * constants.u # [kg]
70
+
71
+ @abstractmethod
72
+ def density(
73
+ self,
74
+ time: npt.ArrayLike,
75
+ lat: npt.ArrayLike,
76
+ lon: npt.ArrayLike,
77
+ alt: npt.ArrayLike,
78
+ **kwargs
79
+ ):
80
+ pass
@@ -0,0 +1,149 @@
1
+ """
2
+ # Coordinate functions
3
+ """
4
+ import logging
5
+ import numpy as np
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ # Constants defined by the World Geodetic System 1984 (WGS84)
11
+ WGS84_a = 6378.137 * 1e3
12
+ WGS84_b = 6356.7523142 * 1e3
13
+ WGS84_esq = 6.69437999014 * 0.001
14
+ WGS84_e1sq = 6.73949674228 * 0.001
15
+
16
+
17
+ def geodetic2ecef(lat, lon, alt):
18
+ """Convert WGS84 geodetic coordinates to ECEF coordinates [^1].
19
+
20
+ [^1]: J. Zhu, "Conversion of Earth-centered Earth-fixed coordinates to geodetic coordinates,"
21
+ IEEE Transactions on Aerospace and Electronic Systems, vol. 30, pp. 957-961, 1994.
22
+
23
+ Parameters
24
+ ----------
25
+ lat : float
26
+ Geographic latitude [deg]
27
+ lon : float
28
+ Geographic longitude [deg]
29
+ alt : float
30
+ Geographic altitude [deg]
31
+
32
+ Returns
33
+ -------
34
+ numpy.ndarray
35
+ [x, y, z] in ECEF coordinates
36
+
37
+ """
38
+ lat, lon = np.radians(lat), np.radians(lon)
39
+ xi = np.sqrt(1 - WGS84_esq * np.sin(lat) ** 2)
40
+ x = (WGS84_a / xi + alt) * np.cos(lat) * np.cos(lon)
41
+ y = (WGS84_a / xi + alt) * np.cos(lat) * np.sin(lon)
42
+ z = (WGS84_a / xi * (1 - WGS84_esq) + alt) * np.sin(lat)
43
+ return np.array([x, y, z])
44
+
45
+
46
+ def ecef2geodetic(x, y, z):
47
+ """Convert ECEF coordinates to WGS84 geodetic coordinates [^1].
48
+
49
+ [^1]: J. Zhu, "Conversion of Earth-centered Earth-fixed coordinates to geodetic coordinates,"
50
+ IEEE Transactions on Aerospace and Electronic Systems, vol. 30, pp. 957-961, 1994.
51
+
52
+ Parameters
53
+ ----------
54
+ x : float
55
+ Position along prime meridian [m]
56
+ y : float
57
+ Position along prime meridian + 90 degrees [m]
58
+ z : float
59
+ Position along earth rotation axis [m]
60
+
61
+ Returns
62
+ -------
63
+ numpy.ndarray
64
+ [lat [deg], lon [deg], alt [m]] in WGS84 geodetic coordinates
65
+
66
+ """
67
+ r = np.sqrt(x * x + y * y)
68
+ if r < 1e-9:
69
+ h = np.abs(z) - WGS84_b
70
+ lat = np.sign(z) * np.pi / 2
71
+ lon = 0.0
72
+ else:
73
+ Esq = WGS84_a * WGS84_a - WGS84_b * WGS84_b
74
+ F = 54 * WGS84_b * WGS84_b * z * z
75
+ G = r * r + (1 - WGS84_esq) * z * z - WGS84_esq * Esq
76
+ C = (WGS84_esq * WGS84_esq * F * r * r) / (np.power(G, 3))
77
+ S = np.cbrt(1 + C + np.sqrt(C * C + 2 * C))
78
+ P = F / (3 * np.power((S + 1 / S + 1), 2) * G * G)
79
+ Q = np.sqrt(1 + 2 * WGS84_esq * WGS84_esq * P)
80
+ r_0 = -(P * WGS84_esq * r) / (1 + Q) + np.sqrt(
81
+ 0.5 * WGS84_a * WGS84_a * (1 + 1.0 / Q)
82
+ - P * (1 - WGS84_esq) * z * z / (Q * (1 + Q))
83
+ - 0.5 * P * r * r
84
+ )
85
+ U = np.sqrt(np.power((r - WGS84_esq * r_0), 2) + z * z)
86
+ V = np.sqrt(np.power((r - WGS84_esq * r_0), 2) + (1 - WGS84_esq) * z * z)
87
+ Z_0 = WGS84_b * WGS84_b * z / (WGS84_a * V)
88
+ h = U * (1 - WGS84_b * WGS84_b / (WGS84_a * V))
89
+ lat = np.arctan((z + WGS84_e1sq * Z_0) / r)
90
+ lon = np.arctan2(y, x)
91
+
92
+ return np.array([np.degrees(lat), np.degrees(lon), h])
93
+
94
+
95
+ def enu2ecef(lat, lon, alt, e, n, u):
96
+ """NEU (north/east/up) to ECEF coordinate system conversion. Degrees are used."""
97
+ return ned2ecef(lat, lon, alt, n, e, -u)
98
+
99
+
100
+ def ned2ecef(lat, lon, alt, n, e, d):
101
+ """NED (north/east/down) to ECEF coordinate system conversion. Degrees are used."""
102
+ lat, lon = np.radians(lat), np.radians(lon)
103
+ mx = np.array(
104
+ [
105
+ [-np.sin(lon), -np.sin(lat) * np.cos(lon), np.cos(lat) * np.cos(lon)],
106
+ [np.cos(lon), -np.sin(lat) * np.sin(lon), np.cos(lat) * np.sin(lon)],
107
+ [0, np.cos(lat), np.sin(lat)],
108
+ ]
109
+ )
110
+ enu = np.array([e, n, -1.0 * d])
111
+ res = np.dot(mx, enu)
112
+ return res
113
+
114
+
115
+ def ecef2ned(lat, lon, alt, x, y, z):
116
+ """NED (east,north,up) from ECEF coordinate system conversion."""
117
+ lat, lon = np.radians(lat), np.radians(lon)
118
+ mx = np.array(
119
+ [
120
+ [-np.sin(lon), -np.sin(lat) * np.cos(lon), np.cos(lat) * np.cos(lon)],
121
+ [np.cos(lon), -np.sin(lat) * np.sin(lon), np.cos(lat) * np.sin(lon)],
122
+ [0, np.cos(lat), np.sin(lat)],
123
+ ]
124
+ )
125
+ enu = np.array([x, y, z])
126
+ res = np.dot(np.linalg.inv(mx), enu)
127
+ return res
128
+
129
+
130
+ def cart_to_azel(vec):
131
+ """Convert from Cartesian coordinates to spherical in a degrees east of north and elevation fashion"""
132
+ x = vec[0]
133
+ y = vec[1]
134
+ z = vec[2]
135
+ r_ = np.sqrt(x**2 + y**2)
136
+ if r_ < 1e-9:
137
+ el = np.sign(z) * np.pi * 0.5
138
+ az = 0.0
139
+ else:
140
+ el = np.arctan(z / r_)
141
+ az = np.pi / 2 - np.arctan2(y, x)
142
+ return np.degrees(az), np.degrees(el), np.sqrt(x**2 + y**2 + z**2)
143
+
144
+
145
+ def azel_to_cart(az, el, r):
146
+ """Convert from spherical coordinates to Cartesian in a degrees east of north and elevation fashion"""
147
+ _az = np.radians(az)
148
+ _el = np.radians(el)
149
+ return r * np.array([np.sin(_az) * np.cos(_el), np.cos(_az) * np.cos(_el), np.sin(_el)])
metablate/material.py ADDED
@@ -0,0 +1,155 @@
1
+ '''
2
+ # Meteoroid material physics
3
+
4
+ List of properties:
5
+ - `m2`: mean atomic mass [1]
6
+ - `u0`: surface binding energy [eV]
7
+ - `k`: average atomic number [1]
8
+ - `CA`, CB: Clausius-Clapeyron coeff [K]
9
+ - `mu`: mean molecular mass of ablated vapour [kg]
10
+ - `k`: Botzmann constant [J/K]
11
+ - `rho_m`: meteoroid density [kg/m3]
12
+ - `c`: specific heat of meteoroid [J/(K*kg)]
13
+ - `L`: latent heat of fusion + vapourization [J/kg]
14
+
15
+ # TODO: list references for material data
16
+
17
+ [Fe SiO2 C C] mean molecular mass per target atoms corresponding
18
+ to the different meteoroid densities, see Rogers p. 1344 values
19
+ are from Tielens et al.
20
+
21
+ Mean molecular mass of ablated vapor assuming that all products
22
+ liberated from the meteoroid by thermal ablation were released
23
+ in complete molecular form.
24
+
25
+ '''
26
+ from dataclasses import dataclass, asdict
27
+ import logging
28
+ from copy import copy
29
+ from scipy import constants
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ @dataclass
35
+ class Material:
36
+ """Container for material properties
37
+
38
+ TODO: proper names for all vars
39
+
40
+ List of properties:
41
+ - `m2`: mean atomic mass [1]
42
+ - `u0`: surface binding energy [eV]
43
+ - `k`: average atomic number [1]
44
+ - `CA`, CB: Clausius-Clapeyron coeff [K]
45
+ - `mu`: mean molecular mass of ablated vapour [kg]
46
+ - `k`: Botzmann constant [J/K]
47
+ - `rho_m`: meteoroid density [kg/m3]
48
+ - `c`: specific heat of meteoroid [J/(K*kg)]
49
+ - `L`: latent heat of fusion + vapourization [J/kg]
50
+ """
51
+ rho_m: float
52
+ mu: float
53
+ m2: float
54
+ CA: float
55
+ CB: float
56
+ u0: float
57
+ k: float
58
+ z2: float
59
+ c: float
60
+ L: float
61
+
62
+
63
+ MATERIALS = {}
64
+
65
+ iron = Material(
66
+ rho_m = 7800.0,
67
+ mu = 56.0*constants.u,
68
+ m2 = 56.0*constants.u,
69
+ CA = 10.6,
70
+ CB = 16120.,
71
+ u0 = 4.1,
72
+ k = 0.35,
73
+ z2 = 26,
74
+ c = 1200,
75
+ L = 6.0E6,
76
+ )
77
+ MATERIALS["iron"] = iron
78
+
79
+ asteroidal = Material(
80
+ rho_m = 3300.0,
81
+ mu = 50.0*constants.u,
82
+ m2 = 20.0*constants.u,
83
+ CA = 10.6,
84
+ CB = 13500.,
85
+ u0 = 6.4,
86
+ k = 0.1,
87
+ z2 = 10,
88
+ c = 1200,
89
+ L = 6.0E6,
90
+ )
91
+ MATERIALS["asteroidal"] = asteroidal
92
+
93
+ cometary = Material(
94
+ rho_m = 1000.0,
95
+ mu = 20.0*constants.u,
96
+ m2 = 12.0*constants.u,
97
+ CA = 10.6,
98
+ CB = 13500.,
99
+ u0 = 4.0,
100
+ k = 0.65,
101
+ z2 = 6,
102
+ c = 1200,
103
+ L = 6.0E6,
104
+ )
105
+ MATERIALS["cometary"] = cometary
106
+
107
+ porous = Material(
108
+ rho_m = 300.0,
109
+ mu = 20.0*constants.u,
110
+ m2 = 12.0*constants.u,
111
+ CA = 10.6,
112
+ CB = 13500.,
113
+ u0 = 4.0,
114
+ k = 0.65,
115
+ z2 = 6,
116
+ c = 1200,
117
+ L = 6.0E6,
118
+ )
119
+ MATERIALS["porous"] = porous
120
+
121
+
122
+ def available():
123
+ '''List currently available meteoroid materials.
124
+ '''
125
+ return list(MATERIALS.keys())
126
+
127
+
128
+ def get(material, as_dict=True):
129
+ '''Returns the physical parameters of the meteoroid based on its material.
130
+
131
+ # TODO: figure out if we keep as dict or keep as material???
132
+
133
+ Parameters
134
+ ----------
135
+ material : str
136
+ name of material
137
+ as_dict : bool
138
+ Return data as dict instead of as a Material instance
139
+
140
+ Returns
141
+ -------
142
+ Material or dict
143
+ Dictionary of all available material parameters (float or None)
144
+ '''
145
+
146
+ _material = material.lower().strip()
147
+ if _material not in MATERIALS:
148
+ raise ValueError(
149
+ f'No data exists for material "{_material}"\n'
150
+ f'Avalible materials are: {available()}'
151
+ )
152
+ if as_dict:
153
+ return asdict(MATERIALS[_material])
154
+ else:
155
+ return copy(MATERIALS[_material])
@@ -0,0 +1,5 @@
1
+ from .kero_szasz_2008 import KeroSzasz2008
2
+
3
+ __all__ = [
4
+ 'KeroSzasz2008'
5
+ ]