PythonicDISORT 0.6.1__py3-none-any.whl → 0.9.2__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,594 @@
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
+ Nscoeffs, # Number of isotropic source polynomial coefficients
24
+ s_poly_coeffs, # Polynomial coefficients of isotropic source
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
+ use_banded_solver_NLayers, # Number of layers above or equal which to use `scipy.linalg.solve_banded`
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
+ | `Nscoeffs` | scalar |
71
+ | `s_poly_coeffs` | `NLayers x Nscoeffs` or `Nscoeffs` |
72
+ | `there_is_iso_source` | boolean |
73
+ | `scale_tau` | `NLayers` |
74
+ | `only_flux` | boolean |
75
+ | `use_banded_solver_NLayers` | 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 and there_is_iso_source:
118
+ G_collect, K_collect, B_collect, G_inv_collect_0 = outputs
119
+ B_collect_0 = B_collect[0, :, :]
120
+ elif there_is_beam_source and not there_is_iso_source:
121
+ G_collect, K_collect, B_collect = outputs
122
+ B_collect_0 = B_collect[0, :, :]
123
+ G_inv_collect_0 = None
124
+ elif not there_is_beam_source and there_is_iso_source:
125
+ G_collect, K_collect, G_inv_collect_0 = outputs
126
+ B_collect = None
127
+ else:
128
+ G_collect, K_collect = outputs
129
+ B_collect = None
130
+ G_inv_collect_0 = None
131
+
132
+
133
+ GC_collect = _solve_for_coeffs(
134
+ NFourier,
135
+ G_collect,
136
+ K_collect,
137
+ B_collect,
138
+ G_inv_collect_0,
139
+ tau_arr,
140
+ scaled_tau_arr_with_0,
141
+ mu_arr, mu_arr_pos, mu_arr_pos * W,
142
+ N, NQuad,
143
+ NLayers, NBDRF,
144
+ is_atmos_multilayered,
145
+ BDRF_Fourier_modes,
146
+ mu0, I0,
147
+ there_is_beam_source,
148
+ b_pos, b_neg,
149
+ b_pos_is_scalar, b_neg_is_scalar,
150
+ Nscoeffs,
151
+ s_poly_coeffs,
152
+ there_is_iso_source,
153
+ use_banded_solver_NLayers,
154
+ )
155
+
156
+ G_collect_0 = G_collect[0, :, :, :]
157
+ K_collect_0 = K_collect[0, :, :]
158
+ GC_collect_0 = GC_collect[0, :, :, :]
159
+ # --------------------------------------------------------------------------------------------------------------------------
160
+
161
+ if not only_flux:
162
+
163
+ # Construct the intensity function (refer to Section 3.7 of the Comprehensive Documentation)
164
+ # --------------------------------------------------------------------------------------------------------------------------
165
+ def u(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
166
+ """
167
+ Intensity function with arguments `(tau, phi)` of types `(array or float, array or float)`.
168
+ Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
169
+ Pass `return_Fourier_error = True` (defaults to `False`) to return the
170
+ Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
171
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
172
+ to switch to an antiderivative of the function with respect to `tau`.
173
+ """
174
+ tau = np.atleast_1d(tau)
175
+ phi = np.atleast_1d(phi)
176
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
177
+ raise ValueError("tau input outside the tau range specified for the atmosphere (check `tau_arr`).")
178
+
179
+ # Atmospheric layer indices
180
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
181
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
182
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
183
+
184
+ # Delta-M scaling
185
+ if np.any(scale_tau != np.ones(NLayers)):
186
+ tau_dist_from_top = tau_arr[l] - tau
187
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
188
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
189
+ else:
190
+ scaled_tau = tau
191
+
192
+ exponent = np.concatenate(
193
+ [
194
+ K_collect[:, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[None, :, None],
195
+ K_collect[:, l, N:] * (scaled_tau - scaled_tau_arr_l)[None, :, None],
196
+ ],
197
+ axis=-1,
198
+ )
199
+
200
+ if is_antiderivative_wrt_tau and there_is_beam_source:
201
+ um = (
202
+ np.einsum(
203
+ "mtij, mtj -> mti",
204
+ GC_collect[:, l, :, :],
205
+ np.exp(exponent) / (scale_tau[None, :, None] * K_collect)[:, l, :],
206
+ optimize=True,
207
+ )
208
+ + (B_collect / (-scale_tau / mu0)[None, :, None])[:, l, :]
209
+ * np.exp(-scaled_tau / mu0)[None, :, None]
210
+ )
211
+ elif is_antiderivative_wrt_tau and not there_is_beam_source:
212
+ um = np.einsum(
213
+ "mtij, mtj -> mti",
214
+ GC_collect[:, l, :, :],
215
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect)[:, l, :],
216
+ optimize=True,
217
+ )
218
+ elif not is_antiderivative_wrt_tau and there_is_beam_source:
219
+ um = np.einsum(
220
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
221
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau / mu0)[None, :, None]
222
+ else:
223
+ um = np.einsum(
224
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
225
+ )
226
+
227
+ # Contribution from particular solution for isotropic internal sources
228
+ if there_is_iso_source:
229
+ l_uniq, l_inv = np.unique(l, return_inverse=True)
230
+ _mathscr_v_contribution = _mathscr_v(
231
+ tau, l_inv, Nscoeffs,
232
+ s_poly_coeffs[l_uniq],
233
+ G_collect_0[l_uniq],
234
+ K_collect_0[l_uniq],
235
+ G_inv_collect_0[l_uniq],
236
+ mu_arr,
237
+ is_antiderivative_wrt_tau,
238
+ autograd_compatible
239
+ )
240
+
241
+ if NFourier == 1:
242
+ um = um + _mathscr_v_contribution.T
243
+ elif autograd_compatible:
244
+ um = um + np.concatenate((_mathscr_v_contribution.T, np.zeros((NFourier - 1, len(tau), NQuad))))
245
+ else:
246
+ um[0, :, :] += _mathscr_v_contribution.T
247
+
248
+ u = np.einsum(
249
+ "mti, mp -> itp",
250
+ um,
251
+ np.cos(np.arange(NFourier)[:, None] * (phi0 - phi)[None, :]),
252
+ optimize=True,
253
+ )
254
+
255
+ if return_Fourier_error:
256
+ exponent = np.concatenate(
257
+ [
258
+ K_collect[-1, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
259
+ K_collect[-1, l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
260
+ ],
261
+ axis=-1,
262
+ )
263
+ if is_antiderivative_wrt_tau and there_is_beam_source:
264
+ ulast = (
265
+ np.einsum(
266
+ "tij, tj -> it",
267
+ GC_collect[-1, l, :, :],
268
+ np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
269
+ optimize=True,
270
+ )
271
+ + (B_collect.T[-1, :, :] / (-scale_tau / mu0)[None, :])[:, l]
272
+ * np.exp(-scaled_tau / mu0)[None, :]
273
+ )
274
+ elif is_antiderivative_wrt_tau and not there_is_beam_source:
275
+ ulast = np.einsum(
276
+ "tij, tj -> it",
277
+ GC_collect[-1, l, :, :],
278
+ np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
279
+ optimize=True,
280
+ )
281
+ elif not is_antiderivative_wrt_tau and not there_is_beam_source:
282
+ ulast = np.einsum(
283
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
284
+ ) + B_collect.T[-1, :, l] * np.exp(-scaled_tau / mu0)[None, :]
285
+ else:
286
+ ulast = np.einsum(
287
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
288
+ )
289
+
290
+ u_abs = np.abs(u)
291
+ Fourier_error = np.max(
292
+ np.divide(
293
+ np.abs(
294
+ ulast[:, :, None] * np.cos((NFourier - 1) * (phi0 - phi))[None, None, :]
295
+ ),
296
+ u_abs,
297
+ out=np.zeros_like(u_abs),
298
+ where=u_abs > 1e-15,
299
+ )
300
+ )
301
+
302
+ if return_tau_arr:
303
+ return I0_orig * np.squeeze(u), Fourier_error, tau_arr
304
+ else:
305
+ return I0_orig * np.squeeze(u), Fourier_error
306
+ else:
307
+ if return_tau_arr:
308
+ return I0_orig * np.squeeze(u), tau_arr
309
+ else:
310
+ return I0_orig * np.squeeze(u)
311
+ # --------------------------------------------------------------------------------------------------------------------------
312
+
313
+ # Construct u0
314
+ # --------------------------------------------------------------------------------------------------------------------------
315
+ def u0(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False, _return_act_dscale_for_reclass=False):
316
+ """
317
+ Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
318
+ Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
319
+ This function is useful for calculating actinic fluxes and other quantities of interest,
320
+ but reclassification of delta-scaled flux and other corrections must be done manually
321
+ (for actinic flux `generate_diff_act_flux_funcs` will automatically perform the reclassification).
322
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
323
+ to switch to an antiderivative of the function with respect to `tau`.
324
+ """
325
+ tau = np.atleast_1d(tau)
326
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
327
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
328
+
329
+ # Atmospheric layer indices
330
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
331
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
332
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
333
+
334
+ # Delta-M scaling
335
+ if np.any(scale_tau != np.ones(NLayers)):
336
+ tau_dist_from_top = tau_arr[l] - tau
337
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
338
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
339
+ # The following complements the function `subroutines.generate_diff_act_flux_funcs`
340
+ # in performing the reclassification of delta-scaled actinic flux
341
+ if _return_act_dscale_for_reclass:
342
+ if is_antiderivative_wrt_tau:
343
+ act_dscale_reclassification = (
344
+ I0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0)
345
+ - I0 * np.exp(-tau / mu0) * -mu0
346
+ )
347
+ else:
348
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
349
+ else:
350
+ scaled_tau = tau
351
+ if _return_act_dscale_for_reclass:
352
+ act_dscale_reclassification = 0
353
+
354
+ exponent = np.concatenate(
355
+ [
356
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
357
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
358
+ ],
359
+ axis=-1,
360
+ )
361
+
362
+ if is_antiderivative_wrt_tau and there_is_beam_source:
363
+ u0 = (
364
+ np.einsum(
365
+ "tij, tj -> it",
366
+ GC_collect_0[l, :, :],
367
+ np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
368
+ optimize=True,
369
+ )
370
+ + (B_collect_0.T / (-scale_tau / mu0)[None, :])[:, l]
371
+ * np.exp(-scaled_tau / mu0)[None, :]
372
+ )
373
+ if is_antiderivative_wrt_tau and not there_is_beam_source:
374
+ u0 = np.einsum(
375
+ "tij, tj -> it",
376
+ GC_collect_0[l, :, :],
377
+ np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
378
+ optimize=True,
379
+ )
380
+ if not is_antiderivative_wrt_tau and there_is_beam_source:
381
+ u0 = np.einsum(
382
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
383
+ ) + B_collect_0.T[:, l] * np.exp(-scaled_tau / mu0)[None, :]
384
+ else:
385
+ u0 = np.einsum(
386
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
387
+ )
388
+
389
+ # Contribution from particular solution for isotropic internal sources
390
+ if there_is_iso_source:
391
+ l_uniq, l_inv = np.unique(l, return_inverse=True)
392
+ _mathscr_v_contribution = _mathscr_v(
393
+ tau, l_inv, Nscoeffs,
394
+ s_poly_coeffs[l_uniq],
395
+ G_collect_0[l_uniq],
396
+ K_collect_0[l_uniq],
397
+ G_inv_collect_0[l_uniq],
398
+ mu_arr,
399
+ is_antiderivative_wrt_tau,
400
+ autograd_compatible
401
+ )
402
+ u0 = u0 + _mathscr_v_contribution
403
+
404
+ if return_tau_arr and _return_act_dscale_for_reclass:
405
+ return I0_orig * np.squeeze(u0), tau_arr, act_dscale_reclassification
406
+ elif return_tau_arr and not _return_act_dscale_for_reclass:
407
+ return I0_orig * np.squeeze(u0), tau_arr
408
+ elif not return_tau_arr and _return_act_dscale_for_reclass:
409
+ return I0_orig * np.squeeze(u0), act_dscale_reclassification
410
+ else:
411
+ return I0_orig * np.squeeze(u0)
412
+ # --------------------------------------------------------------------------------------------------------------------------
413
+
414
+ # Construct the flux functions (refer to Section 3.8 of the Comprehensive Documentation)
415
+ # --------------------------------------------------------------------------------------------------------------------------
416
+ GC_pos = GC_collect_0[:, :N, :]
417
+ GC_neg = GC_collect_0[:, N:, :]
418
+ if there_is_beam_source:
419
+ B_pos = B_collect_0[:, :N].T
420
+ B_neg = B_collect_0[:, N:].T
421
+
422
+
423
+ def flux_up(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
424
+ """
425
+ (Energetic) Flux function with argument `tau` (type: array or float) for positive (upward) `mu` values.
426
+ Returns the diffuse flux magnitudes (same type and size as `tau`).
427
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
428
+ to switch to an antiderivative of the function with respect to `tau`.
429
+ """
430
+ tau = np.atleast_1d(tau)
431
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
432
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
433
+
434
+ # Atmospheric layer indices
435
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
436
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
437
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
438
+
439
+ # Delta-M scaling
440
+ if np.any(scale_tau != np.ones(NLayers)):
441
+ tau_dist_from_top = tau_arr[l] - tau
442
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
443
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
444
+ else:
445
+ scaled_tau = tau
446
+
447
+ if there_is_iso_source:
448
+ l_uniq, l_inv = np.unique(l, return_inverse=True)
449
+ _mathscr_v_contribution = _mathscr_v(
450
+ tau, l_inv, Nscoeffs,
451
+ s_poly_coeffs[l_uniq],
452
+ G_collect_0[l_uniq, :N, :],
453
+ K_collect_0[l_uniq],
454
+ G_inv_collect_0[l_uniq],
455
+ mu_arr,
456
+ is_antiderivative_wrt_tau,
457
+ autograd_compatible
458
+ )
459
+ else:
460
+ _mathscr_v_contribution = 0
461
+
462
+ if there_is_beam_source and is_antiderivative_wrt_tau:
463
+ direct_beam_contribution = (
464
+ (B_pos / (-scale_tau / mu0)[None, :])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
465
+ )
466
+ elif there_is_beam_source and not is_antiderivative_wrt_tau:
467
+ direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau / mu0)[None, :]
468
+ else:
469
+ direct_beam_contribution = 0
470
+
471
+ exponent = np.concatenate(
472
+ [
473
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
474
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
475
+ ],
476
+ axis=-1,
477
+ )
478
+
479
+ if is_antiderivative_wrt_tau:
480
+ u0_pos = (
481
+ np.einsum(
482
+ "tij, tj -> it",
483
+ GC_pos[l, :, :],
484
+ np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
485
+ )
486
+ + direct_beam_contribution
487
+ + _mathscr_v_contribution
488
+ )
489
+ else:
490
+ u0_pos = (
491
+ np.einsum("tij, tj -> it", GC_pos[l, :, :], np.exp(exponent))
492
+ + direct_beam_contribution
493
+ + _mathscr_v_contribution
494
+ )
495
+ flux = 2 * pi * (mu_arr_pos * W) @ u0_pos
496
+
497
+ if return_tau_arr:
498
+ return I0_orig * np.squeeze(flux)[()], tau_arr
499
+ else:
500
+ return I0_orig * np.squeeze(flux)[()]
501
+
502
+
503
+ def flux_down(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
504
+ """
505
+ (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
506
+ Returns a tuple of the diffuse and direct flux magnitudes respectively where each entry is of the
507
+ same type and size as `tau`.
508
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
509
+ to switch to an antiderivative of the function with respect to `tau`.
510
+ """
511
+ tau = np.atleast_1d(tau)
512
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
513
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
514
+
515
+ # Atmospheric layer indices
516
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
517
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
518
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
519
+
520
+ # Delta-M scaling
521
+ if np.any(scale_tau != np.ones(NLayers)):
522
+ tau_dist_from_top = tau_arr[l] - tau
523
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
524
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
525
+ else:
526
+ scaled_tau = tau
527
+
528
+ if there_is_iso_source:
529
+ l_uniq, l_inv = np.unique(l, return_inverse=True)
530
+ _mathscr_v_contribution = _mathscr_v(
531
+ tau, l_inv, Nscoeffs,
532
+ s_poly_coeffs[l_uniq],
533
+ G_collect_0[l_uniq, N:, :],
534
+ K_collect_0[l_uniq],
535
+ G_inv_collect_0[l_uniq],
536
+ mu_arr,
537
+ is_antiderivative_wrt_tau,
538
+ autograd_compatible
539
+ )
540
+ else:
541
+ _mathscr_v_contribution = 0
542
+
543
+ if there_is_beam_source and is_antiderivative_wrt_tau:
544
+ direct_beam_contribution = (B_neg / (-scale_tau / mu0)[None, l])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
545
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0) * -mu0
546
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau / mu0)[None, l]
547
+ elif there_is_beam_source and not is_antiderivative_wrt_tau:
548
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau / mu0)[None, :]
549
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0)
550
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
551
+ else:
552
+ direct_beam_contribution = 0
553
+ direct_beam = 0
554
+ direct_beam_scaled = 0
555
+
556
+ exponent = np.concatenate(
557
+ [
558
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
559
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
560
+ ],
561
+ axis=-1,
562
+ )
563
+
564
+ if is_antiderivative_wrt_tau:
565
+ u0_neg = (
566
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :])
567
+ + direct_beam_contribution
568
+ + _mathscr_v_contribution
569
+ )
570
+ else:
571
+ u0_neg = (
572
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent))
573
+ + direct_beam_contribution
574
+ + _mathscr_v_contribution
575
+ )
576
+ diffuse_flux = 2 * pi * (mu_arr_pos * W) @ u0_neg + direct_beam_scaled - direct_beam
577
+
578
+ if return_tau_arr:
579
+ return (
580
+ I0_orig * np.squeeze(diffuse_flux)[()],
581
+ I0_orig * I0 * np.squeeze(direct_beam)[()],
582
+ tau_arr
583
+ )
584
+ else:
585
+ return (
586
+ I0_orig * np.squeeze(diffuse_flux)[()],
587
+ I0_orig * I0 * np.squeeze(direct_beam)[()],
588
+ )
589
+ # --------------------------------------------------------------------------------------------------------------------------
590
+
591
+ if only_flux:
592
+ return flux_up, flux_down, u0, G_collect_0, K_collect_0 # TODO: Remove the temporary G_collect_0, K_collect_0
593
+ else:
594
+ return flux_up, flux_down, u0, u