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.
@@ -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
+