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,679 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.optimize as sco
|
|
3
|
+
import scipy.special as scs
|
|
4
|
+
import scipy.interpolate as sci
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def alpha_direct(
|
|
8
|
+
aerodynamic_cd,
|
|
9
|
+
sea_level_rho,
|
|
10
|
+
atmospheric_scale_height,
|
|
11
|
+
initial_cross_section,
|
|
12
|
+
initial_mass,
|
|
13
|
+
radiant_local_elevation,
|
|
14
|
+
degrees=False,
|
|
15
|
+
):
|
|
16
|
+
"""Ballistic coefficient used to describe the analytic solutions to the
|
|
17
|
+
ablation equations described in [^1].
|
|
18
|
+
|
|
19
|
+
$$
|
|
20
|
+
\\alpha = \\frac{1}{2} c_d \\frac{\\rho_0 h_0 S_e}{M_e \\sin(\\gamma)}
|
|
21
|
+
$$
|
|
22
|
+
|
|
23
|
+
[^1]: Gritsevich MI (2009) Determination of parameters of meteor bodies based
|
|
24
|
+
on flight observational data. Advances in Space Research 44:323-334.
|
|
25
|
+
https://doi.org/10.1016/j.asr.2009.03.030
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
aerodynamic_cd : float or numpy.ndarray
|
|
30
|
+
Aerodynamic drag coefficient [1]
|
|
31
|
+
sea_level_rho : float or numpy.ndarray
|
|
32
|
+
Atmospheric density of homogenous atmosphere at sea-level [kg/m^3].
|
|
33
|
+
atmospheric_scale_height : float or numpy.ndarray
|
|
34
|
+
Characteristic scale height of the homogenous atmosphere [m]
|
|
35
|
+
initial_cross_section : float or numpy.ndarray
|
|
36
|
+
Initial meteoroid cross-sectional area [m^2].
|
|
37
|
+
initial_mass : float or numpy.ndarray
|
|
38
|
+
Initial mass of the meteoroid [kg].
|
|
39
|
+
radiant_local_elevation : float or numpy.ndarray
|
|
40
|
+
Elevation angle of local radiant at start of ablation [deg].
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
float or numpy.ndarray
|
|
45
|
+
Ballistic coefficient [1]
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
el = np.radians(radiant_local_elevation) if degrees else radiant_local_elevation
|
|
49
|
+
return (
|
|
50
|
+
0.5
|
|
51
|
+
* aerodynamic_cd
|
|
52
|
+
* sea_level_rho
|
|
53
|
+
* atmospheric_scale_height
|
|
54
|
+
* initial_cross_section
|
|
55
|
+
/ (initial_mass * np.sin(el))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def beta_direct(
|
|
60
|
+
shape_change_coefficient,
|
|
61
|
+
heat_exchange_coefficient,
|
|
62
|
+
initial_velocity,
|
|
63
|
+
aerodynamic_cd,
|
|
64
|
+
enthalpy_of_massloss,
|
|
65
|
+
):
|
|
66
|
+
"""Mass loss parameter used to describe the analytic solutions to the
|
|
67
|
+
ablation equations described in [^1].
|
|
68
|
+
|
|
69
|
+
$$
|
|
70
|
+
\\beta = (1 - \\mu) \\frac{c_h V_e^2}{2c_d H^*}
|
|
71
|
+
$$
|
|
72
|
+
|
|
73
|
+
[^1]: Gritsevich MI (2009) Determination of parameters of meteor bodies based
|
|
74
|
+
on flight observational data. Advances in Space Research 44:323-334.
|
|
75
|
+
https://doi.org/10.1016/j.asr.2009.03.030
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
shape_change_coefficient : float or numpy.ndarray
|
|
80
|
+
Coefficient $\\mu$ describing the relation between the *dimensionless*
|
|
81
|
+
mass of the body and the *dimensionless* cross-sectional (i.e. projected)
|
|
82
|
+
area by $s = m^{\\mu}$. Since $s,m$ are the dimensionless quantities
|
|
83
|
+
normalized by the initial state, $\\mu$ also characterizes the possible
|
|
84
|
+
role of rotation during the flight. It is assumed as constant during ablation.
|
|
85
|
+
heat_exchange_coefficient : float or numpy.ndarray
|
|
86
|
+
The amount of the heat flux from the ambient gas that is spent
|
|
87
|
+
for ablating the object [1].
|
|
88
|
+
initial_velocity : float or numpy.ndarray
|
|
89
|
+
Initial meteoroid entry-velocity before interaction with the Earth's atmosphere [m/s].
|
|
90
|
+
aerodynamic_cd : float or numpy.ndarray
|
|
91
|
+
Aerodynamic drag coefficient [1]
|
|
92
|
+
enthalpy_of_massloss : float or numpy.ndarray
|
|
93
|
+
Initial mass of the meteoroid [J/kg].
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
float or numpy.ndarray
|
|
98
|
+
Mass loss parameter [1]
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
return (
|
|
102
|
+
(1 - shape_change_coefficient)
|
|
103
|
+
* heat_exchange_coefficient
|
|
104
|
+
* initial_velocity**2
|
|
105
|
+
/ (2 * aerodynamic_cd * enthalpy_of_massloss)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ablation_coefficient(beta, shape_change_coefficient, initial_velocity):
|
|
110
|
+
"""Ablation coefficient calculated from the parameters of the
|
|
111
|
+
analytic solutions to the ablation equations described in [^1].
|
|
112
|
+
|
|
113
|
+
$$
|
|
114
|
+
\\sigma = \\frac{2\\beta}{(1 - \\mu) V_e^2}
|
|
115
|
+
$$
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
[^1]: Gritsevich MI (2009) Determination of parameters of meteor bodies based
|
|
119
|
+
on flight observational data. Advances in Space Research 44:323-334.
|
|
120
|
+
https://doi.org/10.1016/j.asr.2009.03.030
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
beta : float or numpy.ndarray
|
|
125
|
+
Mass loss parameter used to describe the analytic solutions to the
|
|
126
|
+
ablation equations.
|
|
127
|
+
shape_change_coefficient : float or numpy.ndarray
|
|
128
|
+
Coefficient $\\mu$ describing the relation between the *dimensionless*
|
|
129
|
+
mass of the body and the *dimensionless* cross-sectional (i.e. projected)
|
|
130
|
+
area by $s = m^{\\mu}$. Since $s,m$ are the dimensionless quantities
|
|
131
|
+
normalized by the initial state, $\\mu$ also characterizes the possible
|
|
132
|
+
role of rotation during the flight. It is assumed as constant during ablation.
|
|
133
|
+
initial_velocity : float or numpy.ndarray
|
|
134
|
+
Initial meteoroid entry-velocity before interaction with the Earth's atmosphere [m/s].
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
float or numpy.ndarray
|
|
139
|
+
Ablation coefficient [1]
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
return 2 * beta / ((1 - shape_change_coefficient) * initial_velocity**2)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def area_to_mass_ratio(
|
|
146
|
+
alpha,
|
|
147
|
+
aerodynamic_cd,
|
|
148
|
+
sea_level_rho,
|
|
149
|
+
atmospheric_scale_height,
|
|
150
|
+
radiant_local_elevation,
|
|
151
|
+
degrees=False,
|
|
152
|
+
):
|
|
153
|
+
"""Area to mass ratio from alpha."""
|
|
154
|
+
el = np.radians(radiant_local_elevation) if degrees else radiant_local_elevation
|
|
155
|
+
sin_gamma = np.sin(el)
|
|
156
|
+
return 2 * alpha * sin_gamma / (aerodynamic_cd * sea_level_rho * atmospheric_scale_height)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def alpha_from_ballistic_coefficient(
|
|
160
|
+
ballistic_coefficient,
|
|
161
|
+
sea_level_rho,
|
|
162
|
+
atmospheric_scale_height,
|
|
163
|
+
radiant_local_elevation,
|
|
164
|
+
degrees=False,
|
|
165
|
+
):
|
|
166
|
+
el = np.radians(radiant_local_elevation) if degrees else radiant_local_elevation
|
|
167
|
+
return sea_level_rho * atmospheric_scale_height / (2 * ballistic_coefficient * np.sin(el))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ballistic_coefficient_from_alpha(
|
|
171
|
+
alpha,
|
|
172
|
+
sea_level_rho,
|
|
173
|
+
atmospheric_scale_height,
|
|
174
|
+
radiant_local_elevation,
|
|
175
|
+
degrees=False,
|
|
176
|
+
):
|
|
177
|
+
el = np.radians(radiant_local_elevation) if degrees else radiant_local_elevation
|
|
178
|
+
return sea_level_rho * atmospheric_scale_height / (2 * alpha * np.sin(el))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def shape_factor_direct(initial_cross_section, initial_mass, bulk_density):
|
|
182
|
+
"""
|
|
183
|
+
Shape factor $ A_e $ is defined as the ratio between the body cross-sectional
|
|
184
|
+
area $ S_e $ and its volume $ W_e^{\\frac{2}{3}} $ to the 2/3'd power [^1]. To not
|
|
185
|
+
have to work with volume, the formula is re-written using the bulk density to
|
|
186
|
+
|
|
187
|
+
$$
|
|
188
|
+
A_e = \\frac{S_e}{W_e^{\\frac{2}{3}}} = S_e\\frac{\\rho^{\\frac{2}{3}}}{M_e^{\\frac{2}{3}}}
|
|
189
|
+
$$
|
|
190
|
+
|
|
191
|
+
[^1]: Gritsevich MI (2009) Determination of parameters of meteor bodies based
|
|
192
|
+
on flight observational data. Advances in Space Research 44:323-334.
|
|
193
|
+
https://doi.org/10.1016/j.asr.2009.03.030
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
return initial_cross_section * (bulk_density / initial_mass) ** (2.0 / 3.0)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def initial_mass_direct(
|
|
200
|
+
alpha,
|
|
201
|
+
aerodynamic_cd,
|
|
202
|
+
sea_level_rho,
|
|
203
|
+
atmospheric_scale_height,
|
|
204
|
+
radiant_local_elevation,
|
|
205
|
+
bulk_density,
|
|
206
|
+
shape_factor,
|
|
207
|
+
degrees=False,
|
|
208
|
+
):
|
|
209
|
+
"""Directly compute initial mass from analytic solution coefficients"""
|
|
210
|
+
|
|
211
|
+
el = np.radians(radiant_local_elevation) if degrees else radiant_local_elevation
|
|
212
|
+
sin_gamma = np.sin(el)
|
|
213
|
+
return (
|
|
214
|
+
0.5
|
|
215
|
+
* aerodynamic_cd
|
|
216
|
+
* sea_level_rho
|
|
217
|
+
* atmospheric_scale_height
|
|
218
|
+
* shape_factor
|
|
219
|
+
/ (alpha * sin_gamma * bulk_density ** (2.0 / 3.0))
|
|
220
|
+
) ** 3
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def mass_direct(velocity, initial_mass, beta, shape_change_coefficient, initial_velocity=None):
|
|
224
|
+
"""Shorthand for `norm_mass_direct` scaled to physical units"""
|
|
225
|
+
return initial_mass * norm_mass_direct(
|
|
226
|
+
velocity, beta, shape_change_coefficient, initial_velocity=initial_velocity
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def norm_mass_direct(velocity, beta, shape_change_coefficient, initial_velocity=None):
|
|
231
|
+
"""The solution for mass from the analytic solutions of the ablation eqations [^1].
|
|
232
|
+
|
|
233
|
+
$$
|
|
234
|
+
M = M_e e^{- \\frac{\\beta}{1 - \\mu} \\left ( 1 - \\left ( \\frac{V}{V_e} \\right )^2 \\right ) }
|
|
235
|
+
$$
|
|
236
|
+
|
|
237
|
+
[^1]: Sansom EK, Gritsevich M, Devillepoix HAR, et al (2019)
|
|
238
|
+
Determining Fireball Fates Using the α–β Criterion. ApJ 885:115.
|
|
239
|
+
https://doi.org/10.3847/1538-4357/ab4516
|
|
240
|
+
"""
|
|
241
|
+
v = velocity
|
|
242
|
+
if initial_velocity is not None:
|
|
243
|
+
v = v / initial_velocity
|
|
244
|
+
return np.exp(-beta / (1 - shape_change_coefficient) * (1 - v**2))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def height_direct(velocity, atmospheric_scale_height, alpha, beta, initial_velocity=None):
|
|
248
|
+
"""Shorthand for `norm_mass_direct` scaled to physical units"""
|
|
249
|
+
return atmospheric_scale_height * norm_height_direct(
|
|
250
|
+
velocity, alpha, beta, initial_velocity=initial_velocity
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def approx_norm_velocity(
|
|
255
|
+
height,
|
|
256
|
+
alpha,
|
|
257
|
+
beta,
|
|
258
|
+
atmospheric_scale_height=None,
|
|
259
|
+
start=1 - 1e-12,
|
|
260
|
+
root_find_kwargs={"tol": 1e-12, "maxiter": 200},
|
|
261
|
+
):
|
|
262
|
+
"""Approximate solution for velocity for large beta as a function of height from the analytic
|
|
263
|
+
solutions of the ablation eqations.
|
|
264
|
+
"""
|
|
265
|
+
Yvalues = height
|
|
266
|
+
if atmospheric_scale_height is not None:
|
|
267
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
268
|
+
|
|
269
|
+
if not isinstance(Yvalues, np.ndarray):
|
|
270
|
+
Yvalues = np.array([Yvalues])
|
|
271
|
+
|
|
272
|
+
def func_exact(v, y):
|
|
273
|
+
return np.log(alpha) + 0.83 * beta * (1 - v) - np.log(-np.log(v)) - y
|
|
274
|
+
|
|
275
|
+
def abs_func_exact(v, y):
|
|
276
|
+
return np.abs(np.log(alpha) + 0.83 * beta * (1 - v) - np.log(-np.log(v)) - y)
|
|
277
|
+
|
|
278
|
+
def dabs_func_exact(v, y):
|
|
279
|
+
return (-0.83 * beta - 1 / (v * np.log(v))) * np.sign(func_exact(v, y))
|
|
280
|
+
|
|
281
|
+
Vvalues = np.zeros_like(Yvalues)
|
|
282
|
+
for ind in range(len(Yvalues)):
|
|
283
|
+
try:
|
|
284
|
+
Vvalues[ind] = sco.newton(
|
|
285
|
+
abs_func_exact,
|
|
286
|
+
start,
|
|
287
|
+
fprime=dabs_func_exact,
|
|
288
|
+
args=(Yvalues[ind],),
|
|
289
|
+
**root_find_kwargs,
|
|
290
|
+
)
|
|
291
|
+
except RuntimeError:
|
|
292
|
+
Vvalues[ind] = np.nan
|
|
293
|
+
|
|
294
|
+
if not isinstance(height, np.ndarray):
|
|
295
|
+
Vvalues = Vvalues[0]
|
|
296
|
+
return Vvalues
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def approx_norm_height_direct(velocity, alpha, beta, initial_velocity=None):
|
|
300
|
+
"""Approximate solution for height for large beta as a function of velocity from the analytic
|
|
301
|
+
solutions of the ablation eqations.
|
|
302
|
+
"""
|
|
303
|
+
v = velocity
|
|
304
|
+
if initial_velocity is not None:
|
|
305
|
+
v = v / initial_velocity
|
|
306
|
+
return np.log(alpha) + 0.83 * beta * (1 - v) - np.log(-np.log(v))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def norm_height_direct(velocity, alpha, beta, initial_velocity=None):
|
|
310
|
+
"""The solution for height as a function of velocity from the analytic
|
|
311
|
+
solutions of the ablation eqations.
|
|
312
|
+
"""
|
|
313
|
+
v = velocity
|
|
314
|
+
if initial_velocity is not None:
|
|
315
|
+
v = v / initial_velocity
|
|
316
|
+
return np.log(alpha) + beta - np.log((scs.expi(beta) - scs.expi(beta * v**2)) * 0.5)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def velocity_direct(mass, beta, initial_velocity, shape_change_coefficient, initial_mass=None):
|
|
320
|
+
"""Shorthand..."""
|
|
321
|
+
return initial_velocity * norm_velocity_direct(
|
|
322
|
+
mass, beta, shape_change_coefficient, initial_mass=initial_mass
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def norm_velocity_direct(mass, beta, shape_change_coefficient, initial_mass=None):
|
|
327
|
+
"""Inverse of `norm_mass_direct`"""
|
|
328
|
+
m = mass
|
|
329
|
+
if initial_mass is not None:
|
|
330
|
+
m = m / initial_mass
|
|
331
|
+
return np.sqrt(np.abs(1 + np.log(m) * (1 - shape_change_coefficient) / beta))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def velocity_estimate(height, initial_velocity, alpha, beta, atmospheric_scale_height=None):
|
|
335
|
+
"""Shorthand for `norm_mass_direct` scaled to physical units"""
|
|
336
|
+
return initial_velocity * norm_velocity_estimate(
|
|
337
|
+
height, alpha, beta, atmospheric_scale_height=atmospheric_scale_height
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def norm_velocity_estimate_brute(
|
|
342
|
+
height,
|
|
343
|
+
alpha,
|
|
344
|
+
beta,
|
|
345
|
+
atmospheric_scale_height=None,
|
|
346
|
+
resolution=1000,
|
|
347
|
+
edge_delta=1e-10,
|
|
348
|
+
sampling=None,
|
|
349
|
+
):
|
|
350
|
+
"""Brute force approach to estimating velocity as a function of height from the analytic
|
|
351
|
+
solutions of the ablation equations.
|
|
352
|
+
"""
|
|
353
|
+
y = height
|
|
354
|
+
if atmospheric_scale_height is not None:
|
|
355
|
+
y = y / atmospheric_scale_height
|
|
356
|
+
|
|
357
|
+
if sampling is None:
|
|
358
|
+
# Sample according to a rescaled inverse exponential line to match the
|
|
359
|
+
# tangent space of the interpolated function
|
|
360
|
+
v_grid = 1 - np.exp(np.linspace(np.log(1 - edge_delta), np.log(edge_delta), resolution))
|
|
361
|
+
else:
|
|
362
|
+
v_grid = sampling
|
|
363
|
+
h_grid = norm_height_direct(v_grid, alpha, beta)
|
|
364
|
+
interp = sci.interp1d(h_grid, v_grid, kind="linear", fill_value=np.nan, bounds_error=False)
|
|
365
|
+
return interp(y)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def norm_velocity_estimate(
|
|
369
|
+
height,
|
|
370
|
+
alpha,
|
|
371
|
+
beta,
|
|
372
|
+
atmospheric_scale_height=None,
|
|
373
|
+
start=1 - 1e-12,
|
|
374
|
+
minimize_kwargs={"method": "Nelder-Mead"},
|
|
375
|
+
):
|
|
376
|
+
"""Root-solver estimation of velocity as a function of height from the analytic
|
|
377
|
+
solutions of the ablation equations.
|
|
378
|
+
"""
|
|
379
|
+
Yvalues = height
|
|
380
|
+
if atmospheric_scale_height is not None:
|
|
381
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
382
|
+
|
|
383
|
+
if not isinstance(Yvalues, np.ndarray):
|
|
384
|
+
Yvalues = np.array([Yvalues])
|
|
385
|
+
|
|
386
|
+
def func_exact(v, y):
|
|
387
|
+
return np.abs(
|
|
388
|
+
np.log(alpha) + beta - np.log((scs.expi(beta) - scs.expi(beta * v**2)) * 0.5) - y
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# If beta is larger than 500, the expi estimation algorithms start having trouble
|
|
392
|
+
# as they use float64, we need scs.expi(beta) to work to compute the difference value
|
|
393
|
+
if beta > 600:
|
|
394
|
+
if isinstance(height, np.ndarray):
|
|
395
|
+
return np.full_like(height, np.nan)
|
|
396
|
+
else:
|
|
397
|
+
return np.nan
|
|
398
|
+
|
|
399
|
+
Vvalues = np.zeros_like(Yvalues)
|
|
400
|
+
for ind in range(len(Yvalues)):
|
|
401
|
+
res = sco.minimize(func_exact, start, args=(Yvalues[ind],), **minimize_kwargs)
|
|
402
|
+
Vvalues[ind] = res.x
|
|
403
|
+
|
|
404
|
+
if not isinstance(height, np.ndarray):
|
|
405
|
+
Vvalues = Vvalues[0]
|
|
406
|
+
return Vvalues
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def final_mass_direct(
|
|
410
|
+
final_velocity,
|
|
411
|
+
initial_mass,
|
|
412
|
+
beta,
|
|
413
|
+
shape_change_coefficient,
|
|
414
|
+
initial_velocity=None,
|
|
415
|
+
):
|
|
416
|
+
"""Final mass based on the analytic solutions in
|
|
417
|
+
[`mass_direct`][`ablate.ablation.alpha_beta.mass_direct`].
|
|
418
|
+
"""
|
|
419
|
+
return mass_direct(
|
|
420
|
+
final_velocity,
|
|
421
|
+
initial_mass,
|
|
422
|
+
beta,
|
|
423
|
+
shape_change_coefficient,
|
|
424
|
+
initial_velocity=initial_velocity,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def scale_hight_to_exponential_atm(atm_total_mass_density, atmospheric_scale_height, sea_level_rho):
|
|
429
|
+
"""Scale heights using an arbitrary but strictly decreeing atmospheric
|
|
430
|
+
density model as a function of the original height, so that the new heights gives the
|
|
431
|
+
same density using a simple exponential atmospheric density model.
|
|
432
|
+
"""
|
|
433
|
+
return -np.log(atm_total_mass_density / sea_level_rho) * atmospheric_scale_height
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def scale_hight_to_model_atm(
|
|
437
|
+
height,
|
|
438
|
+
atmosphere,
|
|
439
|
+
sea_level_rho,
|
|
440
|
+
density_args=(),
|
|
441
|
+
density_kwargs={},
|
|
442
|
+
atmospheric_scale_height=None,
|
|
443
|
+
lims=None,
|
|
444
|
+
root_find_kwargs={},
|
|
445
|
+
):
|
|
446
|
+
"""Scale heights from an exponential model to an arbitrary but strictly decreeing atmospheric
|
|
447
|
+
density model as a function of the new height, so that the new heights in the atmospheric
|
|
448
|
+
model gives the same density as the simple exponential model.
|
|
449
|
+
"""
|
|
450
|
+
Yvalues = height
|
|
451
|
+
if atmospheric_scale_height is not None:
|
|
452
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
453
|
+
|
|
454
|
+
def get_density(h):
|
|
455
|
+
d = atmosphere.density(*density_args, alt=h, **density_kwargs)
|
|
456
|
+
return d["Total"].values.squeeze()
|
|
457
|
+
|
|
458
|
+
def func_exact(h, y):
|
|
459
|
+
return sea_level_rho * np.exp(-y) - get_density(h)
|
|
460
|
+
|
|
461
|
+
scaled_height = np.zeros_like(height)
|
|
462
|
+
for ind in range(len(height)):
|
|
463
|
+
_lim = lims if lims is not None else [height[ind] * 0.7, height[ind] * 1.3]
|
|
464
|
+
scaled_height[ind] = sco.bisect(
|
|
465
|
+
func_exact, _lim[0], _lim[1], args=(Yvalues[ind],), **root_find_kwargs
|
|
466
|
+
)
|
|
467
|
+
return scaled_height
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def atmosphere_density(height, atmospheric_scale_height, sea_level_rho):
|
|
471
|
+
"""The atmospheric density model used by alpha-beta"""
|
|
472
|
+
return sea_level_rho * np.exp(-height / atmospheric_scale_height)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def Q5(x, velocities, yvals, fill_value=10.0):
|
|
476
|
+
""" """
|
|
477
|
+
if len(x.shape) == 1:
|
|
478
|
+
x.shape = (x.size, 1)
|
|
479
|
+
size = x.shape[1]
|
|
480
|
+
|
|
481
|
+
res = np.zeros((size,))
|
|
482
|
+
for i in range(len(velocities)):
|
|
483
|
+
vval = velocities[i] / (x[2, ...] * 1000.0)
|
|
484
|
+
r0 = 2 * x[0, ...] * np.exp(-yvals[i]) - (
|
|
485
|
+
(scs.expi(x[1, ...]) - scs.expi(x[1, ...] * vval**2)) * np.exp(-x[1, ...])
|
|
486
|
+
)
|
|
487
|
+
r0[np.isnan(r0)] = fill_value
|
|
488
|
+
res += r0**2
|
|
489
|
+
if res.size == 1:
|
|
490
|
+
return res[0]
|
|
491
|
+
else:
|
|
492
|
+
return res
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def solve_alpha_beta_velocity_versionQ5(
|
|
496
|
+
velocities,
|
|
497
|
+
heights,
|
|
498
|
+
atmospheric_scale_height=None,
|
|
499
|
+
start=None,
|
|
500
|
+
bounds=((1e-2, 1e6), (1e-4, 1e3), (0, None)),
|
|
501
|
+
minimize_kwargs={},
|
|
502
|
+
):
|
|
503
|
+
"""Solve for alpha and beta using minimization of the least squares of the
|
|
504
|
+
observables input into the preserved analytical relation [^1].
|
|
505
|
+
|
|
506
|
+
Adapted from [^2] and [^3].
|
|
507
|
+
|
|
508
|
+
[^1]: In preparation.
|
|
509
|
+
[^2]: https://github.com/desertfireballnetwork/alpha_beta_modules
|
|
510
|
+
[^3]: https://doi.org/10.1016/j.asr.2009.03.030
|
|
511
|
+
"""
|
|
512
|
+
Yvalues = heights
|
|
513
|
+
if atmospheric_scale_height is not None:
|
|
514
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
515
|
+
|
|
516
|
+
b0 = 1.0
|
|
517
|
+
a0 = np.exp(Yvalues[-1]) / (2.0 * b0)
|
|
518
|
+
# /1000 is a hack to make velocities small so minimisation doesnt use stupid steps
|
|
519
|
+
v0 = np.nanmean(velocities[:10]) / 1000
|
|
520
|
+
if start is None:
|
|
521
|
+
start = [a0, b0, v0]
|
|
522
|
+
else:
|
|
523
|
+
if start[1] is None:
|
|
524
|
+
start[1] = b0
|
|
525
|
+
if start[0] is None:
|
|
526
|
+
start[0] = np.exp(Yvalues[-1]) / (2.0 * start[1])
|
|
527
|
+
|
|
528
|
+
if start[2] is None:
|
|
529
|
+
start[2] = v0
|
|
530
|
+
else:
|
|
531
|
+
start[2] *= 1e-3
|
|
532
|
+
|
|
533
|
+
res = sco.minimize(Q5, start, args=(velocities, Yvalues), bounds=bounds, **minimize_kwargs)
|
|
534
|
+
out = res.x
|
|
535
|
+
out[2] *= 1000.0 # fix velocities for return
|
|
536
|
+
return out
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def Q4(x, vvals, yvals):
|
|
540
|
+
""" """
|
|
541
|
+
res = 0.0
|
|
542
|
+
for i in range(len(vvals)):
|
|
543
|
+
r0 = 2 * x[0, ...] * np.exp(-yvals[i])
|
|
544
|
+
r0 -= (scs.expi(x[1, ...]) - scs.expi(x[1, ...] * vvals[i] ** 2)) * np.exp(-x[1, ...])
|
|
545
|
+
res += pow(r0, 2)
|
|
546
|
+
return res
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def solve_alpha_beta_versionQ4(
|
|
550
|
+
velocities,
|
|
551
|
+
heights,
|
|
552
|
+
initial_velocity=None,
|
|
553
|
+
atmospheric_scale_height=None,
|
|
554
|
+
start=None,
|
|
555
|
+
bounds=((1e-3, 1e4), (1e-5, 1e3)),
|
|
556
|
+
minimize_kwargs={},
|
|
557
|
+
):
|
|
558
|
+
"""Solve for alpha and beta using minimization of the least squares of the
|
|
559
|
+
observables input into the preserved analytical relation [^1].
|
|
560
|
+
|
|
561
|
+
[^1]: Gritsevich MI (2008) Identification of fireball dynamic parameters.
|
|
562
|
+
Moscow University Mechanics Bulletin 63:1-5.
|
|
563
|
+
https://doi.org/10.1007/s11971-008-1001-5
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
Yvalues = heights
|
|
567
|
+
if atmospheric_scale_height is not None:
|
|
568
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
569
|
+
|
|
570
|
+
Vvalues = velocities
|
|
571
|
+
if initial_velocity is not None:
|
|
572
|
+
Vvalues = Vvalues / initial_velocity
|
|
573
|
+
# TODO: automatic better initial guess for beta
|
|
574
|
+
b0 = 1.0
|
|
575
|
+
a0 = np.exp(Yvalues[-1]) / (2.0 * b0)
|
|
576
|
+
if start is None:
|
|
577
|
+
start = [a0, b0]
|
|
578
|
+
else:
|
|
579
|
+
if start[1] is None:
|
|
580
|
+
start[1] = b0
|
|
581
|
+
if start[0] is None:
|
|
582
|
+
start[0] = np.exp(Yvalues[-1]) / (2.0 * start[1])
|
|
583
|
+
|
|
584
|
+
res = sco.minimize(Q4, start, args=(Vvalues, Yvalues), bounds=bounds, **minimize_kwargs)
|
|
585
|
+
return res.x
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def logposterior_alpha_beta(
|
|
589
|
+
alpha,
|
|
590
|
+
beta,
|
|
591
|
+
heights,
|
|
592
|
+
velocities,
|
|
593
|
+
velocities_std,
|
|
594
|
+
initial_velocity=None,
|
|
595
|
+
atmospheric_scale_height=None,
|
|
596
|
+
inverse_kwargs={},
|
|
597
|
+
):
|
|
598
|
+
Vvalues = velocities
|
|
599
|
+
Vstd = velocities_std
|
|
600
|
+
if initial_velocity is not None:
|
|
601
|
+
Vvalues = Vvalues / initial_velocity
|
|
602
|
+
Vstd = Vstd / initial_velocity
|
|
603
|
+
|
|
604
|
+
Yvalues = heights
|
|
605
|
+
if atmospheric_scale_height is not None:
|
|
606
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
607
|
+
# TODO: propagate a gaussian trough alpha-beta solution to also include h_std in the posterior
|
|
608
|
+
vn = norm_velocity_estimate(
|
|
609
|
+
Yvalues, alpha, beta, atmospheric_scale_height=None, **inverse_kwargs
|
|
610
|
+
)
|
|
611
|
+
return -0.5 * np.sum(((Vvalues - vn) / Vstd) ** 2)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def solve_alpha_beta_posterior(
|
|
615
|
+
velocities,
|
|
616
|
+
velocities_std,
|
|
617
|
+
heights,
|
|
618
|
+
atmospheric_scale_height=None,
|
|
619
|
+
start=None,
|
|
620
|
+
bounds=((0.01, 1000000.0), (0.0001, 200.0), (0, None)),
|
|
621
|
+
minimize_kwargs={},
|
|
622
|
+
inverse_kwargs={},
|
|
623
|
+
):
|
|
624
|
+
"""Solve for alpha and beta using minimization of the posterior [^1].
|
|
625
|
+
|
|
626
|
+
[^1]: Kastinen et al (202x) in preparation
|
|
627
|
+
"""
|
|
628
|
+
Yvalues = heights
|
|
629
|
+
if atmospheric_scale_height is not None:
|
|
630
|
+
Yvalues = Yvalues / atmospheric_scale_height
|
|
631
|
+
|
|
632
|
+
b0 = 1.0
|
|
633
|
+
a0 = np.exp(Yvalues[-1]) / (2.0 * b0)
|
|
634
|
+
v0 = velocities[0]
|
|
635
|
+
if start is None:
|
|
636
|
+
start = [a0, b0, v0]
|
|
637
|
+
else:
|
|
638
|
+
if start[1] is None:
|
|
639
|
+
start[1] = b0
|
|
640
|
+
if start[0] is None:
|
|
641
|
+
start[0] = np.exp(Yvalues[-1]) / (2.0 * start[1])
|
|
642
|
+
if start[2] is None:
|
|
643
|
+
start[2] = v0
|
|
644
|
+
|
|
645
|
+
def wrap_func(x):
|
|
646
|
+
return -logposterior_alpha_beta(
|
|
647
|
+
x[0],
|
|
648
|
+
x[1],
|
|
649
|
+
Yvalues,
|
|
650
|
+
velocities,
|
|
651
|
+
velocities_std,
|
|
652
|
+
initial_velocity=x[2],
|
|
653
|
+
atmospheric_scale_height=None,
|
|
654
|
+
inverse_kwargs=inverse_kwargs,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
res = sco.minimize(wrap_func, start, bounds=bounds, **minimize_kwargs)
|
|
658
|
+
out = res.x
|
|
659
|
+
return out
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def velocity_smoother_dfn(velocity, iters=2):
|
|
663
|
+
"""Velocity smoother based on consistency of neighboring data points,
|
|
664
|
+
adapted from [^1]. Removes the first and last point `iters` times.
|
|
665
|
+
|
|
666
|
+
[^1]: https://github.com/desertfireballnetwork/alpha_beta_modules
|
|
667
|
+
"""
|
|
668
|
+
vel_smooth = velocity.copy()
|
|
669
|
+
for ind in range(iters):
|
|
670
|
+
vel_smooth = (vel_smooth[:-2] + vel_smooth[1:-1] + vel_smooth[2:]) / 3.0
|
|
671
|
+
return vel_smooth
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def velocity_smoother_mov_avg(velocity, window=7):
|
|
675
|
+
movavg = np.cumsum(velocity)
|
|
676
|
+
movavg[window:] = movavg[window:] - movavg[:-window]
|
|
677
|
+
return movavg[(window - 1):] / window
|
|
678
|
+
|
|
679
|
+
|