diffenix 0.0.0.1__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,1510 @@
1
+ import numpy as np
2
+ from scipy.integrate import trapezoid, cumulative_trapezoid, quad, solve_ivp
3
+ from scipy.interpolate import Akima1DInterpolator, FloaterHormannInterpolator, PchipInterpolator
4
+ from scipy.optimize import root
5
+ from scipy.signal import savgol_filter
6
+ import phenigraph as g
7
+
8
+ from diffenix.Constantes import *
9
+ from diffenix.numericals_methods import linear_interpolation, rk4, gradient2
10
+
11
+
12
+ def linear_interp(t_new, t_old, values):
13
+ if isinstance(t_new, list | np.ndarray):
14
+ return np.array([linear_interp(t_new=t, t_old=t_old, values=values) for t in t_new])
15
+
16
+ i = np.argmin(np.abs(t_new - np.array(t_old)))
17
+ if i == 0 or (i < len(t_old) - 1 and t_new > t_old[i]):
18
+ t1 = t_old[i]
19
+ t2 = t_old[i + 1]
20
+ alpha = (t_new - t1) / (t2 - t1)
21
+ return (1 - alpha) * values[i] + alpha * values[i + 1]
22
+ else:
23
+ t1 = t_old[i - 1]
24
+ t2 = t_old[i]
25
+ alpha = (t_new - t1) / (t2 - t1)
26
+ return (1 - alpha) * values[i - 1] + alpha * values[i]
27
+
28
+
29
+ class Compteur:
30
+ """
31
+ This object can be used to estimate the number of calls for a function if given in parameter
32
+ and incremented in the function (mutable object)
33
+
34
+ """
35
+
36
+ def __init__(self):
37
+ self.value: int = 0
38
+
39
+
40
+ def number_density(a, amax: np.float64, abig: np.float64, amed: np.float64, amin: np.float64,
41
+ qh: np.float64, qm: np.float64, ql: np.float64,
42
+ Mtot: np.float64 = np.double(1.), rho: np.float64 = np.double(1.)) -> np.float64 | np.ndarray:
43
+ """
44
+
45
+ Return the number density of asteroid of size a
46
+
47
+ Parameters
48
+ ----------
49
+ a : np.float64 | np.ndarray
50
+ The diameter(s) of asteroids
51
+ amax : np.float64
52
+ The maximum diameter
53
+ abig : np.float64
54
+ A reference diameter
55
+ amed : np.float64
56
+
57
+ amin : np.float64
58
+ The minimum diameter
59
+ qh : np.float64
60
+ The slope of distribution for the highest radius
61
+ qm : np.float64
62
+ The slope of distribution for medium seized asteroids
63
+ ql : np.float64
64
+ The slope of distribution for the smallest asteroids
65
+ Mtot : np.float64, optional, default=1
66
+ The total mass of the belt.
67
+ The result is directly proportional to Mtot.
68
+ number_density(Mtot)=Mtot*number_density(1.).
69
+ It can be used to estimate the number density
70
+ in a belt = sigma*number_density(1.) with sigma the surface density
71
+ rho : np.float64
72
+ The mean density of asteroids
73
+
74
+ Returns
75
+ -------
76
+ np.float64 | np.ndarray
77
+ The number density of asteroids for diameter(s) a
78
+
79
+ """
80
+
81
+ # Normalization of the distribution. Nmax is the value of the number density for a=amax calculated such as
82
+ # the integration of the number density multiplied by the mass of asteroids for a diameter a giving the total mass
83
+ Nmax: np.float64 = (6. * Mtot
84
+ / (Pi * rho
85
+ * (amax * amax * amax * amax / (qh + 4.)
86
+ + ((abig / amax) ** qh)
87
+ * ((abig * abig * abig * abig * (1. / (qm + 4.) - 1 / (qh + 4.)))
88
+ + ((amed / abig) ** qm)
89
+ * (amed * amed * amed * amed * (1. / (ql + 4.) - 1. / (qm + 4.))
90
+ - ((amin / amed) ** ql) * amin * amin * amin * amin / (ql + 4.)))
91
+ )))
92
+ # Nmax: np.float64 = (6. * Mbelt
93
+ # / (Pi * rho_refr
94
+ # * (((abig / amax) ** qh) * ((amed / abig) ** qm) * ql * (1 - (amin / amed) ** (ql - 1))
95
+ # + ((abig / amax) ** qh) * qm * (1 - (amed / abig) ** (qm - 1))
96
+ # + qh * (1 - (abig / amax) ** (qh - 1)))))
97
+ if type(a) is np.ndarray:
98
+ n: np.ndarray = np.zeros_like(a)
99
+ idx: np.ndarray = np.intersect1d(np.argwhere(a >= amin)[:, 0], np.argwhere(a < amed)[:, 0])
100
+ n[idx] = Nmax * ((abig / amax) ** qh) * ((amed / abig) ** qm) * ((a[idx] / amed) ** ql)
101
+ idx: np.ndarray = np.intersect1d(np.argwhere(a >= amed)[:, 0], np.argwhere(a < abig)[:, 0])
102
+ n[idx] = Nmax * ((abig / amax) ** qh) * ((a[idx] / abig) ** qm)
103
+ idx: np.ndarray = np.intersect1d(np.argwhere(a >= abig)[:, 0], np.argwhere(a <= amax)[:, 0])
104
+ n[idx] = Nmax * ((a[idx] / amax) ** qh)
105
+ return n
106
+ elif a < amin or a > amax:
107
+ return zero
108
+ elif a < amed:
109
+ return Nmax * ((abig / amax) ** qh) * ((amed / abig) ** qm) * ((a / amed) ** ql)
110
+ elif a < abig:
111
+ return Nmax * ((abig / amax) ** qh) * ((a / abig) ** qm)
112
+ else:
113
+ return Nmax * ((a / amax) ** qh)
114
+
115
+
116
+ mu_H2O: np.float64 = np.double(18. * 1e-3) * C_M
117
+ Tref: np.float64 = np.double(273.) # K
118
+ hvap: np.float64 = np.double(2.78e6) * C_E / C_M
119
+ Bh2o: np.float64 = np.double(mu_H2O * hvap / Rgp)
120
+ Ah2o: np.float64 = np.double(1e5 * C_M / (C_t * C_L)) * np.exp(Bh2o / Tref)
121
+
122
+
123
+ def temp_belt(r: np.ndarray | np.float64, t: np.float64 | np.ndarray, consts: dict
124
+ ) -> np.ndarray | np.float64:
125
+ """
126
+
127
+ Calculate and return the temperature at radius r and time t
128
+
129
+ Parameters
130
+ ----------
131
+ r : np.ndarray | np.float64
132
+ radius
133
+ t : np.ndarray | np.float64
134
+ the current time
135
+ consts : dict
136
+ the constantes
137
+ Returns
138
+ -------
139
+ np.ndarray | np.float64
140
+
141
+ """
142
+ if "Ms" in consts.keys():
143
+ if interpLbol is not None:
144
+ temp0 = (278 * (((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4)) * (
145
+ (a0 / au) ** (-1 / 2)))
146
+ else:
147
+ temp0 = (278 * (((1. - consts["Abelt"]) * consts["Ls"] / Lsun) ** (1 / 4)) * (
148
+ (a0 / au) ** (-1 / 2)))
149
+ else:
150
+ if interpLbol is not None:
151
+ temp0 = (278 * (((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4)) * (
152
+ (a0 / au) ** (-1 / 2)))
153
+ else:
154
+ temp0 = (278 * (((1. - consts["Abelt"]) * consts["Ls"] / Lsun) ** (1 / 4)) * ((a0 / au) ** (-1 / 2)))
155
+ if isinstance(t, float | np.float64 | int | np.int64) and t < 0:
156
+ return zero
157
+ elif t.shape == r.shape and np.any(t < zero):
158
+ # temp[t < consts["t0"]] = 10
159
+ temp0[t < consts["t0"]] = 10
160
+
161
+ temp = temp0 * (abs(r) / a0) ** (- 1 / 2)
162
+ return temp
163
+
164
+
165
+ def tau_diff(pp: np.ndarray, rr: np.ndarray, t: np.float64 | np.ndarray, consts: dict) -> np.ndarray:
166
+ """
167
+ Calculate the diffusion timescale at time t and depth p
168
+
169
+ Parameters
170
+ ----------
171
+
172
+ p: np.ndarray
173
+ The depth at which the gas is realised
174
+
175
+ r: np.ndarray
176
+ The distance from the central star at which the gas is realised
177
+
178
+ t: np.ndarray
179
+ The time at which the gas is realised
180
+
181
+ consts
182
+ All the necessary constants
183
+
184
+ Returns
185
+ -------
186
+ np.ndarray
187
+ The time needed for the gas to go at the surface
188
+
189
+ """
190
+ return (3. * pp * pp * np.sqrt(2. * Pi * mu_H2O
191
+ / (Rgp * temp_belt(rr, t - pp * pp / consts["K"], consts)))
192
+ / (4. * consts["Phi"] * consts["rp"]))
193
+
194
+
195
+ def sigmap_rho(t: np.float64 | np.ndarray,
196
+ rr: np.ndarray, pp: np.ndarray,
197
+ consts: dict, comp: Compteur = Compteur()) \
198
+ -> np.ndarray:
199
+ """
200
+ Calculate and return the gas generation rate at depth p, radius r and time t.
201
+ This doesn't take into acompte the limited amonth of material
202
+
203
+ Parameters
204
+ ----------
205
+ t : np.ndarray | np.float64
206
+ The current time
207
+ r : np.ndarray | np.float64
208
+ The distance to the central star (semi-major axis)
209
+ p : np.ndarray | np.float64
210
+ The depth into the asteroid/KBO
211
+ consts : dict
212
+ The constantes
213
+ comp : Compteur, optional
214
+ To estimate the number of iterations
215
+
216
+ Returns
217
+ -------
218
+ np.ndarray | np.float64
219
+
220
+ """
221
+
222
+ S: np.float64 = 3. * (1. - consts["Phi"]) / consts["rp"]
223
+ T: np.ndarray = temp_belt(rr, t - pp * pp / consts["K"], consts)
224
+ # The pp * pp / consts["K"] factor model the thermal diffusion
225
+
226
+ drho_sub = (S * Ah2o * np.exp(-Bh2o / T)
227
+ * np.sqrt(mu_H2O / (2. * Pi * Rgp * T)))
228
+ if comp.value % 1000 == 0:
229
+ print("Iteration ", comp.value, ": t=", t.max() / Myr, " Myr")
230
+ # print("Iteration ", comp.value, ": t=", t / Myr, " Myr")
231
+
232
+ comp.value += 1
233
+ return drho_sub
234
+
235
+
236
+ def minit(r: np.ndarray, pinit: np.float64, rho_ice: np.float64, a0: np.float64, delta_a: np.float64,
237
+ Mbelt: np.float64, kwargsN) -> np.float64:
238
+ p: np.ndarray = np.geomspace(kwargsN['amin'], kwargsN['amax'], 10000)
239
+ rr: np.ndarray = r[:, np.newaxis] + np.zeros_like(p)
240
+ pp: np.ndarray = (p[:, np.newaxis] + np.zeros_like(r)).T
241
+ rhod_control = 4. * Pi * cumulative_trapezoid(p * p * rho_ice * np.ones_like(pp), p, axis=1, initial=zero)
242
+ rhod_control[pp > pinit] *= 0
243
+ sigma0: np.ndarray = (Mbelt * ((r / a0) ** (-3. / 2.))
244
+ / (4. * Pi * a0 * a0 * (np.sqrt(1. + delta_a / a0)
245
+ - np.sqrt(1. - delta_a / a0)))) # initial surface density
246
+ ni: np.ndarray = ((np.array([number_density(2. * d_2, Mtot=np.double(1.), **kwargsN)
247
+ for d_2 in p])[:, np.newaxis] + np.zeros(len(r))).T
248
+ * (sigma0[:, np.newaxis] + np.zeros(len(p)))) # number density
249
+ sig_init: np.ndarray = trapezoid(ni * rhod_control, 2 * p)
250
+ return trapezoid(2. * Pi * r * sig_init, r)
251
+
252
+
253
+ def sigma_dot(Mbelt: np.float64, r: np.ndarray,
254
+ tps: np.ndarray,
255
+ a0: np.float64, delta_a: np.float64,
256
+ tinit: np.float64, tmax: np.float64,
257
+ f_ice: np.float64, rho_refr: np.float64, rho_ice: np.float64,
258
+ Abelt: np.float64, K: np.float64,
259
+ Its: int, Phi: np.float64,
260
+ dmin: np.float64, dmax: np.float64, Its_p: int,
261
+ rp: np.float64,
262
+ Itt: int, kwargsN: dict, **kwargs
263
+ ) -> np.ndarray:
264
+ """
265
+ Estimate the surface density mass generation rate for asteroids of diameters d at the distance r of the central star
266
+ and of diameter d in a belt of mass Mbelt during the time times_loc
267
+
268
+ Parameters
269
+ ----------
270
+ Mbelt : np.float64
271
+ The total belt's mass
272
+ r : np.ndarray
273
+ The distance to the central star
274
+ p : np.ndarray
275
+ The asteroids/KBO diameters
276
+ tps : np.ndarray
277
+ The time
278
+ a0 : np.float64
279
+ The belt's semi-major axis
280
+ delta_a : np.float64
281
+ The (half) size pf the belt
282
+ tinit : np.float64
283
+ The initial time
284
+ tmax : np.float64
285
+ The maximum time
286
+ f_ice : np.float64
287
+ The initial ice to refractory mass ratio
288
+ rho_refr : np.float64
289
+ The asteroids/KBO density
290
+ rho_ice : np.float64
291
+ The asteroids/KBO ice density
292
+ Abelt : np.float64
293
+ The asteroids/KBO albedo
294
+ K : np.float64
295
+ The thermal diffusion coefficient
296
+ Its : np.float64
297
+ The number of spatial steps
298
+ Phi : np.float64
299
+ The porosity (dimensionless number)
300
+ dmin : np.float64
301
+ The minimum diameters
302
+ dmax : np.float64
303
+ The maximum diameters
304
+ Its_p : np.float64
305
+ The number of diameters steps
306
+ rp : np.float64
307
+ The pore's radius
308
+ Itt : np.float64
309
+ The number of time steps
310
+ kwargsN : dict
311
+ The directory with all informations to build the belt's size number distribution
312
+ kwargs : dict
313
+
314
+ Returns
315
+ -------
316
+ np.ndarray
317
+
318
+ """
319
+ if r is None:
320
+ r = np.linspace(a0 - delta_a, a0 + delta_a, Its)
321
+
322
+ pmin: np.float64 = max(dmin / 2., np.sqrt(K * 0.1 * Myr))
323
+ # pmin: np.float64 = max(dmin / 2., np.sqrt(K * 0.1 * yr))
324
+ p = np.geomspace(pmin / 2., dmax / 2., Its_p)
325
+ print("pmin= ", pmin / C_L, "m")
326
+
327
+ m_init: np.float64 = minit(r=r, pinit=pmin, rho_ice=rho_ice,
328
+ a0=a0, delta_a=delta_a, Mbelt=Mbelt, kwargsN=kwargsN)
329
+
330
+ if tps is None:
331
+ tps = np.geomspace(max(tinit, yr), tmax, Itt)
332
+ else:
333
+ tinit = tps.min()
334
+ tmax = tps.max()
335
+
336
+ consts: dict = {"C_L": C_L, "C_M": C_M, "C_t": C_t,
337
+ "Ms": Ms, "Abelt": Abelt, "K": K,
338
+ "a0": a0, "delta_a": delta_a, "t0": t0,
339
+ "rho_refr": rho_refr,
340
+ "Its": Its, "Its_p": Its_p,
341
+ "f_ice": f_ice,
342
+ "Phi": Phi, "rp": rp}
343
+
344
+ # Estimation of the gas sublimation rate realised for every distance of the central star r and depth p
345
+ # The factor tau_diff(p, r, t, consts) model the diffusion of the gas through the pore of refractory materials
346
+ # The thermal diffusion is modeled in sigmap_rho
347
+
348
+ rr: np.ndarray = r[::5, np.newaxis] + np.zeros_like(p)
349
+ pp: np.ndarray = (p[:, np.newaxis] + np.zeros_like(r[::5])).T
350
+ comp: Compteur = Compteur()
351
+
352
+ if kwargsN is None: # Default size distribution
353
+ kwargsN = dict(amax=np.double(1.e6) * C_L,
354
+ abig=np.double(120e3) * C_L,
355
+ amed=np.double(20.e3) * C_L, amin=C_L,
356
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
357
+ rho=rho_refr)
358
+
359
+ def integrate(ti: np.float64, tf: np.float64, yinit: np.ndarray, dtinit: np.float64 = None,
360
+ kwargsN=kwargsN) -> tuple[np.ndarray]:
361
+ if dtinit is None:
362
+ # dtinit = np.double(1e-10) * (tf - ti)
363
+ dtinit = np.double(1e-12) * (tf - ti)
364
+
365
+ def system(t, y):
366
+ yp = sigmap_rho(t=t - tau_diff(pp=pp, rr=rr, t=t, consts=consts) - pp * pp / consts["K"],
367
+ rr=rr, pp=pp, consts=consts, comp=comp).flatten()
368
+ # yp = sigmap_rho(t=t, rr=rr, pp=pp, consts=consts, comp=comp).flatten()
369
+ # Taking into acompte the limited amounth of ice
370
+ yp[y > rho_ice] *= zero
371
+ # yp[y > 0.9 * rho_ice] *= (y[y > 0.9 * rho_ice] / Myr) * np.exp(- np.maximum(200, rho_ice / np.maximum(abs(y[y > 0.9 * rho_ice] - rho_ice), 1e-200)))
372
+ # yp[pp.flatten() < pmin] *= zero
373
+ return yp
374
+
375
+ #
376
+ sol_ivp = solve_ivp(system, t_span=[ti, tf],
377
+ y0=yinit,
378
+ max_step=(tmax - ti) / Itt, method="RK45", args=[],
379
+ rtol=np.double(1e-4), atol=atol, first_step=dtinit)
380
+ print(sol_ivp.message)
381
+
382
+ sol_ivp.y = np.minimum(sol_ivp.y, rho_ice)
383
+ res_final = sol_ivp.y[:, -1]
384
+ times_loc = sol_ivp.t
385
+
386
+ rhodot = np.gradient(sol_ivp.y.T.reshape((len(sol_ivp.t), pp.shape[0], pp.shape[1])), sol_ivp.t, axis=0)
387
+
388
+ yplot = sol_ivp.y.T[-1].reshape(pp.shape).T
389
+ yplot[np.isnan(yplot)] = zero
390
+ yplot[abs(yplot) == np.inf] = zero
391
+ if np.any(yplot > 0):
392
+ yplot = np.maximum(yplot, 0.1 * min(yplot[yplot > 0]))
393
+ else:
394
+ yplot = np.maximum(yplot, rho_ice * 1e-50)
395
+ if yplot.min() < yplot.max():
396
+ print("test max : ", yplot.max() / rho_ice)
397
+ gtest = g.image(yplot / rho_ice, r[::5] / C_L, p, colorscale="log", show=False, cmap="inferno")
398
+ gtest.config_ax(yscale="log")
399
+ gtest.title = r"$\rho_{lost}$"
400
+ gtest.show()
401
+ else:
402
+ print(yplot.max() / rho_ice)
403
+
404
+ rhodot_d: np.ndarray = 4. * Pi * cumulative_trapezoid(p * p * rhodot, p, axis=2, initial=zero)
405
+
406
+ rho_d: np.ndarray = 4. * Pi * cumulative_trapezoid(
407
+ p * p * (sol_ivp.y.T.reshape((len(sol_ivp.t), pp.shape[0], pp.shape[1]))),
408
+ p, axis=2, initial=zero)
409
+ if kwargsN is None: # Default size distribution
410
+ kwargsN = dict(amax=np.double(1.e6) * C_L,
411
+ abig=np.double(120e3) * C_L,
412
+ amed=np.double(20.e3) * C_L, amin=C_L,
413
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
414
+ rho=rho_refr)
415
+
416
+ ## Calcul of number of asteroids for all diameters and distance to central star
417
+ sigma0: np.ndarray = (Mbelt * ((r[::5] / a0) ** (-3. / 2.))
418
+ / (4. * Pi * a0 * a0 * (np.sqrt(1. + delta_a / a0)
419
+ - np.sqrt(1. - delta_a / a0)))) # initial surface density
420
+ ni: np.ndarray = ((np.array([number_density(2. * d_2, Mtot=np.double(1.), **kwargsN)
421
+ for d_2 in p])[:, np.newaxis] + np.zeros(len(r[::5]))).T
422
+ * (sigma0[:, np.newaxis] + np.zeros(len(p)))) #number density
423
+
424
+ # Multiplying the gas production rate for one asteroid by the number of asteroids and integrated along d axis
425
+ res: np.ndarray = np.zeros((len(times_loc), len(r[::5])), dtype="double")
426
+ sig_lost: np.ndarray = np.zeros((len(times_loc), len(r[::5])), dtype="double")
427
+ for i in range(1, len(times_loc)):
428
+ res[i] = trapezoid(ni * rhodot_d[i], 2. * p)
429
+ sig_lost[i] = trapezoid(ni * rho_d[i], 2 * p)
430
+
431
+ mlost = trapezoid(2. * Pi * r[::5] * sig_lost, r[::5])
432
+ print("tf = ", times_loc[-1] / Myr, " Myr. Total mass lost : ", mlost[-1] / Mearth,
433
+ "Fraction of total mass loss ", 100 * mlost[-1] / (Mbelt * f_ice), "%")
434
+ # g.loglog(times_loc, [mlost, minit])
435
+ # return res, times_loc, res_final
436
+ return np.gradient(sig_lost, times_loc, axis=0), times_loc, res_final
437
+
438
+ times_intermediates = np.linspace(tinit, tmax, 100)[1:]
439
+ # times_intermediates = np.linspace(tinit, tmax, 4)[1:]
440
+ res_final = np.zeros(len(r[::5]) * len(p))
441
+ res = []
442
+ times = []
443
+ ti: np.float64 = tinit
444
+ dtinit = np.double(1e-10) * (times_intermediates[1] - ti)
445
+ for t in times_intermediates:
446
+ res_loc, times_loc, res_final = integrate(ti=ti, tf=t, yinit=res_final, dtinit=dtinit)
447
+ dtinit = times_loc[-1] - times_loc[-2]
448
+ print(res_loc.shape, times_loc.shape, len(r))
449
+ times.extend(list(times_loc))
450
+ res.extend(list(res_loc))
451
+ print(len(res), res[-1].shape)
452
+ ti = times_loc[-1]
453
+ print("Initial gas mass : ", m_init)
454
+ return np.array([linear_interp(r, r[::5], sig) for sig in res]), np.array(times), m_init, p
455
+
456
+
457
+ def mbelt_coll(t: np.float64, smin: np.float64 = np.double(1.e-8) * C_L,
458
+ smax: np.float64 = np.double(4.e6) * C_L,
459
+ sb: np.float64 = np.double(316) * C_L, qp: np.float64 = np.double(3),
460
+ qs: np.float64 = np.double(11 / 6), qg: np.float64 = np.double(1.67), e: np.float64 = np.double(0.075),
461
+ i: np.float64 = None, r: np.float64 = None, dr: np.float64 = np.double(4. * au),
462
+ As: np.float64 = np.double(5.) * C_E / C_M, bs: np.float64 = np.double(-0.1),
463
+ bg: np.float64 = np.double(0.5), M0: np.float64 = KB_Mbelt, Ms: np.float64 = Ms,
464
+ rho_ast: np.float64 = KB_rho_refr, method_root: str = "lm",
465
+ st_est: np.float64 | np.ndarray = None) -> np.float64 | np.ndarray:
466
+ """
467
+ Estimate the masse of a collisional belt as function of time (formula 38 of Löhne et al. 2008)
468
+
469
+ Parameters
470
+ ----------
471
+
472
+ t : np.float64
473
+ The time
474
+ smin : np.float64
475
+ Minimum size of bodys
476
+
477
+ smax : np.float64
478
+ Maximum size of bodys
479
+
480
+ sb : np.float64
481
+ Medium size
482
+
483
+ qp : np.float64
484
+ Primordial slope as defined in Löhne et al. 2008
485
+
486
+ qs : np.float64
487
+ Intermediate slope
488
+
489
+ qg : np.float64
490
+ Slope for bodys in collisional regim
491
+
492
+ e : np.float64
493
+ Eccentricity
494
+
495
+ i : np.float64
496
+ Inclination
497
+
498
+ r : np.float64
499
+ Distance of the center of the belt to the central star
500
+
501
+ dr : np.float64
502
+ Radial extension of the belt
503
+
504
+ As : np.float64
505
+ Minimal disruption energy (see formula 1 of Löhne et al. 2008)
506
+
507
+ bs : np.float64
508
+ Slope of the massique disruption energy (see formula 1 of Löhne et al 2008)
509
+
510
+ bg : np.float64
511
+ Slope of the massique disruption energy (see formula 1 of Löhne et al. 2008)
512
+
513
+ M0 : np.float64
514
+ Initial belt's mass
515
+
516
+ Ms : np.float64
517
+ Central star's mass
518
+
519
+ rho_ast : np.float64
520
+ Density of asteroids/KBO
521
+
522
+ method_root : str, optional, default="lm"
523
+ The method for the root-finding routine used to find st the size such as tau(st)=t
524
+
525
+ st_est : np.float64
526
+ Estimation of st
527
+
528
+ Returns
529
+ -------
530
+ np.float64 | np.ndarray
531
+ mbelt
532
+
533
+ References
534
+ ----------
535
+ Löhne et al. 2008
536
+
537
+ """
538
+ # qp: np.float64 = np.double(1.9)
539
+ if r is None:
540
+ r: np.float64 = a0
541
+ if i is None:
542
+ i: np.float64 = e / np.double(2.)
543
+
544
+ # dr: np.float64 = r / np.double(4.)
545
+ # As: np.float64 = np.double(5e2) * C_E / C_M
546
+ QDb: np.float64 = As * ((sb / C_L) ** (3. * bs) + (sb / (1e3 * C_L)) ** (3. * bg))
547
+ f_ei: np.float64 = np.sqrt((5 / 4) * e * e + i * i)
548
+ # Qd_smax: np.float64 = QDb * ((smax / sb) ** (3. * bg))
549
+ Qd_smax: np.float64 = As * ((smax / C_L) ** (3. * bs) + (smax / (1e3 * C_L)) ** (3. * bg))
550
+ XC_smax: np.float64 = (2. * Qd_smax * r / (f_ei * f_ei * G * Ms)) ** (1 / 3.)
551
+ G_smax: np.float64 = ((XC_smax ** (5. - 3. * qg) - 1.)
552
+ + (2. * (qg - 5. / 3.) / (qg - 4 / 3))
553
+ * (XC_smax ** (4. - 3. * qg) - 1.)
554
+ + ((qg - 5. / 3.) / (qg - 1.))
555
+ * (XC_smax ** (3. - 3. * qg) - 1.))
556
+
557
+ def tau(s: np.float64 | np.ndarray):
558
+ """
559
+ The collisional timescale for bodys of size s
560
+
561
+ Parameters
562
+ ----------
563
+ s: np.float64 | np.ndarray
564
+ The size
565
+ Returns
566
+ -------
567
+ np.float64 | np.ndarray
568
+
569
+ """
570
+ Qd_s: np.float64 = As * ((s / C_L) ** (3. * bs) + (s / (1e3 * C_L)) ** (3. * bg))
571
+ XC_s: np.float64 = (2. * Qd_s * r / (f_ei * f_ei * G * Ms)) ** (1 / 3.)
572
+ G_qp_s: np.float64 = ((XC_s ** (5. - 3. * qp) - (smax / s) ** (5. - 3. * qp))
573
+ + (2. * (qp - 5. / 3.) / (qp - 4 / 3))
574
+ * (XC_s ** (4. - 3. * qp) - (smax / s) ** (4. - 3. * qp))
575
+ + ((qp - 5. / 3.) / (qp - 1.))
576
+ * (XC_s ** (3. - 3. * qp) - (smax / s) ** (3. - 3. * qp)))
577
+ return ((16 * Pi * rho_ast / (3. * M0))
578
+ * ((s / smax) ** (3. * qp - 5))
579
+ * (smax * (r ** (5. / 2.)) * dr / np.sqrt(G * Ms))
580
+ * ((qp - 5. / 3.) / (2 - qp))
581
+ * (1. - ((smin / smax) ** (6. - 3. * qp)))
582
+ * i / (f_ei * G_qp_s))
583
+
584
+ tau_max: np.float64 = ((16. * Pi * rho_ast / (3. * M0)) * smax
585
+ * ((r ** (5 / 2)) * dr / np.sqrt(G * Ms))
586
+ * (((qg - 5. / 3.) / (2. - qp)) / (1. - ((smin / smax) ** (6. - 3. * qp))))
587
+ * i / (f_ei * G_smax)) # Maximum collisional timescale
588
+
589
+ # print("tau max=", tau_max, tau(smax))
590
+
591
+ def rech_st(s: np.float64 | np.ndarray) -> np.float64 | np.ndarray:
592
+ """
593
+ The function is used to find the scale for which tau(s)=t
594
+
595
+ Parameters
596
+ ----------
597
+ s: np.float64 | np.ndarray
598
+ The size to test
599
+
600
+ Returns
601
+ -------
602
+ np.float64 | np.ndarray
603
+ A parameter null where tau(s)=t
604
+ """
605
+ return (t - tau(s)) / t
606
+
607
+ tau_b: np.float64 = tau(sb)
608
+ if st_est is None: # Estimation of st such as tau(s_t)=t
609
+ st_est: np.float64 | np.ndarray = sb * ((t / tau_b) ** (1 / (3. * qp - 5. + 3. * (qp - 1) * bg)))
610
+ st: np.float64 | np.ndarray = root(rech_st, st_est, method=method_root, tol=1e-12).x
611
+ st = st[0]
612
+
613
+ if isinstance(t, float | np.float64) and t > tau_max:
614
+ return zero
615
+ if t < tau_b:
616
+ sb: np.float64 = st
617
+
618
+ C1: np.float64 = M0 / (1. - (smin / smax) ** (6. - 3. * qp))
619
+ C2: np.float64 = (((sb / smax) ** (6. - 3. * qp))
620
+ * (1. - (2. - qp) / (2. - qg)))
621
+ C3: np.float64 = (((sb / smax) ** (6. - 3. * qp))
622
+ * ((2. - qp) / (2. - qs) - (2. - qp) / (2. - qg)))
623
+ C4: np.float64 = (((sb / smax) ** (3. * qs - 3. * qp))
624
+ * ((smin / smax) ** (6. - 3. * qs))
625
+ * (2. - qp) / (2. - qs))
626
+ alpha_st: np.float64 = np.double(1. / (3. * qp - 5 + 3. * (qp - 1.) * bg))
627
+
628
+ res = C1 * (1 - C2 * ((st / sb) ** (6. - 3. * qp)) + ((st / sb) ** (3. * qg - 3. * qp)) * (C3 - C4)) / (
629
+ 1 + t / tau_max)
630
+
631
+ if isinstance(t, np.ndarray):
632
+ res[t > tau_max] *= zero
633
+
634
+ return res
635
+
636
+
637
+ class SolrhoBelt:
638
+ def __init__(self,
639
+ sublimation_model: str,
640
+ Mbelt: np.float64,
641
+ Ms: np.float64,
642
+ a0: np.float64, delta_a: np.float64,
643
+ tinit: np.float64, tmax: np.float64,
644
+ rho_refr: np.float64, rho_ice: np.float64, f_ice: np.float64,
645
+ A_ast: np.float64, K: np.float64,
646
+ Its: int, Phi: np.float64,
647
+ dmin: np.float64, sb: np.float64, dmax: np.float64,
648
+ e: np.float64, i: np.float64, As: np.float64, bs: np.float64, bg: np.float64,
649
+ Its_p: int, rp: np.float64, Itt: int,
650
+ t0_diss: np.float64, t1_diss: np.float64,
651
+ radius: np.ndarray[np.float64] | None = None,
652
+ kwargs_N: dict = None, sig_dots: np.ndarray = None, tps: np.ndarray = None,
653
+ m_init: np.float64 = zero,
654
+ const_mdot: np.float64 = zero, **kwargs
655
+ ):
656
+ """
657
+ Initialise a SolrhoBelt object
658
+ This object is used to get the gas surface density generation rate thru its method_root
659
+ interp_sig_dot(t)
660
+
661
+ Parameters
662
+ ----------
663
+ sublimation_model : str, {"none", "thermal_full", "constant_rate"}
664
+ The model use to estimate the gas generation rate :
665
+ - none : No gas is produced
666
+ - thermal_full : the gas is produced by sublimation : all sublimation, thermal diffusion and molecular
667
+ diffusion are take into acompte
668
+ - constant_rate : The gas is produced at a constant rate of const_mdot
669
+ Mbelt : np.float64
670
+ The total belt's mass
671
+ Ms : np.float64
672
+ The central star's mass
673
+ a0 : np.float64
674
+ The belt's semi major axis (located at the middle of the belt)
675
+ delta_a : np.float64
676
+ The belt's (half) size
677
+ tinit : np.float64
678
+ The initial time
679
+ tmax : np.float64
680
+ The final time
681
+ rho_refr : np.float64
682
+ The density of refractory materials
683
+ rho_ice : np.float64
684
+ The ice's density
685
+ f_ice : np.float64
686
+ The ice to refractory mass ratio
687
+ A_ast : np.float64
688
+ Asteroids/KBO albedo
689
+ K : np.float64
690
+ Thermal diffusion coefficient
691
+ Its : np.float64
692
+ Number of spatial iterations
693
+ Phi : np.float64
694
+ Porosity (dimensionless number between 0 to 1)
695
+ dmin : np.float64
696
+ The minimum radius of asteroids/KBO
697
+ sb : np.float64
698
+ The final transition size between collisional and primordial regim of asteroids/KBO
699
+ dmax : np.float64
700
+ The maximum radius of asteroids/KBO
701
+ e : np.float64
702
+ Eccentricity
703
+ i : np.float64
704
+ Inclinaison
705
+ As : np.float64
706
+ Minimal disruption energy (see formula 1 of Löhne et al 2008)
707
+ bs : np.float64
708
+ Slope of the massique disruption energy (see formula 1 of Löhne et al 2008)
709
+ bg : np.float64
710
+ Slope of the massique disruption energy (see formula 1 of Löhne et al 2008)
711
+ Its_p : np.float64
712
+ The number of steps in diameters space
713
+ rp : np.float64
714
+ The pore's radius to estimate the gas diffusion thru the solid
715
+ Itt : np.float64
716
+ Number of spatial iterations
717
+ t0_diss : np.float64
718
+ Belt's lifetime before dissipation
719
+ t1_diss : np.float64
720
+ Dissipation timescale
721
+ radius : np.ndarray
722
+ Table of radius
723
+ kwargs_N : dict
724
+ keywords arguments for the size distribution of asteroids/KBO
725
+ sig_dots : np.ndarray
726
+ table of sigma dot as function of time and radius (shape=(Ttt, Its)) if it has already been calculated
727
+ This table is interpolated with the CubicSpline function to get interp_sig_dot(t)
728
+ const_mdot : np.float64, optional, default=0.
729
+ The mass generation rate for the `constant_rate` sublimation model
730
+
731
+ """
732
+ self.sublimation_model: str = sublimation_model
733
+ self.const_mdot: np.float64 = const_mdot
734
+ self.Mbelt: np.float64 = Mbelt
735
+ self.Ms: np.float64 = Ms
736
+ self.Abelt: np.float64 = A_ast
737
+ self.K: np.float64 = K
738
+ self.a0: np.float64 = a0
739
+ self.delta_a: np.float64 = delta_a
740
+ self.t0: np.float64 = tinit
741
+ self.tmax: np.float64 = tmax
742
+ self.rho_refr: np.float64 = rho_refr
743
+ self.rho_ice: np.float64 = rho_ice
744
+ self.Its: int = Its
745
+ self.Its_p: int = Its_p
746
+ self.Itt: int = Itt
747
+ self.f_ice: np.float64 = f_ice # ice to refractory mass ratios
748
+ self.Phi: np.float64 = Phi # Porosity
749
+ self.rp: np.float64 = rp # Pore's radius
750
+ self.dmin: np.float64 = dmin
751
+ self.sb: np.float64 = sb
752
+ self.dmax: np.float64 = dmax
753
+ self.e: np.float64 = e
754
+ self.i: np.float64 = i
755
+ self.As: np.float64 = As
756
+ self.bs: np.float64 = bs
757
+ self.bg: np.float64 = bg
758
+ self.m_init: np.float64 = m_init
759
+ if radius is None:
760
+ self.r = np.linspace(a0 - delta_a / 2., a0 + delta_a /2., Its)
761
+ else:
762
+ self.r = np.copy(radius)
763
+ self.Its = len(self.r)
764
+ # self.p = np.linspace(dmin / 2., dmax / 2., Its_p)
765
+ self.p = np.geomspace(dmin / 2., dmax / 2., Its_p)
766
+ if tps is None:
767
+ self.tps: np.ndarray = tinit + np.geomspace(yr, tmax - tinit, Itt)
768
+ else:
769
+ self.tps: np.ndarray = tps
770
+
771
+ if kwargs_N is None:
772
+ self.kwargs_N = dict(amax=np.double(1.e6) * C_L,
773
+ abig=np.double(120e3) * C_L,
774
+ amed=np.double(20.e3) * C_L, amin=C_L,
775
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
776
+ rho=self.rho_refr)
777
+ else:
778
+ self.kwargs_N = kwargs_N.copy()
779
+ self.sig_dots = None
780
+
781
+ if sig_dots is not None:
782
+ self.sig_dots = sig_dots
783
+ # print("test :", len(self.tps), sig_dots.shape)
784
+ tps, idx = np.unique(self.tps, return_index=True)
785
+ self.tps = np.copy(tps)
786
+ if self.sig_dots.shape[0] == len(self.tps):
787
+ self.sig_dots = (self.sig_dots[idx]).copy()
788
+ else:
789
+ self.sig_dots = (self.sig_dots.T[idx]).copy()
790
+
791
+ def linear_interp(t_new):
792
+ if isinstance(t_new, list | np.ndarray):
793
+ return np.array([linear_interp(t) for t in t_new])
794
+
795
+ i = np.argmin(np.abs(t_new - np.array(self.tps)))
796
+ if i == 0 or (i < len(self.tps) - 1 and t_new > self.tps[i]):
797
+ t1 = self.tps[i]
798
+ t2 = self.tps[i + 1]
799
+ alpha = (t_new - t1) / (t2 - t1)
800
+ return (1 - alpha) * self.sig_dots[i] + alpha * self.sig_dots[i + 1]
801
+ else:
802
+ t1 = self.tps[i - 1]
803
+ t2 = self.tps[i]
804
+ alpha = (t_new - t1) / (t2 - t1)
805
+ return (1 - alpha) * self.sig_dots[i - 1] + alpha * self.sig_dots[i]
806
+
807
+ self.interp_sig_dot = linear_interp
808
+ # self.interp_sig_dot = linear_interpolation(self.tps, self.sig_dots)
809
+
810
+ # def interp_sig_dot(t: np.float64 | np.ndarray) -> np.ndarray[np.float64]:
811
+ # return np.interp(t, self.tps, self.sig_dots)
812
+ # self.interp_sig_dot = interp_sig_dot
813
+ # self.interp_sig_dot = linear_interpolation(self.tps, sig_dots)
814
+ # self.interp_sig_dot = PchipInterpolator(self.tps, sig_dots)
815
+ # self.interp_sig_dot = CubicSpline(self.tps - 0.1 * Myr, sig_dots)
816
+ # self.interp_sig_dot = FloaterHormannInterpolator(self.tps, sig_dots)
817
+ # self.interp_log_sig_dot = CubicSpline(self.tps, np.maximum(np.log10(self.sig_dots), 1e-200))
818
+ #
819
+ # def interp_sig_dot(t: np.float64 | np.ndarray) -> np.ndarray[np.float64]:
820
+ # return 10 ** self.interp_log_sig_dot(t)
821
+ #
822
+ # self.interp_sig_dot = interp_sig_dot
823
+ elif self.sublimation_model == "none":
824
+ def null(t: np.float64 | np.ndarray) -> np.float64:
825
+ return zero
826
+
827
+ self.interp_sig_dot = null
828
+
829
+ elif self.sublimation_model == "thermal_full":
830
+ # self.sig_dots, self.tps, self.m_init, self.p = sigma_dot(r=self.r, tps=self.tps, kwargsN=self.kwargs_N, **self.const())
831
+ # print("test shape", self.sig_dots.shape, self.r.shape, self.tps.shape)
832
+ # M = trapezoid(trapezoid(2. * np.pi * self.r * self.sig_dots, self.r, axis=1), self.tps)
833
+ # print("Test mass", M / Mearth)
834
+ # # while M > 1.1 * self.Mbelt * self.f_ice:
835
+ # # self.Itt = int(self.Itt * 1.2)
836
+ # # print("Integration failure, restarting", self.Itt)
837
+ # # self.sig_dots, self.tps = sigma_dot(r=self.r, p=self.p, tps=self.tps, kwargsN=self.kwargs_N,
838
+ # # **self.const())
839
+ # # M = trapezoid(trapezoid(2. * np.pi * self.r * self.sig_dots, self.r), self.tps)
840
+ # # self.sig_dots = np.maximum(savgol_filter(self.sig_dots, 7, 3, axis=1), zero)
841
+ # # dissipation of the belt
842
+ # # if np.any(self.tps - 0.1 * Myr > t0_diss):
843
+ # # self.sig_dots[self.tps - 0.1 * Myr > t0_diss] *= np.exp(
844
+ # # -(self.tps[self.tps - 0.1 * Myr > t0_diss] - t0_diss) / t1_diss)
845
+ # tps_u, idx = np.unique(self.tps, return_index=True)
846
+ # self.tps = tps_u.copy()
847
+ # self.Itt = len(self.tps)
848
+ # self.sig_dots = (self.sig_dots[idx]).copy()
849
+
850
+ self.sig_dots = sig_dot_full_diff(tps=self.tps, r=self.r, const=self.const(),
851
+ kwargsN=self.kwargs_N, It_d=200)
852
+
853
+ minit = 1e-1 * self.Mbelt * self.f_ice
854
+ if np.any(cumulative_trapezoid(trapezoid(2. * Pi * self.r * self.sig_dots, self.r),
855
+ self.tps, initial=0.) >= minit):
856
+ iinit = np.argwhere(cumulative_trapezoid(trapezoid(2. * Pi * self.r * self.sig_dots, self.r),
857
+ self.tps, initial=0.) >= minit)[0, 0]
858
+ if self.tps[iinit] - self.t0 < 0.1 * Myr:
859
+ self.m_init = trapezoid(trapezoid(2. * Pi * self.r * self.sig_dots[:iinit], self.r),
860
+ self.tps[:iinit])
861
+ self.tps -= self.tps[iinit] - tinit
862
+ else:
863
+ self.m_init = 0.
864
+ else:
865
+ iinit= 0
866
+ minit = zero
867
+
868
+
869
+
870
+ def linear_interp(t_new):
871
+ if isinstance(t_new, list | np.ndarray):
872
+ return np.array([linear_interp(t) for t in t_new])
873
+
874
+ i = np.argmin(np.abs(t_new - np.array(self.tps)))
875
+ if i == 0 or (i < len(self.tps) - 1 and t_new > self.tps[i]):
876
+ t1 = self.tps[i]
877
+ t2 = self.tps[i + 1]
878
+ alpha = (t_new - t1) / (t2 - t1)
879
+ return (1 - alpha) * self.sig_dots[i] + alpha * self.sig_dots[i + 1]
880
+ else:
881
+ t1 = self.tps[i - 1]
882
+ t2 = self.tps[i]
883
+ alpha = (t_new - t1) / (t2 - t1)
884
+ return (1 - alpha) * self.sig_dots[i - 1] + alpha * self.sig_dots[i]
885
+
886
+ self.interp_sig_dot = linear_interp
887
+
888
+ # self.interp_sig_dot = linear_interpolation(self.tps, self.sig_dots)
889
+ # self.interp_sig_dot = PchipInterpolator(self.tps, self.sig_dots)
890
+ # self.interp_sig_dot = CubicSpline(self.tps - 0.1 * Myr, self.sig_dots)
891
+ # self.interp_log_sig_dot = CubicSpline(self.tps, np.maximum(np.log10(self.sig_dots), 1e-200))
892
+ #
893
+ # def interp_sig_dot(t: np.float64 | np.ndarray) -> np.ndarray[np.float64]:
894
+ # return 10 ** self.interp_log_sig_dot(t)
895
+ # self.interp_sig_dot = interp_sig_dot
896
+ # self.interp_sig_dot = FloaterHormannInterpolator(self.tps, self.sig_dots)
897
+ # selot = CubicSpline(self.tps, self.sig_dots)
898
+ elif self.sublimation_model == "collisions":
899
+
900
+ rho = rho_refr
901
+ # with f_ice = rho_ice / rho
902
+
903
+ mbelt_: np.ndarray = np.array([(mbelt_coll(t, smax=self.dmax, smin=self.dmin,
904
+ sb=self.sb, qp=np.double((-self.kwargs_N["qh"] + 2.) / 3.),
905
+ qs=np.double((-self.kwargs_N["ql"] + 2.) / 3.),
906
+ qg=np.double((-self.kwargs_N["qm"] + 2.) / 3.), e=self.e,
907
+ i=self.i, r=self.a0, dr=self.delta_a, As=self.As,
908
+ M0=self.Mbelt, rho_ast=rho,
909
+ method_root="lm", bs=self.bs, bg=self.bg)) for t in self.tps])
910
+
911
+ mdot_: np.ndarray = - self.f_ice * np.gradient(mbelt_, self.tps - 0.1 * Myr)
912
+ # dissipation of the belt
913
+ if np.any(self.tps - 0.1 * Myr > t0_diss):
914
+ mdot_[self.tps - 0.1 * Myr > t0_diss] *= np.exp(
915
+ -(self.tps[self.tps - 0.1 * Myr > t0_diss] - t0_diss) / t1_diss)
916
+ sigmad0: np.float64 = mdot_ / (
917
+ 4. * Pi * a0 * a0 * (np.sqrt(1. + delta_a / a0) - np.sqrt(1. - delta_a / a0)))
918
+ self.sig_dots: np.ndarray = np.full((len(self.tps), len(self.r)), sigmad0)
919
+ self.sig_dots *= ((self.r / self.a0) ** (-3. / 2.))
920
+ self.interp_sig_dot = CubicSpline(self.tps - 0.5 * Myr, self.sig_dots)
921
+
922
+ elif self.sublimation_model == "thermal_diff":
923
+ # The sublimation timescale and molecular diffusion timescale are neglected
924
+ def n(diameter: np.float64):
925
+ return number_density(diameter, **self.kwargs_N)
926
+
927
+ rho = rho_refr
928
+
929
+ def sig_dot_therm(t: np.float64):
930
+ t0 = 1e3 * yr
931
+
932
+ mdot_t = (2. * Pi * rho * self.f_ice * (self.K ** (3 / 2))
933
+ * quad(n, max(self.kwargs_N["amin"], 2. * np.sqrt(self.K * (t + t0))),
934
+ self.kwargs_N["amax"])[0] * np.sqrt(t))
935
+ if np.isnan(mdot_t):
936
+ mdot_t = zero
937
+ elif mdot_t < 0: # the integration with quad has not worked
938
+ diams = np.geomspace(max(self.kwargs_N["amin"], 2. * np.sqrt(self.K * (t + t0))),
939
+ self.kwargs_N["amax"], 1000)
940
+ mdot_t = (2. * Pi * rho * f_ice * (self.K ** (3 / 2))
941
+ * trapezoid([n(d) for d in diams], diams) * np.sqrt(t))
942
+ if mdot_t < 0: # the integration with quad has not worked
943
+ diams = np.geomspace(max(self.kwargs_N["amin"], 2. * np.sqrt(self.K * (t + t0))),
944
+ self.kwargs_N["amax"], 10000)
945
+ mdot_t = (2. * Pi * rho * f_ice * (self.K ** (3 / 2))
946
+ * trapezoid([n(d) for d in diams], diams) * np.sqrt(t))
947
+
948
+ sigmad0: np.float64 = mdot_t / (
949
+ 4. * Pi * a0 * a0 * (np.sqrt(1. + delta_a / a0) - np.sqrt(1. - delta_a / a0)))
950
+ if t > t0_diss:
951
+ sigmad0 *= np.exp(-(t - t0_diss) / t1_diss)
952
+ return sigmad0 * ((self.r / self.a0) ** (-3. / 2.))
953
+
954
+ self.interp_sig_dot = sig_dot_therm
955
+
956
+ elif self.sublimation_model == "constant_rate":
957
+ # norm_gauss = quad(lambda x: np.exp(-x * x / 2.), 0, 4.)[0]
958
+ # sigmar: np.float64 = np.double(self.delta_a / (2. * np.sqrt(2. * np.log(2))))
959
+ # sigmap_0: np.float64 = m_dot / (4. * Pi * sigmar * self.a0 * norm_gauss)
960
+ # sigmap: np.ndarray = sigmap_0 * np.exp(- (self.r - self.a0) * (self.r - self.a0) / (
961
+ # 2. * sigmar * sigmar))
962
+ # sigmap_0: np.float64 = m_dot / (4. * Pi * self.a0 * self.a0 * (
963
+ # np.sqrt(1 + self.delta_a / (2 * self.a0)) - np.sqrt(1 - self.delta_a / (2 * self.a0))))
964
+ sigmap_0: np.float64 = const_mdot / (4. * Pi * self.a0 * self.a0 * (
965
+ np.sqrt(1 + self.delta_a / self.a0) - np.sqrt(1 - self.delta_a / self.a0)))
966
+ sigmap: np.ndarray = sigmap_0 * ((self.r / self.a0) ** (-3 / 2))
967
+
968
+ def cons_rate(t: np.float64 | np.ndarray) -> np.ndarray:
969
+ if t < t0_diss:
970
+ return sigmap
971
+ else:
972
+ return sigmap * np.exp(-max((t - t0_diss) / t1_diss, 150))
973
+
974
+ self.interp_sig_dot = cons_rate
975
+
976
+ else:
977
+ raise UserWarning("The sublimation model ", self.sublimation_model, " is not implemented (yet)")
978
+
979
+ def const(self, prefix: str = "") -> dict:
980
+ return {prefix + "sublimation_model": self.sublimation_model,
981
+ prefix + "tinit": self.t0, prefix + "tmax": self.tmax,
982
+ prefix + "Mbelt": self.Mbelt,
983
+ prefix + "Ms": self.Ms, prefix + "Abelt": self.Abelt, prefix + "K": self.K,
984
+ prefix + "a0": self.a0, prefix + "delta_a": self.delta_a, prefix + "t0": self.t0,
985
+ prefix + "rho_refr": self.rho_refr, prefix + "rho_ice": self.rho_ice,
986
+ prefix + "Its": self.Its, prefix + "Its_p": self.Its_p,
987
+ prefix + "f_ice": self.f_ice,
988
+ prefix + "Phi": self.Phi, prefix + "rp": self.rp,
989
+ prefix + "Itt": self.Itt,
990
+ prefix + "dmin": self.dmin,
991
+ prefix + "dmax": self.dmax,
992
+ }
993
+
994
+ def return_kwargsN(self, prefix: str = "") -> dict:
995
+ res = {}
996
+ for k in self.kwargs_N.keys():
997
+ res[prefix + k] = self.kwargs_N[k]
998
+ return res
999
+
1000
+ def graph_image_surface_density(self,
1001
+ vmin: np.float64 | str = 1e-10 * Mearth / (au * au * Myr),
1002
+ vmax: np.float64 | str = None,
1003
+ size_time: int = 500, size_radius: int = 1000,
1004
+ cmap: str = "inferno", nb_levels: int = 10,
1005
+ color_min: str | tuple = None, color_max: str | tuple = None,
1006
+ show: bool = True, save: bool = False, directory: str = None,
1007
+ plot_label_levels: bool = True,
1008
+ **kwargs_ax
1009
+ ) -> g.Graphique:
1010
+ """
1011
+ Build a Graphique that represant the evolution of surface density generation rate
1012
+ as function of time and radius
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ vmin : np.float64 | str , optional, {"auto", np.loat64}, default = 1e-10 * Mearth / Myr
1017
+ The minimum value of surface density to represant in the result (below it will be considered as saturated)
1018
+ If "auto", the minimal value will be the minimal value of the surface density represanted
1019
+ vmax : np.float64 | str, optional, {"auto", np.loat64},
1020
+ The maximum value of surface density to represant in the result (above it will be considered as saturated)
1021
+ Default, v_max is not considered like for "auto" where the maximal value will be the maximal value of the surface density represanted
1022
+
1023
+ size_time : int, optional, default=1000
1024
+ The image's size for the time coordinate (x-axis)
1025
+ size_radius : int, optional, default=1000
1026
+ The image's size for the radius coordinate (y-axis)
1027
+ cmap : str, optional, default = "inferno"
1028
+ The colormap used for the image
1029
+ nb_levels : int, optional, default=10
1030
+ The number of levels in the image
1031
+ The number of levels to plot in addition to the image
1032
+ color_min : str | tuple, optional
1033
+ The color associated with the minimum value for a `default` cmap
1034
+ color_max : str | tuple, optional
1035
+ The color associated with the maximum value for a `default` cmap
1036
+ plot_label_levels : bool, optional, default=False
1037
+ To plot (or not) labels for the contours (if True, labels are only plot every two levels)
1038
+ show : bool, optional, default = True
1039
+ Whether to show the image
1040
+ save : bool, optional, default = False
1041
+ Whether to save the image (if True, both the Graphique and the png image will be saved)
1042
+ directory : str, optional
1043
+ The directory to save the Graphique, default=self.directory
1044
+
1045
+ Returns
1046
+ -------
1047
+ g.Graphique
1048
+
1049
+ """
1050
+
1051
+ if isinstance(vmin, str) and vmin != "auto":
1052
+ raise UserWarning(f"The vmin={vmin} is not a valid value for this simulation")
1053
+ elif isinstance(vmin, str):
1054
+ vmin = -inf
1055
+
1056
+ if vmax is None:
1057
+ vmax = np.inf
1058
+ if isinstance(vmax, str) and vmax != "auto":
1059
+ raise UserWarning(f"The vmax={vmax} is not a valid value for this simulation")
1060
+ elif isinstance(vmax, str):
1061
+ vmax = -inf
1062
+
1063
+ gr: g.Graphique = g.image(self.sig_dots[::max(len(self.tps) // size_time, 1),
1064
+ ::max(len(self.r) // size_radius, 1)].T,
1065
+ np.array(self.tps)[::max(len(self.tps) // size_time, 1)] / Myr,
1066
+ np.array(self.r)[::max(len(self.r) // size_radius, 1)] / au,
1067
+ cmap=cmap, colorscale="log", show=False, vmin=vmin, vmax=vmax,
1068
+ color_min=color_min, color_max=color_max)
1069
+ if vmin == -inf:
1070
+ vmin = np.min(gr.array_image)
1071
+
1072
+ if vmax < inf:
1073
+ vmax_levels = vmax
1074
+ else:
1075
+ vmax_levels: np.float64 = np.max(gr.array_image)
1076
+
1077
+ if nb_levels > 0:
1078
+ levels = np.geomspace(vmin, vmax_levels, nb_levels)
1079
+ gr.contours(levels, color="k")
1080
+
1081
+ gr.config_colorbar(ticks=levels,
1082
+ ticks_labels=["{:.2e}".format(t) for t in
1083
+ levels],
1084
+ label="Surface density production rate"
1085
+ " [M$_\oplus$.au$^{-2}$.Myr$^{-1}$]")
1086
+ else:
1087
+ gr.config_colorbar(ticks=np.geomspace(vmin, vmax_levels, 10),
1088
+ ticks_labels=["{:.2e}".format(t) for t in
1089
+ np.geomspace(vmin, vmax_levels, 10)],
1090
+ label="Surface density production rate"
1091
+ " [M$_\oplus$.au$^{-2}$.Myr$^{-1}$]")
1092
+
1093
+ gr.config_ax(xlabel="Time [Myr]", ylabel="Distance to the central star [au] ",
1094
+ xscale="log", yscale="log", xlim=[self.tps[2], self.tps[-1]], ylim=[self.r[0], self.r[-1]])
1095
+ if vmax < np.inf and np.any(self.sig_dots > vmax):
1096
+ gr.config_colorbar(0, extend="max")
1097
+ if np.any(self.sig_dots < vmin):
1098
+ if 'extend' in gr.param_colorbar[0].keys():
1099
+ gr.config_colorbar(0, extend="both")
1100
+ else:
1101
+ gr.config_colorbar(0, extend="min")
1102
+ gr.filename = "Image_total_surface_density_production"
1103
+
1104
+ gr.config_ax(**kwargs_ax)
1105
+ if plot_label_levels:
1106
+ gr.config_labels_contours(levels=gr.levels[::2], fmt="%.0e")
1107
+ else:
1108
+ gr.config_labels_contours(levels=[], fmt="%.0e")
1109
+
1110
+ if directory is not None:
1111
+ gr.directory = directory
1112
+
1113
+ if show:
1114
+ gr.show()
1115
+ if save:
1116
+ gr.ext = ".png"
1117
+ gr.save_figure(dpi=400)
1118
+ gr.save()
1119
+ return gr
1120
+
1121
+ def graph_mdot(self, size_time: int = 500,
1122
+ show: bool = True, save: bool = False, directory: str = None,
1123
+ **kwargs_ax) -> g.Graphique:
1124
+ """
1125
+ Build a Graphique that represant the evolution of total gas mass generation rate
1126
+ as function of time
1127
+
1128
+ Parameters
1129
+ ----------
1130
+ size_time : int, optional, default=1000
1131
+ The size for the time coordinate (x-axis)
1132
+ show : bool, optional, default = True
1133
+ Whether to show the image
1134
+ save : bool, optional, default = False
1135
+ Whether to save the image (if True, both the Graphique and the png image will be saved)
1136
+ directory : str, optional
1137
+ The directory to save the Graphique, default=self.directory
1138
+
1139
+ Returns
1140
+ -------
1141
+ g.Graphique
1142
+
1143
+ """
1144
+ gr: g.Graphique = g.loglog(np.array(self.tps)[::max(len(self.tps) // size_time, 1)] / Myr,
1145
+ trapezoid(2. * Pi * self.r * self.sig_dots[::max(len(self.tps) // size_time, 1)],
1146
+ self.r, axis=1)
1147
+ * Myr / Mearth, show=False)
1148
+ gr.config_ax(xlabel="Time [Myr]", ylabel="$\dot{M}$ [M$_\oplus$.Myr$^{-1}$] ",
1149
+ xscale="log", yscale="log", xlim=[self.tps[2], self.tps[-1]])
1150
+ gr.filename = "Image_total_mass_production_rate"
1151
+
1152
+ gr.config_ax(**kwargs_ax)
1153
+
1154
+ if directory is not None:
1155
+ gr.directory = directory
1156
+ if show:
1157
+ gr.show()
1158
+ if save:
1159
+ gr.ext = ".png"
1160
+ gr.save_figure(dpi=400)
1161
+ gr.save()
1162
+ return gr
1163
+
1164
+
1165
+ def thermal_diff(d: np.float64, consts: dict, It_d: int = 300, t: np.ndarray = None
1166
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
1167
+ """
1168
+ Compute the thermal diffusion equation into a bodi of radius d as a function of time assuming the bodi to be
1169
+ at the distance a_0 from the central star. To get the temperatures at others location r, one must multiplie the
1170
+ result by (r / a0) ** (-1/2)
1171
+
1172
+ Parameters
1173
+ ----------
1174
+ d : np.float64
1175
+ The size of the bodi
1176
+ consts : dict
1177
+ The dictionary of constant parameters
1178
+ It_d : int
1179
+ The number of spatials steps
1180
+ t : np.ndarray
1181
+ the times at which evaluate the solution
1182
+
1183
+ Returns
1184
+ -------
1185
+ np.ndarray
1186
+
1187
+ """
1188
+ r = np.linspace(d / (100 * It_d), d, It_d)
1189
+ dr = r[1] - r[0]
1190
+
1191
+ def system(t: np.float64, T: np.ndarray, compteur: Compteur = Compteur()):
1192
+ T[-1] = (278 * (((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4))
1193
+ * ((consts["a0"] / au) ** (-1 / 2)))
1194
+ # T[0] = 1
1195
+ T[0] = T[1]
1196
+ # dT = consts["K"] * (gradient2(T, dr) + np.gradient(T, r) / r) - T * interp_L(t, 1) / (4 * interp_L(t))
1197
+ dT = consts["K"] * (gradient2(T, dr) + np.gradient(T, r) / r)
1198
+ dT[0] = 0
1199
+ dT[-1] = 0
1200
+ if compteur.value > 0 and compteur.value % 10000 == 0:
1201
+ print("Iteration ", compteur.value, "t={:.4f} Myr".format(t / Myr))
1202
+ compteur.value += 1
1203
+ return dT
1204
+
1205
+ Tinit = np.zeros(It_d) + 0.1
1206
+ # Tinit[0] = 1
1207
+ compt: Compteur = Compteur()
1208
+ sol_ivp = solve_ivp(system, t_span=[consts["tinit"], consts["tmax"]],
1209
+ y0=Tinit, t_eval=t,
1210
+ max_step=Myr, method="LSODA", args=[compt],
1211
+ rtol=np.double(1e-6), atol=atol)
1212
+ if sol_ivp.status == -1:
1213
+ print(sol_ivp.message)
1214
+ if t is None:
1215
+ t = np.geomspace(consts["tinit"], consts["tmax"], 1000)
1216
+ temp_lim = (278 * (((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4)) * (
1217
+ (a0 / au) ** (-1 / 2)))
1218
+ return r, t, (temp_lim[:, np.newaxis] + np.zeros_like(r)).T
1219
+ sol_ivp.y[-1] = (278 * (((1. - consts["Abelt"]) * interpLbol(sol_ivp.t, consts["Ms"]) / Lsun) ** (1 / 4)) * (
1220
+ (a0 / au) ** (-1 / 2)))
1221
+ sol_ivp.y[0] = sol_ivp.y[1]
1222
+ return r, sol_ivp.t, sol_ivp.y
1223
+
1224
+
1225
+ def mdot_s_l(rd: np.ndarray, t: np.ndarray, temp: np.ndarray, consts: dict) -> np.ndarray:
1226
+ """
1227
+ Compute the gas mass generation for a given bodie
1228
+
1229
+ Parameters
1230
+ ----------
1231
+ rd : np.ndarray
1232
+ The radial coordinates inside the asteroid / KBO
1233
+ t : np.ndarray
1234
+ The time
1235
+ temp : np.ndarray
1236
+ The array of temperature as function of rd and t calculated by `thermal_diff`
1237
+ consts: dict
1238
+ The dictionary of constants
1239
+
1240
+ Returns
1241
+ -------
1242
+ np.ndarray
1243
+ the gas mass generation rate as function of time
1244
+
1245
+ """
1246
+ S: np.float64 = 3. * (1. - consts["Phi"]) / consts["rp"]
1247
+ dr = rd[1] - rd[0]
1248
+ rr = rd[:, np.newaxis] + np.zeros(len(t))
1249
+ mdot = (4 * Pi / 3.) * (S * Ah2o * np.exp(-Bh2o / temp) * np.sqrt(mu_H2O / (2. * Pi * Rgp * temp))) * (
1250
+ (rr + dr) ** 3 - (rr - dr) ** 3)
1251
+ mlost_max = (4 * Pi / 3.) * consts["rho_ice"] * ((rr + dr) ** 3 - (rr - dr) ** 3)
1252
+ mdot[cumulative_trapezoid(mdot, t, initial=0., axis=1) >= mlost_max] *= zero
1253
+ mmax = (4 * Pi / 3.) * consts["rho_ice"] * (rd.max() ** 3 - rd.min() ** 3)
1254
+ if np.any(cumulative_trapezoid(mdot.sum(axis=0), t, initial=0.) >= mmax):
1255
+ i = np.argwhere((cumulative_trapezoid(mdot.sum(axis=0), t, initial=0.) >= mmax))[0, 0]
1256
+ mdot[:, max(0, i - 1):] *= zero
1257
+ return mdot.sum(axis=0)
1258
+
1259
+
1260
+ def mdot_s(s: np.float64, t: np.ndarray, r: np.ndarray, consts: dict, It_d=300) -> np.ndarray:
1261
+ """
1262
+ Compute the gas mass generation rate for a given bodie at differents radius
1263
+
1264
+ Parameters
1265
+ ----------
1266
+ s: np.float64
1267
+ The size of the considered bodie
1268
+ t: np.ndarray
1269
+ The time at which evaluate the gas mass generation rate
1270
+ r: np.ndarray
1271
+ The distances from the central star at which evaluate the gas mass generation rate
1272
+ consts : dict
1273
+ The dictionary of constants
1274
+ It_d : int
1275
+ The number of iterations
1276
+
1277
+ Returns
1278
+ -------
1279
+ np.ndarray
1280
+ the gas mass generation rate as function of time and radius
1281
+
1282
+ """
1283
+ print("s={:.4f}".format(s / C_L), "m")
1284
+
1285
+ rd, t, temp = thermal_diff(d=s / 2, consts=consts, It_d=It_d, t=t)
1286
+ return np.array([mdot_s_l(rd=rd, t=t, temp=temp * (abs(R) / consts["a0"]) ** (-1 / 2),
1287
+ consts=consts) for R in r])
1288
+
1289
+
1290
+ def mdot_s_surf(s: np.float64, t: np.ndarray, r: np.ndarray, consts: dict, It_d=300) -> np.ndarray:
1291
+ """
1292
+ Compute the gas mass generation rate for a given bodies at differents radius if
1293
+ the gas sublimate only at its surface
1294
+
1295
+ Parameters
1296
+ ----------
1297
+ s: np.float64
1298
+ The size of the considered bodie
1299
+ t: np.ndarray
1300
+ The time at which evaluate the gas mass generation rate
1301
+ r: np.ndarray
1302
+ The distances from the central star at which evaluate the gas mass generation rate
1303
+ consts : dict
1304
+ The dictionary of constants
1305
+ It_d : int
1306
+ The number of iterations
1307
+
1308
+ Returns
1309
+ -------
1310
+ np.ndarray
1311
+ the gas mass generation rate as function of time and radius
1312
+
1313
+ """
1314
+ # print("s={:.4f}".format(s / C_L), "m")
1315
+ a1: np.float64 = 7.08e35 / (C_L * C_L * C_t)
1316
+ a2: np.float64 = 6062 # K
1317
+ T0: np.ndarray[np.float64] = (278 * ((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4)
1318
+ * ((consts["a0"] / au) ** (-1 / 2)))
1319
+ res: np.ndarray[np.float64] = np.array([Pi * s * s * 18 * mp * a1 *
1320
+ np.exp(- a2 / (T0 * (abs(R) / consts["a0"]) ** (-1 / 2)))
1321
+ / np.sqrt(T0 * (abs(R) / consts["a0"]) ** (-1 / 2)) for R in r])
1322
+ mlost_max = (Pi * s * s * s / 6.) * consts["rho_ice"]
1323
+ res[cumulative_trapezoid(res, t, initial=0., axis=1) >= mlost_max] *= zero
1324
+ return res
1325
+
1326
+
1327
+ def mdot_s_vol(s: np.float64, t: np.ndarray, r: np.ndarray, consts: dict, It_d=300) -> np.ndarray:
1328
+ """
1329
+ Compute the gas mass generation rate for a given bodies at differents radius if
1330
+ the gas sublimate in the whole volume neglecting thermal diffusion
1331
+
1332
+ Parameters
1333
+ ----------
1334
+ s: np.float64
1335
+ The size of the considered bodie
1336
+ t: np.ndarray
1337
+ The time at which evaluate the gas mass generation rate
1338
+ r: np.ndarray
1339
+ The distances from the central star at which evaluate the gas mass generation rate
1340
+ consts : dict
1341
+ The dictionary of constants
1342
+ It_d : int
1343
+ The number of iterations
1344
+
1345
+ Returns
1346
+ -------
1347
+ np.ndarray
1348
+ the gas mass generation rate as function of time and radius
1349
+
1350
+ """
1351
+ # print("s={:.4f}".format(s / C_L), "m")
1352
+ T0: np.ndarray[np.float64] = (278 * ((1. - consts["Abelt"]) * interpLbol(t, consts["Ms"]) / Lsun) ** (1 / 4)
1353
+ * ((consts["a0"] / au) ** (-1 / 2)))
1354
+ res: np.ndarray[np.float64] = np.array([(s * s * s / 6) * (3 * (1 - consts["Phi"]) / consts["rp"])
1355
+ * Ah2o * np.exp(- Bh2o / (T0 * (abs(R) / consts["a0"]) ** (-1 / 2)))
1356
+ * np.sqrt(
1357
+ 18 * mp / (2. * Pi * kb * T0 * (abs(R) / consts["a0"]) ** (-1 / 2)))
1358
+ for R in r])
1359
+ mlost_max = (Pi * s * s * s / 6.) * consts["rho_ice"]
1360
+ res[cumulative_trapezoid(res, t, initial=0., axis=1) >= mlost_max] *= zero
1361
+ return res
1362
+
1363
+
1364
+ def sig_dot_full_diff(tps: np.ndarray, r: np.ndarray, const: dict, kwargsN: dict = None, It_d: int = 100) -> np.ndarray:
1365
+ """
1366
+ Compute the gas surface density generation rate for a belt discribed into the const dictionary
1367
+
1368
+ Parameters
1369
+ ----------
1370
+ tps : np.ndarray
1371
+ The time at which evaluate the result
1372
+ r : np.ndarray
1373
+ The distance to the central star at which evaluate the solution
1374
+ const : dict
1375
+ The dictionary of constants
1376
+ kwargsN : dict
1377
+ The dictionary that defined the size distribution
1378
+ It_d : int
1379
+ The number of depth steps for thermal diffusion integration
1380
+
1381
+ Returns
1382
+ -------
1383
+ np.ndarray
1384
+
1385
+ """
1386
+
1387
+ sizes = np.geomspace(const["dmin"], const["dmax"], const["Its_p"])
1388
+
1389
+ mdots = np.array([mdot_s(s=s, t=tps, r=r, consts=const, It_d=It_d) for s in sizes])
1390
+ # The gas mass generation rates of each bodies's size for each radius for each time
1391
+
1392
+ if kwargsN is None: # Default size distribution
1393
+ kwargsN = dict(amax=np.double(1.e6) * C_L,
1394
+ abig=np.double(120e3) * C_L,
1395
+ amed=np.double(20.e3) * C_L, amin=C_L,
1396
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
1397
+ rho=np.double(3e3 * C_M / (C_L * C_L * C_L)))
1398
+
1399
+ ## Calcul of number of asteroids for all diameters and distance to central star
1400
+ sigma0: np.ndarray = (const["Mbelt"] * ((r / const["a0"]) ** (-3. / 2.))
1401
+ / (4. * Pi * const["a0"] * const["a0"] * (np.sqrt(1. + const["delta_a"] / const["a0"])
1402
+ - np.sqrt(
1403
+ 1. - const["delta_a"] / const["a0"])))) # initial surface density
1404
+ ni: np.ndarray = ((np.array([number_density(s, Mtot=np.double(1.), **kwargsN)
1405
+ for s in sizes])[:, np.newaxis] + np.zeros(len(r)))
1406
+ * (sigma0[:, np.newaxis] + np.zeros(len(sizes))).T) # number density
1407
+ ni: np.ndarray = ni[..., np.newaxis] + np.zeros(mdots.shape[-1])
1408
+ print("ni max={:.4f} min={:.4f}".format(ni.max(), ni.min()))
1409
+ print("mdots max={:.4f} mdots={:.4f}".format(mdots.max(), mdots.min()))
1410
+ return trapezoid(ni * mdots, sizes, axis=0).T
1411
+
1412
+
1413
+ def sig_dot_sub_surf(tps: np.ndarray, r: np.ndarray, const: dict, kwargsN: dict = None, It_d: int = 100) -> np.ndarray:
1414
+ """
1415
+ Compute the gas surface density generation rate for a belt discribed into the const dictionary
1416
+ if the sublimation append only at the surface of asteroids
1417
+
1418
+ Parameters
1419
+ ----------
1420
+ tps : np.ndarray
1421
+ The time at which evaluate the result
1422
+ r : np.ndarray
1423
+ The distance to the central star at which evaluate the solution
1424
+ const : dict
1425
+ The dictionary of constants
1426
+ kwargsN : dict
1427
+ The dictionary that defined the size distribution
1428
+ It_d : int
1429
+ The number of depth steps for thermal diffusion integration
1430
+
1431
+ Returns
1432
+ -------
1433
+ np.ndarray
1434
+
1435
+ """
1436
+
1437
+ sizes = np.geomspace(const["dmin"], const["dmax"], const["Its_p"])
1438
+
1439
+ mdots = np.array([mdot_s_surf(s=s, t=tps, r=r, consts=const, It_d=It_d) for s in sizes])
1440
+ # The gas mass generation rates of each bodies's size for each radius for each time
1441
+
1442
+ if kwargsN is None: # Default size distribution
1443
+ kwargsN = dict(amax=np.double(1.e6) * C_L,
1444
+ abig=np.double(120e3) * C_L,
1445
+ amed=np.double(20.e3) * C_L, amin=C_L,
1446
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
1447
+ rho=np.double(3e3 * C_M / (C_L * C_L * C_L)))
1448
+
1449
+ ## Calcul of number of asteroids for all diameters and distance to central star
1450
+ sigma0: np.ndarray = (const["Mbelt"] * ((r / const["a0"]) ** (-3. / 2.))
1451
+ / (4. * Pi * const["a0"] * const["a0"] * (np.sqrt(1. + const["delta_a"] / const["a0"])
1452
+ - np.sqrt(
1453
+ 1. - const["delta_a"] / const["a0"])))) # initial surface density
1454
+ ni: np.ndarray = ((np.array([number_density(s, Mtot=np.double(1.), **kwargsN)
1455
+ for s in sizes])[:, np.newaxis] + np.zeros(len(r)))
1456
+ * (sigma0[:, np.newaxis] + np.zeros(len(sizes))).T) # number density
1457
+ ni: np.ndarray = ni[..., np.newaxis] + np.zeros(mdots.shape[-1])
1458
+ # print("ni max={:.4f} min={:.4f}".format(ni.max(), ni.min()))
1459
+ # print("mdots max={:.4f} mdots={:.4f}".format(mdots.max(), mdots.min()))
1460
+ return trapezoid(ni * mdots, sizes, axis=0).T
1461
+
1462
+
1463
+ def sig_dot_sub_vol(tps: np.ndarray, r: np.ndarray, const: dict, kwargsN: dict = None, It_d: int = 100) -> np.ndarray:
1464
+ """
1465
+ Compute the gas surface density generation rate for a belt discribed into the const dictionary
1466
+ if the sublimation append only at the surface of asteroids
1467
+
1468
+ Parameters
1469
+ ----------
1470
+ tps : np.ndarray
1471
+ The time at which evaluate the result
1472
+ r : np.ndarray
1473
+ The distance to the central star at which evaluate the solution
1474
+ const : dict
1475
+ The dictionary of constants
1476
+ kwargsN : dict
1477
+ The dictionary that defined the size distribution
1478
+ It_d : int
1479
+ The number of depth steps for thermal diffusion integration
1480
+
1481
+ Returns
1482
+ -------
1483
+ np.ndarray
1484
+
1485
+ """
1486
+
1487
+ sizes = np.geomspace(const["dmin"], const["dmax"], const["Its_p"])
1488
+
1489
+ mdots = np.array([mdot_s_vol(s=s, t=tps, r=r, consts=const, It_d=It_d) for s in sizes])
1490
+ # The gas mass generation rates of each bodies's size for each radius for each time
1491
+
1492
+ if kwargsN is None: # Default size distribution
1493
+ kwargsN = dict(amax=np.double(1.e6) * C_L,
1494
+ abig=np.double(120e3) * C_L,
1495
+ amed=np.double(20.e3) * C_L, amin=C_L,
1496
+ qh=np.double(-4.5), qm=np.double(-1.2), ql=np.double(-3.6),
1497
+ rho=np.double(3e3 * C_M / (C_L * C_L * C_L)))
1498
+
1499
+ ## Calcul of number of asteroids for all diameters and distance to central star
1500
+ sigma0: np.ndarray = (const["Mbelt"] * ((r / const["a0"]) ** (-3. / 2.))
1501
+ / (4. * Pi * const["a0"] * const["a0"] * (np.sqrt(1. + const["delta_a"] / const["a0"])
1502
+ - np.sqrt(
1503
+ 1. - const["delta_a"] / const["a0"])))) # initial surface density
1504
+ ni: np.ndarray = ((np.array([number_density(s, Mtot=np.double(1.), **kwargsN)
1505
+ for s in sizes])[:, np.newaxis] + np.zeros(len(r)))
1506
+ * (sigma0[:, np.newaxis] + np.zeros(len(sizes))).T) # number density
1507
+ ni: np.ndarray = ni[..., np.newaxis] + np.zeros(mdots.shape[-1])
1508
+ # print("ni max={:.4f} min={:.4f}".format(ni.max(), ni.min()))
1509
+ # print("mdots max={:.4f} mdots={:.4f}".format(mdots.max(), mdots.min()))
1510
+ return trapezoid(ni * mdots, sizes, axis=0).T