PythonicDISORT 0.6.2__py3-none-any.whl → 0.8.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,569 @@
1
+ from PythonicDISORT.subroutines import _mathscr_v
2
+ from PythonicDISORT._solve_for_gen_and_part_sols import _solve_for_gen_and_part_sols
3
+ from PythonicDISORT._solve_for_coeffs import _solve_for_coeffs
4
+
5
+ import numpy as np
6
+ from math import pi
7
+
8
+ def _assemble_intensity_and_fluxes(
9
+ scaled_omega_arr, # Delta-scaled single-scattering albedos
10
+ tau_arr, # Lower boundary of layers
11
+ scaled_tau_arr_with_0, # Delta-scaled lower boundary of layers with 0 inserted in front
12
+ mu_arr_pos, mu_arr, # Quadrature nodes for 1) upper 2) both hemispheres
13
+ M_inv, W, # 1) 1 / mu; 2) quadrature weights for each hemisphere
14
+ N, NQuad, NLeg, # Number of 1) upper 2) both hemispheres quadrature nodes; 3) phase function Legendre coefficients
15
+ NFourier, NLayers, NBDRF, # Number of 1) intensity Fourier modes; 2) layers; 3) BDRF Fourier modes
16
+ is_atmos_multilayered, # Is the atmosphere multilayered?
17
+ weighted_scaled_Leg_coeffs, # Weighted and delta-scaled Legendre coefficients
18
+ BDRF_Fourier_modes, # BDRF Fourier modes
19
+ mu0, I0, I0_orig, phi0, # Properties of direct beam. If I0 was rescaled to 1, I0_orig is the original I0
20
+ there_is_beam_source, # Is there a beam source?
21
+ b_pos, b_neg, # Dirichlet BCs
22
+ b_pos_is_scalar, b_neg_is_scalar, # Is each Dirichlet BCs scalar (isotropic)?
23
+ s_poly_coeffs, # Polynomial coefficients of isotropic source
24
+ Nscoeffs, # Number of isotropic source polynomial coefficients
25
+ there_is_iso_source, # Is there an isotropic source?
26
+ scale_tau, # Delta-scale factor for tau
27
+ only_flux, # Only compute fluxes?
28
+ NLayers_to_use_sparse, # Number of layers above or equal which to use sparse matrices
29
+ _autograd_compatible, # Should the output functions be compatible with autograd?
30
+ ):
31
+ """Assembles the solution functions: intensity `u`, upward flux `flux_up`, downward flux `flux_down`
32
+ from the previously solved eigenpairs and coefficients from the boundary conditions.
33
+ Returns these solution functions.
34
+ This function is wrapped by the `pydisort` function.
35
+ It should be called through `pydisort` and never directly.
36
+ It has many seemingly redundant arguments, which are described below,
37
+ to maximize precomputation in `pydisort`
38
+ These arguments are passed on to the `_solve_for_gen_and_part_sols` and `_solve_for_coeffs` functions
39
+ which this function wraps. See the Jupyter Notebook, especially section 3, for
40
+ documentation, explanation and derivation.
41
+
42
+ Arguments of _assemble_intensity_and_fluxes
43
+ | Variable | Type / Shape |
44
+ | ------------------------------ | ---------------------------------- |
45
+ | `scaled_omega_arr` | `NLayers` |
46
+ | `tau_arr` | `NLayers` |
47
+ | `scaled_tau_arr_with_0` | `NLayers + 1` |
48
+ | `mu_arr_pos` | `NQuad/2` |
49
+ | `mu_arr` | `NQuad` |
50
+ | `M_inv` | `NQuad/2` |
51
+ | `W` | `NQuad/2` |
52
+ | `N` | scalar |
53
+ | `NQuad` | scalar |
54
+ | `NLeg` | scalar |
55
+ | `NFourier` | scalar |
56
+ | `NLayers` | scalar |
57
+ | `NBDRF` | scalar |
58
+ | `is_atmos_multilayered` | boolean |
59
+ | `weighted_scaled_Leg_coeffs` | `NLayers x NLeg` |
60
+ | `BDRF_Fourier_modes` | `NBDRF` |
61
+ | `mu0` | scalar |
62
+ | `I0` | scalar |
63
+ | `I0_orig` | scalar |
64
+ | `phi0` | scalar |
65
+ | `there_is_beam_source` | scalar |
66
+ | `b_pos` | `NQuad/2 x NFourier` or scalar |
67
+ | `b_neg` | `NQuad/2 x NFourier` or scalar |
68
+ | `b_pos_is_scalar` | boolean |
69
+ | `b_neg_is_scalar` | boolean |
70
+ | `s_poly_coeffs` | `NLayers x Nscoeffs` or `Nscoeffs` |
71
+ | `Nscoeffs` | scalar |
72
+ | `there_is_iso_source` | boolean |
73
+ | `scale_tau` | `NLayers` |
74
+ | `only_flux` | boolean |
75
+ | `NLayers_to_use_sparse` | scalar |
76
+ | `_autograd_compatible` | boolean |
77
+
78
+ Notable internal variables of _assemble_intensity_and_fluxes
79
+ | Variable | Type / Shape |
80
+ | ----------------- | -------------------------------------- |
81
+ | `G_collect` | `NFourier x NLayers x NQuad x NQuad` |
82
+ | `G_collect_0` | `NLayers x NQuad x NQuad` |
83
+ | `G_inv_collect_0` | `NLayers x NQuad x NQuad` or `None` |
84
+ | `K_collect` | `NFourier x NLayers x NQuad` |
85
+ | `K_collect_0` | `NLayers x NQuad` |
86
+ | `B_collect` | `NFourier x NLayers x NQuad` or `None` |
87
+ | `B_collect_0` | `NLayers x NQuad` or `None` |
88
+ | `B_pos` | `NLayers x NQuad/2` or `None` |
89
+ | `B_neg` | `NLayers x NQuad/2` or `None` |
90
+ | `GC_collect` | `NFourier x NLayers x NQuad x NQuad` |
91
+ | `GC_collect_0` | `NLayers x NQuad x NQuad` |
92
+ | `GC_pos` | `NLayers x NQuad/2 x NQuad` |
93
+ | `GC_neg` | `NLayers x NQuad/2 x NQuad` |
94
+ """
95
+ if _autograd_compatible:
96
+ import autograd.numpy as np
97
+ else:
98
+ import numpy as np
99
+
100
+ ################################## Assemble uncorrected intensity and flux functions #######################################
101
+
102
+ # Compute all the necessary quantities
103
+ # --------------------------------------------------------------------------------------------------------------------------
104
+ outputs = _solve_for_gen_and_part_sols(
105
+ NFourier,
106
+ scaled_omega_arr,
107
+ mu_arr_pos, mu_arr,
108
+ M_inv, W,
109
+ N, NQuad, NLeg,
110
+ NLayers,
111
+ weighted_scaled_Leg_coeffs,
112
+ mu0, I0,
113
+ there_is_beam_source,
114
+ Nscoeffs,
115
+ there_is_iso_source,
116
+ )
117
+ if there_is_beam_source:
118
+ if there_is_iso_source:
119
+ G_collect, K_collect, B_collect, G_inv_collect_0 = outputs
120
+ else:
121
+ G_collect, K_collect, B_collect = outputs
122
+ G_inv_collect_0 = None
123
+ B_collect_0 = B_collect[0, :, :]
124
+ else:
125
+ if there_is_iso_source:
126
+ G_collect, K_collect, G_inv_collect_0 = outputs
127
+ else:
128
+ G_collect, K_collect = outputs
129
+ G_inv_collect_0 = None
130
+ B_collect = None
131
+
132
+ GC_collect = _solve_for_coeffs(
133
+ NFourier,
134
+ G_collect,
135
+ K_collect,
136
+ B_collect,
137
+ G_inv_collect_0,
138
+ tau_arr,
139
+ scaled_tau_arr_with_0,
140
+ mu_arr, mu_arr_pos, mu_arr_pos * W,
141
+ N, NQuad,
142
+ NLayers, NBDRF,
143
+ is_atmos_multilayered,
144
+ BDRF_Fourier_modes,
145
+ mu0, I0,
146
+ there_is_beam_source,
147
+ b_pos, b_neg,
148
+ b_pos_is_scalar, b_neg_is_scalar,
149
+ s_poly_coeffs,
150
+ Nscoeffs,
151
+ there_is_iso_source,
152
+ NLayers_to_use_sparse,
153
+ )
154
+
155
+ G_collect_0 = G_collect[0, :, :, :]
156
+ K_collect_0 = K_collect[0, :, :]
157
+ GC_collect_0 = GC_collect[0, :, :, :]
158
+ # --------------------------------------------------------------------------------------------------------------------------
159
+
160
+ if not only_flux:
161
+
162
+ # Construct the intensity function (refer to Section 3.7 of the Comprehensive Documentation)
163
+ # --------------------------------------------------------------------------------------------------------------------------
164
+ def u(tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
165
+ """
166
+ Intensity function with arguments `(tau, phi)` of types `(array or float, array or float)`.
167
+ Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
168
+ Pass `return_Fourier_error = True` (defaults to `False`) to return the
169
+ Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
170
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
171
+ to switch to an antiderivative of the function with respect to `tau`.
172
+ """
173
+ tau = np.atleast_1d(tau)
174
+ phi = np.atleast_1d(phi)
175
+ assert np.all(tau >= 0)
176
+ assert np.all(tau <= tau_arr[-1])
177
+
178
+ # Atmospheric layer indices
179
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
180
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
181
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
182
+
183
+ # Delta-M scaling
184
+ if np.any(scale_tau != np.ones(NLayers)):
185
+ tau_dist_from_top = tau_arr[l] - tau
186
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
187
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
188
+ else:
189
+ scaled_tau = tau
190
+
191
+ exponent = np.concatenate(
192
+ [
193
+ K_collect[:, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[None, :, None],
194
+ K_collect[:, l, N:] * (scaled_tau - scaled_tau_arr_l)[None, :, None],
195
+ ],
196
+ axis=-1,
197
+ )
198
+
199
+ if is_antiderivative_wrt_tau:
200
+ if there_is_beam_source:
201
+ um = np.einsum(
202
+ "mtij, mtj -> mti",
203
+ GC_collect[:, l, :, :],
204
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect[:, l, :]),
205
+ optimize=True,
206
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau[None, :, None] / mu0) / (
207
+ -scale_tau[None, l, None] / mu0
208
+ )
209
+ else:
210
+ um = np.einsum(
211
+ "mtij, mtj -> mti",
212
+ GC_collect[:, l, :, :],
213
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect[:, l, :]),
214
+ optimize=True,
215
+ )
216
+ else:
217
+ if there_is_beam_source:
218
+ um = np.einsum(
219
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
220
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau[None, :, None] / mu0)
221
+ else:
222
+ um = np.einsum(
223
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
224
+ )
225
+
226
+ # Contribution from particular solution for isotropic internal sources
227
+ if there_is_iso_source:
228
+ _mathscr_v_contribution = _mathscr_v(
229
+ tau, l,
230
+ s_poly_coeffs,
231
+ Nscoeffs,
232
+ G_collect_0,
233
+ K_collect_0,
234
+ G_inv_collect_0,
235
+ mu_arr,
236
+ _autograd_compatible,
237
+ is_antiderivative_wrt_tau
238
+ )
239
+
240
+ if _autograd_compatible:
241
+ um = um + np.concatenate((_mathscr_v_contribution.T, np.zeros((NFourier - 1, len(tau), NQuad))))
242
+ else:
243
+ um[0, :, :] += _mathscr_v_contribution.T
244
+
245
+ intensities = np.einsum(
246
+ "mti, mp -> itp",
247
+ um,
248
+ np.cos(np.arange(NFourier)[:, None] * (phi0 - phi)[None, :]),
249
+ optimize=True,
250
+ )
251
+
252
+ if return_Fourier_error:
253
+ exponent = np.concatenate(
254
+ [
255
+ K_collect[-1, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
256
+ K_collect[-1, l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
257
+ ],
258
+ axis=-1,
259
+ )
260
+ if is_antiderivative_wrt_tau:
261
+ if there_is_beam_source:
262
+ ulast = np.einsum(
263
+ "tij, tj -> it",
264
+ GC_collect[-1, l, :, :],
265
+ np.exp(exponent) / (scale_tau[l, None] * K_collect[-1, l, :]),
266
+ optimize=True,
267
+ ) + B_collect[-1, l, :].T * np.exp(-scaled_tau[None, :] / mu0) / (
268
+ -scale_tau[None, l] / mu0
269
+ )
270
+ else:
271
+ ulast = np.einsum(
272
+ "tij, tj -> it",
273
+ GC_collect[-1, l, :, :],
274
+ np.exp(exponent) / (scale_tau[l, None] * K_collect[-1, l, :]),
275
+ optimize=True,
276
+ )
277
+ else:
278
+ if there_is_beam_source:
279
+ ulast = np.einsum(
280
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
281
+ ) + B_collect[-1, l, :].T * np.exp(-scaled_tau[None, :] / mu0)
282
+ else:
283
+ ulast = np.einsum(
284
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
285
+ )
286
+
287
+ intensities_abs = np.abs(intensities)
288
+ Fourier_error = np.max(
289
+ np.divide(
290
+ np.abs(
291
+ ulast[:, :, None] * np.cos((NFourier - 1) * (phi0 - phi))[None, None, :]
292
+ ),
293
+ intensities_abs,
294
+ out=np.zeros_like(intensities_abs),
295
+ where=intensities_abs > 1e-8,
296
+ )
297
+ )
298
+ return I0_orig * np.squeeze(intensities), Fourier_error
299
+ else:
300
+ return I0_orig * np.squeeze(intensities)
301
+ # --------------------------------------------------------------------------------------------------------------------------
302
+
303
+ # Construct u0
304
+ # --------------------------------------------------------------------------------------------------------------------------
305
+ def u0(tau, is_antiderivative_wrt_tau=False, _return_act_dscale_for_reclass=False):
306
+ """
307
+ Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
308
+ Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
309
+ This function is useful for calculating actinic fluxes and other quantities of interest,
310
+ but reclassification of delta-scaled flux and other corrections must be done manually
311
+ (for actinic flux `generate_diff_act_flux_funcs` will automatically perform the reclassification).
312
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
313
+ to switch to an antiderivative of the function with respect to `tau`.
314
+ """
315
+ tau = np.atleast_1d(tau)
316
+ assert np.all(tau >= 0)
317
+ assert np.all(tau <= tau_arr[-1])
318
+
319
+ # Atmospheric layer indices
320
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
321
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
322
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
323
+
324
+ # Delta-M scaling
325
+ if np.any(scale_tau != np.ones(NLayers)):
326
+ tau_dist_from_top = tau_arr[l] - tau
327
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
328
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
329
+ # The following complements the function `subroutines.generate_diff_act_flux_funcs`
330
+ # in performing the reclassification of delta-scaled actinic flux
331
+ if _return_act_dscale_for_reclass:
332
+ if is_antiderivative_wrt_tau:
333
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0) - I0 * np.exp(-tau / mu0) * -mu0
334
+ else:
335
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
336
+ else:
337
+ scaled_tau = tau
338
+ if _return_act_dscale_for_reclass:
339
+ act_dscale_reclassification = 0
340
+
341
+ exponent = np.concatenate(
342
+ [
343
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
344
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
345
+ ],
346
+ axis=-1,
347
+ )
348
+
349
+ if is_antiderivative_wrt_tau:
350
+ if there_is_beam_source:
351
+ u0 = np.einsum(
352
+ "tij, tj -> it",
353
+ GC_collect_0[l, :, :],
354
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
355
+ optimize=True,
356
+ ) + B_collect_0[l, :].T * np.exp(-scaled_tau[None, :] / mu0) / (
357
+ -scale_tau[None, l] / mu0
358
+ )
359
+ else:
360
+ u0 = np.einsum(
361
+ "tij, tj -> it",
362
+ GC_collect_0[l, :, :],
363
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
364
+ optimize=True,
365
+ )
366
+ else:
367
+ if there_is_beam_source:
368
+ u0 = np.einsum(
369
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
370
+ ) + B_collect_0[l, :].T * np.exp(-scaled_tau[None, :] / mu0)
371
+ else:
372
+ u0 = np.einsum(
373
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
374
+ )
375
+
376
+ # Contribution from particular solution for isotropic internal sources
377
+ if there_is_iso_source:
378
+ _mathscr_v_contribution = _mathscr_v(
379
+ tau, l,
380
+ s_poly_coeffs,
381
+ Nscoeffs,
382
+ G_collect_0,
383
+ K_collect_0,
384
+ G_inv_collect_0,
385
+ mu_arr,
386
+ _autograd_compatible,
387
+ is_antiderivative_wrt_tau
388
+ )
389
+ u0 = u0 + _mathscr_v_contribution
390
+
391
+ if _return_act_dscale_for_reclass:
392
+ return I0_orig * np.squeeze(u0), act_dscale_reclassification
393
+ else:
394
+ return I0_orig * np.squeeze(u0)
395
+ # --------------------------------------------------------------------------------------------------------------------------
396
+
397
+ # Construct the flux functions (refer to Section 3.8 of the Comprehensive Documentation)
398
+ # --------------------------------------------------------------------------------------------------------------------------
399
+ GC_pos = GC_collect_0[:, :N, :]
400
+ GC_neg = GC_collect_0[:, N:, :]
401
+ if there_is_beam_source:
402
+ B_pos = B_collect_0[:, :N].T
403
+ B_neg = B_collect_0[:, N:].T
404
+
405
+
406
+ def flux_up(tau, is_antiderivative_wrt_tau=False):
407
+ """
408
+ (Energetic) Flux function with argument `tau` (type: array or float) for positive (upward) `mu` values.
409
+ Returns the diffuse flux magnitudes (same type and size as `tau`).
410
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
411
+ to switch to an antiderivative of the function with respect to `tau`.
412
+ """
413
+ tau = np.atleast_1d(tau)
414
+ assert np.all(tau >= 0)
415
+ assert np.all(tau <= tau_arr[-1])
416
+
417
+ # Atmospheric layer indices
418
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
419
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
420
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
421
+
422
+ # Delta-M scaling
423
+ if np.any(scale_tau != np.ones(NLayers)):
424
+ tau_dist_from_top = tau_arr[l] - tau
425
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
426
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
427
+ else:
428
+ scaled_tau = tau
429
+
430
+ if there_is_iso_source:
431
+ _mathscr_v_contribution = _mathscr_v(
432
+ tau, l,
433
+ s_poly_coeffs,
434
+ Nscoeffs,
435
+ G_collect_0,
436
+ K_collect_0,
437
+ G_inv_collect_0,
438
+ mu_arr,
439
+ _autograd_compatible,
440
+ is_antiderivative_wrt_tau
441
+ )[:N, :]
442
+ else:
443
+ _mathscr_v_contribution = 0
444
+
445
+ if there_is_beam_source:
446
+ if is_antiderivative_wrt_tau:
447
+ direct_beam_contribution = (
448
+ B_pos[:, l] * np.exp(-scaled_tau[None, :] / mu0) / (-scale_tau[None, l] / mu0)
449
+ )
450
+ else:
451
+ direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau[None, :] / mu0)
452
+ else:
453
+ direct_beam_contribution = 0
454
+
455
+ exponent = np.concatenate(
456
+ [
457
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
458
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
459
+ ],
460
+ axis=-1,
461
+ )
462
+
463
+ if is_antiderivative_wrt_tau:
464
+ u0_pos = (
465
+ np.einsum(
466
+ "tij, tj -> it",
467
+ GC_pos[l, :, :],
468
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
469
+ )
470
+ + direct_beam_contribution
471
+ + _mathscr_v_contribution
472
+ )
473
+ else:
474
+ u0_pos = (
475
+ np.einsum("tij, tj -> it", GC_pos[l, :, :], np.exp(exponent))
476
+ + direct_beam_contribution
477
+ + _mathscr_v_contribution
478
+ )
479
+
480
+ flux = 2 * pi * (mu_arr_pos * W) @ u0_pos
481
+ return I0_orig * np.squeeze(flux)[()]
482
+
483
+
484
+ def flux_down(tau, is_antiderivative_wrt_tau=False):
485
+ """
486
+ (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
487
+ Returns a tuple of the diffuse and direct flux magnitudes respectively where each entry is of the
488
+ same type and size as `tau`.
489
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
490
+ to switch to an antiderivative of the function with respect to `tau`.
491
+ """
492
+ tau = np.atleast_1d(tau)
493
+ assert np.all(tau >= 0)
494
+ assert np.all(tau <= tau_arr[-1])
495
+
496
+ # Atmospheric layer indices
497
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
498
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
499
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
500
+
501
+ # Delta-M scaling
502
+ if np.any(scale_tau != np.ones(NLayers)):
503
+ tau_dist_from_top = tau_arr[l] - tau
504
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
505
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
506
+ else:
507
+ scaled_tau = tau
508
+
509
+ if there_is_iso_source:
510
+ _mathscr_v_contribution = _mathscr_v(
511
+ tau, l,
512
+ s_poly_coeffs,
513
+ Nscoeffs,
514
+ G_collect_0,
515
+ K_collect_0,
516
+ G_inv_collect_0,
517
+ mu_arr,
518
+ _autograd_compatible,
519
+ is_antiderivative_wrt_tau
520
+ )[N:, :]
521
+ else:
522
+ _mathscr_v_contribution = 0
523
+
524
+ if there_is_beam_source:
525
+ if is_antiderivative_wrt_tau:
526
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau[None, :] / mu0) / (-scale_tau[None, l] / mu0)
527
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0) * -mu0
528
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau[None, l] / mu0)
529
+ else:
530
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau[None, :] / mu0)
531
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0)
532
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
533
+ else:
534
+ direct_beam_contribution = 0
535
+ direct_beam = 0
536
+ direct_beam_scaled = 0
537
+
538
+ exponent = np.concatenate(
539
+ [
540
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
541
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
542
+ ],
543
+ axis=-1,
544
+ )
545
+
546
+ if is_antiderivative_wrt_tau:
547
+ u0_neg = (
548
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]))
549
+ + direct_beam_contribution
550
+ + _mathscr_v_contribution
551
+ )
552
+ else:
553
+ u0_neg = (
554
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent))
555
+ + direct_beam_contribution
556
+ + _mathscr_v_contribution
557
+ )
558
+
559
+ diffuse_flux = 2 * pi * (mu_arr_pos * W) @ u0_neg + direct_beam_scaled - direct_beam
560
+ return (
561
+ I0_orig * np.squeeze(diffuse_flux)[()],
562
+ I0_orig * I0 * np.squeeze(direct_beam)[()],
563
+ )
564
+ # --------------------------------------------------------------------------------------------------------------------------
565
+
566
+ if only_flux:
567
+ return flux_up, flux_down, u0
568
+ else:
569
+ return flux_up, flux_down, u0, u