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 +16 -0
- metablate/ablation_model.py +40 -0
- metablate/atmosphere/__init__.py +8 -0
- metablate/atmosphere/atm_msise00.py +67 -0
- metablate/atmosphere/atm_pymsis.py +114 -0
- metablate/atmosphere/atmosphere.py +80 -0
- metablate/coordinates.py +149 -0
- metablate/material.py +155 -0
- metablate/models/__init__.py +5 -0
- metablate/models/dimant_oppenheim_2017.py +293 -0
- metablate/models/kero_szasz_2008.py +348 -0
- metablate/ode.py +82 -0
- metablate/physics/__init__.py +5 -0
- metablate/physics/alpha_beta.py +679 -0
- metablate/physics/ballistic.py +104 -0
- metablate/physics/luminosity.py +78 -0
- metablate/physics/sputtering.py +219 -0
- metablate/physics/thermal_ablation.py +616 -0
- metablate/py.typed +0 -0
- metablate/version.py +3 -0
- metablate-0.1.0.dist-info/METADATA +766 -0
- metablate-0.1.0.dist-info/RECORD +25 -0
- metablate-0.1.0.dist-info/WHEEL +5 -0
- metablate-0.1.0.dist-info/licenses/LICENSE +674 -0
- metablate-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy.special as scs
|
|
4
|
+
import scipy.integrate as sci
|
|
5
|
+
import scipy.constants as consts
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
import multiprocessing as mp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ablated_thermal_speed_bronshten_1983(meteoroid_surface_temperature, meteoroid_molecular_mass):
|
|
11
|
+
return np.sqrt(
|
|
12
|
+
3 * consts.R * meteoroid_surface_temperature / (meteoroid_molecular_mass * consts.N_A)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plasma_frequency(electron_density):
|
|
17
|
+
return np.sqrt(electron_density * consts.e**2 / (consts.epsilon_0 * consts.m_e)) / (2 * np.pi)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def critical_plasma_density(critical_plasma_frequency):
|
|
21
|
+
return (
|
|
22
|
+
(critical_plasma_frequency * 2 * np.pi) ** 2 * consts.epsilon_0 * consts.m_e / (consts.e**2)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def mean_free_path(
|
|
27
|
+
ablated_thermal_speed,
|
|
28
|
+
total_atmospheric_number_density,
|
|
29
|
+
collisional_cross_section,
|
|
30
|
+
meteoroid_velocity,
|
|
31
|
+
):
|
|
32
|
+
return ablated_thermal_speed / (
|
|
33
|
+
total_atmospheric_number_density * collisional_cross_section * meteoroid_velocity
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def collisional_cross_section_bronshten_1983(relative_velocity):
|
|
38
|
+
vrel = relative_velocity * 1e-3 # requires km/s
|
|
39
|
+
return 5.61e-19 * vrel ** (-0.8)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ionization_probability_Na_vondrak_2008(relative_velocity):
|
|
43
|
+
vrel = relative_velocity * 1e-3 # requires km/s
|
|
44
|
+
return 0.933 * (vrel - 8.86) ** 2 * vrel ** (-1.94)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _worker(
|
|
48
|
+
inds, tind, Func3_1, Func3_2, low_lim, up_lim, Rlam23, costh, sinth, pbar, quad_abs_tol
|
|
49
|
+
):
|
|
50
|
+
if pbar:
|
|
51
|
+
tqdm_bar = tqdm(
|
|
52
|
+
desc=f"computing electron density process {tind}",
|
|
53
|
+
total=len(inds),
|
|
54
|
+
position=tind,
|
|
55
|
+
)
|
|
56
|
+
f31 = np.empty(inds.shape, dtype=costh.dtype)
|
|
57
|
+
f32 = np.empty(inds.shape, dtype=costh.dtype)
|
|
58
|
+
for lind, ind in enumerate(inds):
|
|
59
|
+
f31[lind] = do_int_1_quad(low_lim[ind], up_lim[ind], Rlam23[ind], costh[ind], quad_abs_tol)
|
|
60
|
+
f32[lind] = do_int_2_quad(
|
|
61
|
+
low_lim[ind], up_lim[ind], Rlam23[ind], costh[ind], sinth[ind], quad_abs_tol
|
|
62
|
+
)
|
|
63
|
+
if pbar:
|
|
64
|
+
tqdm_bar.update(1)
|
|
65
|
+
with Func3_1.get_lock():
|
|
66
|
+
shared_f31 = np.ndarray(costh.shape, dtype=np.float64, buffer=Func3_1.get_obj())
|
|
67
|
+
shared_f31[inds] = f31
|
|
68
|
+
with Func3_2.get_lock():
|
|
69
|
+
shared_f32 = np.ndarray(costh.shape, dtype=np.float64, buffer=Func3_2.get_obj())
|
|
70
|
+
shared_f32[inds] = f32
|
|
71
|
+
if pbar:
|
|
72
|
+
tqdm_bar.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _do_integral_1(ksi, rlam23, cth):
|
|
76
|
+
return (
|
|
77
|
+
np.sqrt(1 + 2 * rlam23 * ksi ** (2 / 3) / np.pi)
|
|
78
|
+
* np.exp(-3 * rlam23 * ksi ** (2 / 3) / 2)
|
|
79
|
+
* np.sqrt((ksi**2 - cth**2) / (1 - ksi**2))
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _do_integral_2(ksi, rlam23, cth, sth):
|
|
84
|
+
return (
|
|
85
|
+
np.sqrt(1 + 2 * rlam23 * ksi ** (2 / 3) / np.pi)
|
|
86
|
+
* np.exp(-3 * rlam23 * ksi ** (2 / 3) / 2)
|
|
87
|
+
* np.arcsin(np.sqrt(1 - ksi**2) * np.abs(cth) / (ksi * np.abs(sth)))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def do_int_1_quad(a, b, rlam23, cth, quad_abs_tol):
|
|
92
|
+
int1 = sci.quad(
|
|
93
|
+
_do_integral_1,
|
|
94
|
+
a,
|
|
95
|
+
b,
|
|
96
|
+
args=(rlam23, cth),
|
|
97
|
+
epsrel=0,
|
|
98
|
+
epsabs=quad_abs_tol,
|
|
99
|
+
)
|
|
100
|
+
return int1[0]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def do_int_2_quad(a, b, rlam23, cth, sth, quad_abs_tol):
|
|
104
|
+
int2 = sci.quad(
|
|
105
|
+
_do_integral_2,
|
|
106
|
+
a,
|
|
107
|
+
b,
|
|
108
|
+
args=(rlam23, cth, sth),
|
|
109
|
+
epsrel=0,
|
|
110
|
+
epsabs=quad_abs_tol,
|
|
111
|
+
)
|
|
112
|
+
return int2[0]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def plasma_distribution_morphology(
|
|
116
|
+
x_grid,
|
|
117
|
+
y_grid,
|
|
118
|
+
quad_abs_tol=1e-7,
|
|
119
|
+
upper_limit_delta=1e-6,
|
|
120
|
+
threads=None,
|
|
121
|
+
pbar=True,
|
|
122
|
+
):
|
|
123
|
+
"""The plasma distribution morphology component of the Dimant and Oppenheim model, that is
|
|
124
|
+
scaled by mean free path and independent of meteor parameters.
|
|
125
|
+
|
|
126
|
+
grid is in units of mean free path
|
|
127
|
+
"""
|
|
128
|
+
# Generate density distribution
|
|
129
|
+
# Solve for a 2D plane
|
|
130
|
+
[Xlam, Ylam] = np.meshgrid(x_grid, y_grid)
|
|
131
|
+
|
|
132
|
+
Rlam = np.sqrt(Xlam**2 + Ylam**2)
|
|
133
|
+
Rlam[Rlam == 0] = np.nan
|
|
134
|
+
|
|
135
|
+
costh = Xlam / Rlam
|
|
136
|
+
sinth = Ylam / Rlam
|
|
137
|
+
Rlam23 = Rlam ** (2 / 3)
|
|
138
|
+
|
|
139
|
+
Func1 = np.sqrt(2 * np.pi / 3) / Rlam * scs.erf(
|
|
140
|
+
np.sqrt(3 / 2) * Rlam ** (1 / 3) * np.abs(costh) ** (1 / 3)
|
|
141
|
+
) - np.exp(-3 / 2 * Rlam23 * np.abs(costh) ** (2 / 3)) * (
|
|
142
|
+
(4 - np.pi)
|
|
143
|
+
* np.abs(costh)
|
|
144
|
+
/ (2 * np.sqrt((1 + (4 - np.pi) ** 2 * Rlam23 * np.abs(costh) ** (2 / 3)) / (2 * np.pi)))
|
|
145
|
+
+ 2 * np.abs(costh) ** (1 / 3) / Rlam23
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
Func2 = np.sqrt(2 * np.pi / 3) / Rlam * scs.erf(np.sqrt(3 * Rlam23 / 2)) - (
|
|
149
|
+
1 + 2 / Rlam23
|
|
150
|
+
) * np.exp(-3 * Rlam23 / 2)
|
|
151
|
+
|
|
152
|
+
orig_shape = costh.shape
|
|
153
|
+
new_shape = (costh.size,)
|
|
154
|
+
costh = costh.reshape(new_shape)
|
|
155
|
+
sinth = sinth.reshape(new_shape)
|
|
156
|
+
Rlam23 = Rlam23.reshape(new_shape)
|
|
157
|
+
low_lim = np.abs(costh) # Lower limit of integration |cosθ|
|
|
158
|
+
up_lim = np.full_like(
|
|
159
|
+
costh, 1 - upper_limit_delta
|
|
160
|
+
) # Upper limit of integration 1 (avoid singularity at exactly 1)
|
|
161
|
+
|
|
162
|
+
# todo: hack to make sure integration limits are sane
|
|
163
|
+
low_lim[low_lim > up_lim] = up_lim[low_lim > up_lim] - upper_limit_delta
|
|
164
|
+
|
|
165
|
+
# TODO: skip problematic points in the calc
|
|
166
|
+
if threads is None:
|
|
167
|
+
if pbar:
|
|
168
|
+
tqdm_bar = tqdm(desc="computing electron density", total=len(costh))
|
|
169
|
+
Func3_1 = np.empty_like(costh)
|
|
170
|
+
Func3_2 = np.empty_like(costh)
|
|
171
|
+
for ind in range(len(costh)):
|
|
172
|
+
Func3_1[ind] = do_int_1_quad(
|
|
173
|
+
low_lim[ind],
|
|
174
|
+
up_lim[ind],
|
|
175
|
+
Rlam23[ind],
|
|
176
|
+
costh[ind],
|
|
177
|
+
quad_abs_tol,
|
|
178
|
+
)
|
|
179
|
+
Func3_2[ind] = do_int_2_quad(
|
|
180
|
+
low_lim[ind],
|
|
181
|
+
up_lim[ind],
|
|
182
|
+
Rlam23[ind],
|
|
183
|
+
costh[ind],
|
|
184
|
+
sinth[ind],
|
|
185
|
+
quad_abs_tol,
|
|
186
|
+
)
|
|
187
|
+
if pbar:
|
|
188
|
+
tqdm_bar.update(1)
|
|
189
|
+
if pbar:
|
|
190
|
+
tqdm_bar.close()
|
|
191
|
+
else:
|
|
192
|
+
th = []
|
|
193
|
+
chunks = np.array_split(np.arange(len(costh)), threads)
|
|
194
|
+
|
|
195
|
+
f31 = mp.Array(ctypes.c_double, costh.size)
|
|
196
|
+
f32 = mp.Array(ctypes.c_double, costh.size)
|
|
197
|
+
for tind, chunk in enumerate(chunks):
|
|
198
|
+
t = mp.Process(
|
|
199
|
+
target=_worker,
|
|
200
|
+
args=(
|
|
201
|
+
chunk,
|
|
202
|
+
tind,
|
|
203
|
+
f31,
|
|
204
|
+
f32,
|
|
205
|
+
low_lim,
|
|
206
|
+
up_lim,
|
|
207
|
+
Rlam23,
|
|
208
|
+
costh,
|
|
209
|
+
sinth,
|
|
210
|
+
pbar,
|
|
211
|
+
quad_abs_tol,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
th.append(t)
|
|
215
|
+
t.start()
|
|
216
|
+
|
|
217
|
+
for t in th:
|
|
218
|
+
t.join()
|
|
219
|
+
print("\n" * threads)
|
|
220
|
+
Func3_1 = np.ndarray(costh.shape, dtype=np.float64, buffer=f31.get_obj())
|
|
221
|
+
Func3_2 = np.ndarray(costh.shape, dtype=np.float64, buffer=f32.get_obj())
|
|
222
|
+
Func3_1 = Func3_1.reshape(orig_shape)
|
|
223
|
+
Func3_2 = Func3_2.reshape(orig_shape)
|
|
224
|
+
|
|
225
|
+
costh = costh.reshape(orig_shape)
|
|
226
|
+
sinth = sinth.reshape(orig_shape)
|
|
227
|
+
|
|
228
|
+
t1 = np.abs(costh) * Func1
|
|
229
|
+
t2 = costh * Func2
|
|
230
|
+
t3 = np.abs(costh) * Func3_2 + Func3_1
|
|
231
|
+
t3[np.isnan(t3)] = 0
|
|
232
|
+
|
|
233
|
+
ne_morph = np.abs(t1 + t2 + t3)
|
|
234
|
+
|
|
235
|
+
# todo: this is to fix some numerical shenanigans, can probably be solved better
|
|
236
|
+
zero_point = np.argmin(np.abs(y_grid))
|
|
237
|
+
if y_grid[zero_point] < 1e-3:
|
|
238
|
+
ne_morph[zero_point, :] = (ne_morph[zero_point - 1, :] + ne_morph[zero_point + 1, :]) / 2
|
|
239
|
+
|
|
240
|
+
return Xlam, Ylam, Rlam, ne_morph
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def plasma_distribution(
|
|
244
|
+
total_atmospheric_number_density,
|
|
245
|
+
meteoroid_velocity,
|
|
246
|
+
meteoroid_radius,
|
|
247
|
+
plasma_source_density,
|
|
248
|
+
collisional_cross_section,
|
|
249
|
+
ionization_probability,
|
|
250
|
+
base_plasma_distribution_function,
|
|
251
|
+
R_lambda,
|
|
252
|
+
ablated_thermal_speed=951,
|
|
253
|
+
atmospheric_species_mass=4.7e-26,
|
|
254
|
+
ablated_species_mass=3.8e-26,
|
|
255
|
+
):
|
|
256
|
+
"""Calculate meteor head plasma distribution using model from Dimant and
|
|
257
|
+
Oppenheim 2017
|
|
258
|
+
|
|
259
|
+
Originally developed by Tarnecki, Liane et al. 20 June 2020
|
|
260
|
+
Code available at https://zenodo.org/records/4723667
|
|
261
|
+
Paper available at https://doi.org/10.1029/2021JA029525
|
|
262
|
+
Ported to Python by Daniel Kastinen 2025
|
|
263
|
+
|
|
264
|
+
ablated_thermal_speed = 951 #thermal speed of ablated particles, m/s
|
|
265
|
+
# Set meteoroid and atmospheric characteristics
|
|
266
|
+
# atmospheric_species_mass = 4.7e-26 #atmospheric species mass (N2), kg
|
|
267
|
+
# ablated_species_mass = 3.8e-26 #ablated species mass (Na), kg
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
# mean free path
|
|
271
|
+
lamT = ablated_thermal_speed / (
|
|
272
|
+
total_atmospheric_number_density * collisional_cross_section * meteoroid_velocity
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Calculate the ionizing cross section Gion(meteoroid_velocity)
|
|
276
|
+
Gcoll = collisional_cross_section / (4 * np.pi) # net differential cross section
|
|
277
|
+
Gion = Gcoll * ionization_probability
|
|
278
|
+
|
|
279
|
+
coeff_const = (
|
|
280
|
+
8
|
|
281
|
+
* np.pi
|
|
282
|
+
* meteoroid_radius**2
|
|
283
|
+
* plasma_source_density
|
|
284
|
+
* total_atmospheric_number_density
|
|
285
|
+
/ np.sqrt(3)
|
|
286
|
+
* (1 + ablated_species_mass / atmospheric_species_mass)
|
|
287
|
+
* Gion
|
|
288
|
+
) # Density coefficient
|
|
289
|
+
|
|
290
|
+
coeff = coeff_const / (R_lambda * lamT)
|
|
291
|
+
|
|
292
|
+
ne2D = coeff * base_plasma_distribution_function
|
|
293
|
+
return ne2D
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""Model implemented in:
|
|
4
|
+
|
|
5
|
+
Kero, J., Szasz, C., Pellinen-Wannberg, A., Wannberg, G., Westman, A., and Meisel, D. D. (2008).
|
|
6
|
+
Determination of meteoroid physical properties from tristatic radar observations.
|
|
7
|
+
Annales Geophysicae
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Union
|
|
12
|
+
import numpy as np
|
|
13
|
+
import scipy
|
|
14
|
+
from scipy import constants
|
|
15
|
+
import xarray
|
|
16
|
+
|
|
17
|
+
from ..ode import ScipyODESolve
|
|
18
|
+
from .. import physics
|
|
19
|
+
from .. import coordinates
|
|
20
|
+
from ..atmosphere import AtmPymsis, AtmMSISE00
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KeroSzasz2008(ScipyODESolve):
|
|
26
|
+
"""Ablation model
|
|
27
|
+
|
|
28
|
+
Keyword arguments:
|
|
29
|
+
|
|
30
|
+
- temperature0 = 290 [K]: Meteoroid temperature at starting height
|
|
31
|
+
- shape_factor = 1.21 [1]: Shape is assumed to be a sphere.
|
|
32
|
+
- emissivity = 0.9 [1]: Hill et al.; Love & Brownlee: 1; (metal oxides)
|
|
33
|
+
- sputtering = True [bool]: If sputtering is used in mass loss calculation.
|
|
34
|
+
- Gamma = None [1]: Drag coefficient, if :code:`None` it is dynamically
|
|
35
|
+
calculated assuming a transition from (and including) free molecular
|
|
36
|
+
flow to a (and not including) shock regime. Otherwise assumed a
|
|
37
|
+
constant with the given value.
|
|
38
|
+
- Lambda = None [1]: Heat transfer coefficient, if :code:`None` it is
|
|
39
|
+
dynamically calculated assuming a transition from (and including)
|
|
40
|
+
free molecular flow to a (and not including) shock regime.
|
|
41
|
+
Otherwise assumed a constant with the given value.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
DEFAULT_CONFIG = {
|
|
46
|
+
"options": {
|
|
47
|
+
"temperature0": 290,
|
|
48
|
+
"shape_factor": 1.21,
|
|
49
|
+
"emissivity": 0.9,
|
|
50
|
+
"sputtering": False,
|
|
51
|
+
"Gamma": None,
|
|
52
|
+
"Lambda": None,
|
|
53
|
+
"integral_resolution": 100,
|
|
54
|
+
},
|
|
55
|
+
"atmosphere": {
|
|
56
|
+
"f107": None,
|
|
57
|
+
"f107s": None,
|
|
58
|
+
"version": 2.1,
|
|
59
|
+
},
|
|
60
|
+
"integrate": {
|
|
61
|
+
"minimum_mass_kg": 1e-11,
|
|
62
|
+
"max_step_size_sec": 1e-1,
|
|
63
|
+
"max_time_sec": 100.0,
|
|
64
|
+
"method": "RK45",
|
|
65
|
+
},
|
|
66
|
+
"method_options": {},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def __init__(self, atmosphere: Union[AtmPymsis, AtmMSISE00], *args, **kwargs):
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self._G = constants.G
|
|
72
|
+
self._M = 5.9742e24 # [kg] mass of earth
|
|
73
|
+
self.atmosphere = atmosphere
|
|
74
|
+
|
|
75
|
+
def rhs(
|
|
76
|
+
self,
|
|
77
|
+
t,
|
|
78
|
+
mass,
|
|
79
|
+
y,
|
|
80
|
+
material_data,
|
|
81
|
+
Lambda,
|
|
82
|
+
Gamma,
|
|
83
|
+
epoch,
|
|
84
|
+
reference_ecef,
|
|
85
|
+
v_dir_ecef,
|
|
86
|
+
):
|
|
87
|
+
"""The right hand side of the differential equation to be integrated, i.e:
|
|
88
|
+
|
|
89
|
+
.. math::
|
|
90
|
+
|
|
91
|
+
\\frac{\\mathrm{d}v}{\\mathrm{d}t} = \\\\
|
|
92
|
+
\\\\
|
|
93
|
+
\\frac{\\mathrm{d}m}{\\mathrm{d}t} = \\\\
|
|
94
|
+
\\\\
|
|
95
|
+
\\frac{\\mathrm{d}s}{\\mathrm{d}t} = \\\\
|
|
96
|
+
\\\\
|
|
97
|
+
\\frac{\\mathrm{d}T}{\\mathrm{d}t} =
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
The numpy vector is structured as follows (numbers indicating index):
|
|
101
|
+
|
|
102
|
+
0. dvdt
|
|
103
|
+
1. dmdt
|
|
104
|
+
2. dsdt
|
|
105
|
+
3. dTdt
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
vel, s, T = y
|
|
109
|
+
|
|
110
|
+
# meteoroid height above earth surface
|
|
111
|
+
traj = reference_ecef - v_dir_ecef * s
|
|
112
|
+
lat, lon, alt = coordinates.ecef2geodetic(*traj.tolist())
|
|
113
|
+
|
|
114
|
+
ecef = coordinates.geodetic2ecef(lat, lon, alt)
|
|
115
|
+
r = np.linalg.norm(ecef)
|
|
116
|
+
|
|
117
|
+
# logger.debug(f'Position: {ecef*1e-3} km')
|
|
118
|
+
# logger.debug(f'lat = {lat}, lon = {lon}, {alt*1e-3} km')
|
|
119
|
+
|
|
120
|
+
# logger.debug(f't0 + {t} s: ')
|
|
121
|
+
# logger.debug(f'vel = {vel*1e-3} km/s, traj-s = {s*1e-3} km, mass = {mass} kg, temp = {T} K')
|
|
122
|
+
# logger.debug(f'altitude = {alt*1e-3} km')
|
|
123
|
+
|
|
124
|
+
f107 = self.config.get("atmosphere", "f107")
|
|
125
|
+
if f107 is not None:
|
|
126
|
+
f107 = float(f107)
|
|
127
|
+
f107s = self.config.get("atmosphere", "f107s")
|
|
128
|
+
if f107s is not None:
|
|
129
|
+
f107s = float(f107s)
|
|
130
|
+
|
|
131
|
+
atm = self.atmosphere.density(
|
|
132
|
+
time=epoch + np.timedelta64(int(t * 1e6), "us"),
|
|
133
|
+
lat=lat,
|
|
134
|
+
lon=lon,
|
|
135
|
+
alt=alt,
|
|
136
|
+
f107=f107,
|
|
137
|
+
f107s=f107s,
|
|
138
|
+
mass_densities=False,
|
|
139
|
+
version=self.config.getfloat("atmosphere", "version"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
rho_tot = atm["Total"].values.squeeze()
|
|
143
|
+
N_rho_tot = rho_tot / self.atmosphere.mean_mass
|
|
144
|
+
|
|
145
|
+
if self.config.getboolean("options", "sputtering"):
|
|
146
|
+
dmdt_s = physics.sputtering.sputtering(
|
|
147
|
+
mass=mass,
|
|
148
|
+
velocity=vel,
|
|
149
|
+
material_data=material_data,
|
|
150
|
+
density=atm,
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
dmdt_s = 0.0
|
|
154
|
+
|
|
155
|
+
dmdt_a = physics.thermal_ablation.thermal_ablation_hill_et_al_2005(
|
|
156
|
+
mass=mass,
|
|
157
|
+
temperature=T,
|
|
158
|
+
material_data=material_data,
|
|
159
|
+
shape_factor=self.config.getfloat("options", "shape_factor"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# logger.debug(f'dmdt sputtering: {dmdt_s} kg/s')
|
|
163
|
+
# logger.debug(f'dmdt ablation : {dmdt_a} kg/s')
|
|
164
|
+
|
|
165
|
+
if Lambda is None:
|
|
166
|
+
Lambda = physics.thermal_ablation.heat_transfer_bronshten_1983(
|
|
167
|
+
mass=mass,
|
|
168
|
+
velocity=vel,
|
|
169
|
+
temperature=T,
|
|
170
|
+
material_data=material_data,
|
|
171
|
+
atm_total_number_density=N_rho_tot,
|
|
172
|
+
mass_loss_thermal_ablation=dmdt_a,
|
|
173
|
+
atm_mean_mass=self.atmosphere.mean_mass,
|
|
174
|
+
res=self.config.getint("options", "integral_resolution"),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if Gamma is None:
|
|
178
|
+
Gamma = physics.thermal_ablation.drag_coefficient_bronshten_1983(
|
|
179
|
+
mass=mass,
|
|
180
|
+
velocity=vel,
|
|
181
|
+
temperature=T,
|
|
182
|
+
material_data=material_data,
|
|
183
|
+
atm_total_number_density=N_rho_tot,
|
|
184
|
+
atm_mean_mass=self.atmosphere.mean_mass,
|
|
185
|
+
res=self.config.getint("options", "integral_resolution"),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# -- Differential equation for the velocity to solve
|
|
189
|
+
dvdt_d = (
|
|
190
|
+
-Gamma
|
|
191
|
+
* self.config.getfloat("options", "shape_factor")
|
|
192
|
+
* rho_tot
|
|
193
|
+
* vel**2
|
|
194
|
+
/ (mass ** (1.0 / 3.0) * material_data["rho_m"] ** (2.0 / 3.0))
|
|
195
|
+
) # [m/s2] drag equation (because of conservation of linear momentum):
|
|
196
|
+
# decelearation=Drag_coeff*shape_factor*atm_dens*vel/(mass^1/2 * meteoroid_dens^2/3)
|
|
197
|
+
dvdt_g = self._G * self._M / (r**2) # [m/s2] acceleration due to earth gravitaion
|
|
198
|
+
dvdt = dvdt_d + dvdt_g
|
|
199
|
+
|
|
200
|
+
# -- Differential equation for the height to solve
|
|
201
|
+
dsdt = -vel # range from the common volume along the meteoroid trajectory
|
|
202
|
+
|
|
203
|
+
dTdt = physics.thermal_ablation.temperature_rate_hill_et_al_2005(
|
|
204
|
+
mass=mass,
|
|
205
|
+
velocity=vel,
|
|
206
|
+
temperature=T,
|
|
207
|
+
material_data=material_data,
|
|
208
|
+
shape_factor=self.config.getfloat("options", "shape_factor"),
|
|
209
|
+
atm_total_mass_density=rho_tot,
|
|
210
|
+
mass_loss_thermal_ablation=-dmdt_a,
|
|
211
|
+
Lambda=Lambda,
|
|
212
|
+
atm_temperature=self.config.getfloat("options", "temperature0"),
|
|
213
|
+
emissivity=self.config.getfloat("options", "emissivity"),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
dmdt = dmdt_a + dmdt_s # total mass loss
|
|
217
|
+
|
|
218
|
+
ret = np.array([dmdt, dvdt, dsdt, dTdt], dtype=np.float64)
|
|
219
|
+
|
|
220
|
+
# logging.debug(
|
|
221
|
+
# f"DERIVS: vel = {dvdt*1e-3} km/s^2, traj-s = {dsdt*1e-3}"
|
|
222
|
+
# f"km/s, mass = {dmdt} kg/s, temp = {dTdt} K/s"
|
|
223
|
+
# )
|
|
224
|
+
|
|
225
|
+
return ret
|
|
226
|
+
|
|
227
|
+
def run(
|
|
228
|
+
self,
|
|
229
|
+
velocity0,
|
|
230
|
+
mass0,
|
|
231
|
+
altitude0,
|
|
232
|
+
zenith_ang,
|
|
233
|
+
azimuth_ang,
|
|
234
|
+
material_data,
|
|
235
|
+
time,
|
|
236
|
+
lat,
|
|
237
|
+
lon,
|
|
238
|
+
alt,
|
|
239
|
+
):
|
|
240
|
+
"""Runs the ablation model.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
velocity0 : float or numpy.ndarray
|
|
245
|
+
Meteoroid initial velocity [m/s]
|
|
246
|
+
mass0 : float or numpy.ndarray
|
|
247
|
+
Meteoroid initial mass [kg]
|
|
248
|
+
altitude0 : float or numpy.ndarray
|
|
249
|
+
Meteoroid initial altitude [m]
|
|
250
|
+
zenith_ang : float or numpy.ndarray
|
|
251
|
+
Zenith angle of the trajectory (-velocity vector) w.r.t reference point [deg]
|
|
252
|
+
azimuth_ang : float or numpy.ndarray
|
|
253
|
+
Azimuthal angle east of north of the trajectory (-velocity vector) w.r.t reference point [deg]
|
|
254
|
+
material_data : dict
|
|
255
|
+
Meteoroid material data, see [`material_data`][ablate.functions.material.material_data].
|
|
256
|
+
lat : float or numpy.ndarray
|
|
257
|
+
Geographic latitude in degrees of reference point on the meteoroid trajectory
|
|
258
|
+
lon : float or numpy.ndarray
|
|
259
|
+
Geographic longitude in degrees of reference point on the meteoroid trajectory
|
|
260
|
+
alt : float or numpy.ndarray
|
|
261
|
+
Altitude above geoid in meters of reference point on the meteoroid trajectory
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
xarray.Dataset
|
|
266
|
+
mass, velocity, position, temperature, ecef_x, ecef_y, ecef_z, altitude as variables.
|
|
267
|
+
|
|
268
|
+
Notes
|
|
269
|
+
-----
|
|
270
|
+
This function is based on calc_sput.m which was used to verify the
|
|
271
|
+
sputtering described in Rogers et al.: Mass loss due to sputtering and
|
|
272
|
+
thermal processes in meteoroid ablation,
|
|
273
|
+
Planetary and Space Science 53 p. 1341-1354 (2005).
|
|
274
|
+
|
|
275
|
+
TODO: Add additional dynamical parameters to data structure e.g.
|
|
276
|
+
lambda and gamma if they are not constant
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
logger.debug(f"Running {self.__class__} model")
|
|
280
|
+
|
|
281
|
+
reference_ecef = coordinates.geodetic2ecef(lat, lon, alt)
|
|
282
|
+
v_dir = -1.0 * coordinates.azel_to_cart(azimuth_ang, zenith_ang, 1.0)
|
|
283
|
+
v_dir_ecef = coordinates.enu2ecef(lat, lon, alt, *v_dir.tolist())
|
|
284
|
+
|
|
285
|
+
def s_to_geo(s):
|
|
286
|
+
traj = reference_ecef - v_dir_ecef * s
|
|
287
|
+
geo = coordinates.ecef2geodetic(*traj.tolist())
|
|
288
|
+
return geo
|
|
289
|
+
|
|
290
|
+
s0 = scipy.optimize.minimize_scalar(lambda s: np.abs(s_to_geo(s)[2] - altitude0)).x
|
|
291
|
+
t0 = self.config.getfloat("options", "temperature0")
|
|
292
|
+
y0 = np.array(
|
|
293
|
+
[mass0, velocity0, s0, t0],
|
|
294
|
+
dtype=np.float64,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
Lambda = self.config.get("options", "Lambda")
|
|
298
|
+
if Lambda is not None:
|
|
299
|
+
Lambda = float(Lambda)
|
|
300
|
+
Gamma = self.config.get("options", "Gamma")
|
|
301
|
+
if Gamma is not None:
|
|
302
|
+
Gamma = float(Gamma)
|
|
303
|
+
|
|
304
|
+
ivp_result = self.integrate(
|
|
305
|
+
y0,
|
|
306
|
+
material_data,
|
|
307
|
+
Lambda,
|
|
308
|
+
Gamma,
|
|
309
|
+
time,
|
|
310
|
+
reference_ecef,
|
|
311
|
+
v_dir_ecef,
|
|
312
|
+
)
|
|
313
|
+
t = ivp_result.t
|
|
314
|
+
_data = {}
|
|
315
|
+
variables = [
|
|
316
|
+
"mass",
|
|
317
|
+
"velocity",
|
|
318
|
+
"position",
|
|
319
|
+
"temperature",
|
|
320
|
+
"ecef_x",
|
|
321
|
+
"ecef_y",
|
|
322
|
+
"ecef_z",
|
|
323
|
+
"altitude",
|
|
324
|
+
]
|
|
325
|
+
for key in variables:
|
|
326
|
+
_data[key] = (["t"], np.empty(t.shape, dtype=np.float64))
|
|
327
|
+
|
|
328
|
+
results = xarray.Dataset(
|
|
329
|
+
_data,
|
|
330
|
+
coords={"t": t},
|
|
331
|
+
attrs={key: val for key, val in self.config.items("options")},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
results["mass"][:] = ivp_result.y[0, :]
|
|
335
|
+
results["velocity"][:] = ivp_result.y[1, :]
|
|
336
|
+
results["position"][:] = ivp_result.y[2, :]
|
|
337
|
+
results["temperature"][:] = ivp_result.y[3, :]
|
|
338
|
+
|
|
339
|
+
ecef = ivp_result.y[2, :]
|
|
340
|
+
ecef = reference_ecef[:, None] - v_dir_ecef[:, None] * ecef[None, :]
|
|
341
|
+
alts = np.array([s_to_geo(_s)[2] for _s in ivp_result.y[2, :]])
|
|
342
|
+
|
|
343
|
+
results["ecef_x"][:] = ecef[0, :]
|
|
344
|
+
results["ecef_y"][:] = ecef[1, :]
|
|
345
|
+
results["ecef_z"][:] = ecef[2, :]
|
|
346
|
+
results["altitude"][:] = alts
|
|
347
|
+
|
|
348
|
+
return results
|