PythonicDISORT 1.7__py3-none-any.whl → 1.9__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,703 @@
1
+ from PythonicDISORT import subroutines
2
+
3
+ import numpy as np
4
+ from functools import lru_cache
5
+ from numpy.polynomial.legendre import legval
6
+
7
+
8
+ def _NT_corrections(
9
+ u_star, # Delta-M scaled, uncorrected intensity function
10
+ NT_cor, # Perform NT corrections at the quadrature nodes?
11
+ tau_arr, # Lower boundary of layers
12
+ thickness_arr, # Thickness of each layer
13
+ omega_arr, # Single-scattering albedos
14
+ f_arr, # Fractional scattering into peak for each layer
15
+ Leg_coeffs_all, # All available unweighted phase function Legendre coefficients
16
+ weighted_Leg_coeffs_all, # All available weighted phase function Legendre coefficients
17
+ NLeg, NLeg_all, # Number of phase function Legendre coefficients 1) used by the solver; 2) available
18
+ NLayers, # Number of layers
19
+ is_atmos_multilayered, # Is the atmosphere multilayered?
20
+ mu_arr_pos, M_inv, # 1) Upper hemisphere quadrature nodes (cosine polar angles); 2) 1 / `mu_arr_pos`
21
+ scale_tau, # Delta-scale factor for tau
22
+ scaled_tau_arr_with_0, # Delta-scaled lower boundary of layers with 0 inserted in front
23
+ scaled_thickness_arr, # Delta-scaled thickness of each layer
24
+ scaled_omega_arr, # Delta-scaled single-scattering albedos
25
+ mu0, phi0, I0_div_4pi, # Direct beam parameters
26
+ rescale_factor, # Rescale factor of the sources
27
+ there_is_beam_source, # Is there a beam source?
28
+ autograd_compatible, # Should the output functions be compatible with autograd?
29
+ ):
30
+ """Performs the Nakajima-Tanaka (NT) corrections on the intensity, but not the flux, if `NT_cor`
31
+ and if the corrections are applicable. Returns the intensity function `u`, which is otherwise `u_star`.
32
+ Either way, `u` carries the `_NT_data` attribute through which `subroutines.interpolate`
33
+ can apply or remove the corrections after the fact (see its `NT_cor` argument).
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 to maximize precomputation in `pydisort`.
37
+ See the Jupyter Notebook, especially section 3.7.2, for documentation, explanation and derivation.
38
+
39
+ Arguments of _NT_corrections
40
+ | Variable | Type / Shape |
41
+ | ------------------------------ | ------------------------ |
42
+ | `u_star` | function |
43
+ | `NT_cor` | boolean |
44
+ | `tau_arr` | `NLayers` |
45
+ | `thickness_arr` | `NLayers` |
46
+ | `omega_arr` | `NLayers` |
47
+ | `f_arr` | `NLayers` |
48
+ | `Leg_coeffs_all` | `NLayers x NLeg_all` |
49
+ | `weighted_Leg_coeffs_all` | `NLayers x NLeg_all` |
50
+ | `NLeg` | scalar |
51
+ | `NLeg_all` | scalar |
52
+ | `NLayers` | scalar |
53
+ | `is_atmos_multilayered` | boolean |
54
+ | `mu_arr_pos` | `NQuad/2` |
55
+ | `M_inv` | `NQuad/2` |
56
+ | `scale_tau` | `NLayers` |
57
+ | `scaled_tau_arr_with_0` | `NLayers + 1` |
58
+ | `scaled_thickness_arr` | `NLayers` |
59
+ | `scaled_omega_arr` | `NLayers` |
60
+ | `mu0` | scalar |
61
+ | `phi0` | scalar |
62
+ | `I0_div_4pi` | scalar |
63
+ | `rescale_factor` | scalar |
64
+ | `there_is_beam_source` | boolean |
65
+ | `autograd_compatible` | boolean |
66
+
67
+ Notable internal variables of _NT_corrections
68
+ | Variable | Type / Shape |
69
+ | ---------------------------- | -------------------------- |
70
+ | `Leg_coeffs_diff` | `NLayers x NLeg_all` |
71
+ | `cumsum1` | `NLayers` |
72
+ | `omega_avg` | `NLayers` |
73
+ | `cumsum2` | `NLayers` |
74
+ | `f_avg` | `NLayers` |
75
+ | `omega_f_avg` | `NLayers + 1` |
76
+ | `Leg_coeffs_residue` | `NLayers x NLeg_all` |
77
+ | `Leg_coeffs_residue_avg` | `(NLayers + 1) x NLeg_all` |
78
+ """
79
+ if autograd_compatible:
80
+ import autograd.numpy as np
81
+ else:
82
+ import numpy as np
83
+
84
+
85
+ # NT corrections require a beam source, delta-M scaling (`f_arr > 0`), scattering
86
+ # (`omega_arr > 0`) and a truncated phase function (`NLeg < NLeg_all`)
87
+ NT_corrections_possible = there_is_beam_source and np.any(f_arr > 0) and NLeg < NLeg_all and np.any(omega_arr > 0)
88
+
89
+ if NT_corrections_possible:
90
+
91
+ ############################################################ Setup ############################################################
92
+
93
+ # Statics: quantities used by the NT corrections which are independent of the evaluation points.
94
+ # Their computation is deferred, and done at most once, so that `NT_cor=False` runs only pay for
95
+ # them if corrections are later requested through `subroutines.interpolate`.
96
+ @lru_cache(maxsize=None)
97
+ def _compute_NT_statics():
98
+
99
+ statics = {}
100
+
101
+ Leg_coeffs_diff = weighted_Leg_coeffs_all / (1 - f_arr)[:, None]
102
+ Leg_coeffs_diff[:, :NLeg] = (
103
+ (2 * np.arange(NLeg) + 1)[None, :] * (f_arr / (1 - f_arr))[:, None]
104
+ )
105
+
106
+ statics["Leg_coeffs_diff_T"] = np.ascontiguousarray(Leg_coeffs_diff.T)
107
+ statics["exp_scaled_tau_arr_with_0"] = np.exp(-scaled_tau_arr_with_0 / mu0)
108
+
109
+ # IMS corrections
110
+ # --------------------------------------------------------------------------------------------------------------------------
111
+ weights = omega_arr * thickness_arr
112
+ cumsum1 = np.cumsum(weights) # (NLayers,)
113
+ cumsum2 = np.cumsum(f_arr * weights) # (NLayers,)
114
+ # Guarded divisions: a zero cumulative sum means no scattering above
115
+ # the boundary, where `IMS_scalar` and `IMS_Leg_coeffs` correctly vanish
116
+ omega_avg = cumsum1 / tau_arr
117
+ f_avg = np.where(cumsum1 > 0, cumsum2 / np.where(cumsum1 == 0, 1, cumsum1), 0)
118
+ omega_f_avg = omega_avg * f_avg # (NLayers,)
119
+ Leg_coeffs_residue = Leg_coeffs_all.copy()
120
+ Leg_coeffs_residue[:, :NLeg] = f_arr[:, None]
121
+ Leg_coeffs_residue_avg = np.where(
122
+ (cumsum2 > 0)[:, None],
123
+ np.cumsum(Leg_coeffs_residue * weights[:, None], axis=0)
124
+ / np.where(cumsum2 == 0, 1, cumsum2)[:, None],
125
+ 0,
126
+ ) # (NLayers, NLeg_all)
127
+ omega_f_avg = np.concatenate([omega_f_avg[:1], omega_f_avg])
128
+ Leg_coeffs_residue_avg = np.concatenate(
129
+ [Leg_coeffs_residue_avg[:1], Leg_coeffs_residue_avg], axis=0
130
+ )
131
+
132
+ statics["scaled_mu0"] = mu0 / (1 - omega_f_avg) # (NLayers + 1,)
133
+ statics["IMS_scalar"] = (
134
+ I0_div_4pi * omega_f_avg**2 / (1 - omega_f_avg)
135
+ ) # (NLayers + 1,)
136
+ statics["IMS_Leg_coeffs"] = (2 * np.arange(NLeg_all) + 1) * (
137
+ 2 * Leg_coeffs_residue_avg - Leg_coeffs_residue_avg**2
138
+ ) # (NLayers + 1, NLeg_all)
139
+ statics["tau_arr_with_0"] = np.concatenate([np.zeros(1), tau_arr])
140
+ # --------------------------------------------------------------------------------------------------------------------------
141
+
142
+ # TMS corrections (see `_TMS_mu_statics` function below)
143
+ # --------------------------------------------------------------------------------------------------------------------------
144
+ if is_atmos_multilayered:
145
+ statics["TMS_statics_quad_pos"] = _TMS_mu_statics(
146
+ M_inv, True, scaled_thickness_arr, mu0, statics["exp_scaled_tau_arr_with_0"]
147
+ )
148
+ statics["TMS_statics_quad_neg"] = _TMS_mu_statics(
149
+ M_inv, False, scaled_thickness_arr, mu0, statics["exp_scaled_tau_arr_with_0"]
150
+ )
151
+ else:
152
+ statics["TMS_statics_quad_pos"] = None
153
+ statics["TMS_statics_quad_neg"] = None
154
+ return statics
155
+
156
+ ###############################################################################################################################
157
+ ##################################################### Apply NT Corrections ####################################################
158
+
159
+ def _NT_corrections_by_hemisphere(
160
+ mu_pos, abs_mu_neg, tau, phi, is_antiderivative_wrt_tau, TMS_statics_pos=None, TMS_statics_neg=None
161
+ ):
162
+ """NT (TMS + IMS) corrections at the upward `mu_pos` and the downward `-abs_mu_neg`, in that order.
163
+ Returns an (Npos + Nneg, Ntau, Nphi) array. The optional `_TMS_mu_statics` of the two hemispheres
164
+ are only passed for the quadrature nodes, where they are precomputed.
165
+ """
166
+ if np.any(tau < 0) or np.any(tau > tau_arr[-1]):
167
+ raise ValueError("tau input outside the tau range specified for the atmosphere (check `tau_arr`).")
168
+ statics = _compute_NT_statics()
169
+ Nmu_pos = len(mu_pos)
170
+ Nmu = Nmu_pos + len(abs_mu_neg)
171
+
172
+ # Atmospheric layer indices: smallest l with tau <= tau_arr[l]
173
+ l = np.searchsorted(tau_arr, tau, side="left")
174
+
175
+ # Delta-M scaling
176
+ if np.any(scale_tau != 1):
177
+ scaled_tau = scaled_tau_arr_with_0[l + 1] - (tau_arr[l] - tau) * scale_tau[l]
178
+ else:
179
+ scaled_tau = tau
180
+
181
+ # The scattering angle cosines are shared by the TMS and IMS corrections
182
+ nu = np.reshape(
183
+ subroutines.calculate_nu(np.concatenate([mu_pos, -abs_mu_neg]), phi, -mu0, phi0),
184
+ (Nmu, len(phi)),
185
+ )
186
+
187
+ # TMS correction for the intensity (see section 3.7.2)
188
+ NT_corrections = _TMS_correction(
189
+ mu_pos, abs_mu_neg, nu,
190
+ l, scaled_tau,
191
+ is_antiderivative_wrt_tau,
192
+ statics["Leg_coeffs_diff_T"],
193
+ scaled_omega_arr, I0_div_4pi, mu0,
194
+ scale_tau, scaled_thickness_arr,
195
+ scaled_tau_arr_with_0, statics["exp_scaled_tau_arr_with_0"],
196
+ NLayers, is_atmos_multilayered,
197
+ autograd_compatible,
198
+ TMS_statics_pos,
199
+ TMS_statics_neg,
200
+ )
201
+
202
+ # IMS correction for the intensity (see section 3.7.2); it applies only to downward directions
203
+ if Nmu_pos < Nmu:
204
+ IMS_corrections = _IMS_correction(
205
+ abs_mu_neg, nu[Nmu_pos:], tau, l,
206
+ is_antiderivative_wrt_tau,
207
+ statics["scaled_mu0"], statics["IMS_scalar"], statics["IMS_Leg_coeffs"],
208
+ statics["tau_arr_with_0"],
209
+ autograd_compatible,
210
+ )
211
+ if autograd_compatible:
212
+ NT_corrections = NT_corrections + np.concatenate(
213
+ [np.zeros((Nmu_pos, len(tau), len(phi))), IMS_corrections], axis=0
214
+ )
215
+ else:
216
+ NT_corrections[Nmu_pos:, :, :] += IMS_corrections
217
+
218
+ return NT_corrections
219
+
220
+ def _NT_corrections_at_mu(mu, tau, phi, is_antiderivative_wrt_tau=False):
221
+ """NT (TMS + IMS) corrections to the delta-M scaled, uncorrected intensity `u_star`,
222
+ evaluated at arbitrary cosine polar angles `mu`. Returns an (Nmu, Ntau, Nphi) array.
223
+ This function is private but is exposed to `subroutines.interpolate`
224
+ through the `_NT_data` attribute of the intensity function `u`.
225
+ """
226
+ mu = np.atleast_1d(mu)
227
+ tau = np.atleast_1d(tau)
228
+ phi = np.atleast_1d(phi)
229
+
230
+ if not np.all(np.abs(mu) <= 1):
231
+ raise ValueError("mu values must be between -1 and 1.")
232
+ if np.any(np.abs(mu) < 1e-8):
233
+ raise ValueError(
234
+ "NT corrections at evaluation points require `mu` values bounded away from 0 (|mu| >= 1e-8)."
235
+ )
236
+
237
+ mask_pos = mu > 0
238
+ mask_neg = ~mask_pos
239
+ mu_pos = mu[mask_pos]
240
+ NT_corrections = _NT_corrections_by_hemisphere(
241
+ mu_pos, -mu[mask_neg], tau, phi, is_antiderivative_wrt_tau
242
+ )
243
+
244
+ # Return to the original `mu` ordering
245
+ Nmu_pos = len(mu_pos)
246
+ NT_corrections_ordered = np.empty(np.shape(NT_corrections))
247
+ NT_corrections_ordered[mask_pos] = NT_corrections[:Nmu_pos]
248
+ NT_corrections_ordered[mask_neg] = NT_corrections[Nmu_pos:]
249
+ return rescale_factor * NT_corrections_ordered
250
+
251
+ ###############################################################################################################################
252
+
253
+ else:
254
+ _NT_corrections_at_mu = None
255
+
256
+ if NT_cor and NT_corrections_possible:
257
+
258
+ # NT corrections are requested and possible, so compute the statics
259
+ NT_statics = _compute_NT_statics()
260
+
261
+ # The corrected intensity: `u_star` plus the TMS and IMS corrections evaluated at the quadrature nodes
262
+ # --------------------------------------------------------------------------------------------------------------------------
263
+ def u_corrected(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False):
264
+ tau = np.atleast_1d(tau)
265
+ phi = np.atleast_1d(phi)
266
+ NT_corrections = rescale_factor * np.squeeze(
267
+ _NT_corrections_by_hemisphere(
268
+ mu_arr_pos, mu_arr_pos, tau, phi, is_antiderivative_wrt_tau,
269
+ NT_statics["TMS_statics_quad_pos"], NT_statics["TMS_statics_quad_neg"],
270
+ )
271
+ )
272
+
273
+ # The corrected intensity
274
+ if return_Fourier_error:
275
+ u_star_val, Fourier_error = u_star(tau, phi, is_antiderivative_wrt_tau, True)
276
+ return u_star_val + NT_corrections, Fourier_error
277
+ else:
278
+ return u_star(tau, phi, is_antiderivative_wrt_tau) + NT_corrections
279
+
280
+ # `u_corrected` wraps `u_star`, so it needs its own copies of the attributes
281
+ # that `_assemble_intensity_and_fluxes` attaches to `u_star`
282
+ u_corrected._mu_arr_pos = mu_arr_pos
283
+ u_corrected.tau_arr = tau_arr
284
+
285
+ u_out = u_corrected
286
+ else:
287
+ u_out = u_star
288
+
289
+ # Attach the information required by `subroutines.interpolate` to apply or remove NT corrections
290
+ # (see its `NT_cor` argument and GitHub issue #47). This changes neither the API nor the behavior
291
+ # of `pydisort` itself.
292
+ NT_cor_applied = u_out is not u_star
293
+ u_out._NT_data = {
294
+ "NT_cor_applied": NT_cor_applied,
295
+ "u_star": u_star if NT_cor_applied else None,
296
+ "corrections_available": NT_corrections_possible,
297
+ "corrections_at_mu": _NT_corrections_at_mu,
298
+ }
299
+
300
+ return u_out
301
+
302
+
303
+ ############################################### NT-correction helper functions ###############################################
304
+
305
+ def _TMS_mu_statics(M_inv_h, positive_hemisphere, scaled_thickness_arr, mu0, exp_scaled_tau_arr_with_0):
306
+ """Precomputes, for one hemisphere of H `mu` values, the per-layer factors of the contributions
307
+ from other layers to the TMS correction (refer to section 3.7.2 of the Comprehensive Documentation),
308
+ which `_TMS_correction` accumulates across layers. The factors are independent of `tau` and `phi`,
309
+ so `_NT_corrections` precomputes them at the quadrature nodes.
310
+ `M_inv_h` are the reciprocals of the absolute `mu` values of the hemisphere;
311
+ `positive_hemisphere` selects between the upward (True) and downward (False) hemispheres.
312
+
313
+ Arguments of _TMS_mu_statics
314
+ | Variable | Type / Shape |
315
+ | ---------------------------- | -------------------- |
316
+ | `M_inv_h` | `H` |
317
+ | `positive_hemisphere` | boolean |
318
+ | `scaled_thickness_arr` | `NLayers` |
319
+ | `mu0` | scalar |
320
+ | `exp_scaled_tau_arr_with_0` | `NLayers + 1` |
321
+ """
322
+ # Transmittance of each layer along each `mu`
323
+ delta = scaled_thickness_arr[None, :] * M_inv_h[:, None] # (H, NLayers)
324
+ decay = np.exp(-delta) # (H, NLayers)
325
+
326
+ # The square-bracketed difference of exponentials of each layer's contribution, referenced to the boundary
327
+ # of that layer which faces the evaluation points: its top for upward `mu`, its bottom for downward `mu`
328
+ if positive_hemisphere:
329
+ delta_pos = delta + (scaled_thickness_arr / mu0)[None, :] # (H, NLayers)
330
+ fac_pos = -np.expm1(-delta_pos) # (H, NLayers)
331
+ coef = fac_pos * exp_scaled_tau_arr_with_0[:-1][None, :] # (H, NLayers)
332
+ else:
333
+ # The beam factor of `mathscr_B` is folded in (see `_TMS_correction`). Near its removable singularity
334
+ # (|w| < 1) use phi1(-w); elsewhere factor out the larger exponential so that `expm1` sees non-positive arguments
335
+ w = delta - (scaled_thickness_arr / mu0)[None, :] # (H, NLayers)
336
+ y = M_inv_h - 1 / mu0 # (H,)
337
+ near_singularity = np.abs(w) < 1.0
338
+ y_safe = np.where(y == 0.0, 1.0, y)[:, None]
339
+ w_safe = np.where(w == 0.0, 1.0, w)
340
+ phi1_neg_w = np.where(w == 0.0, 1.0, -np.expm1(-w_safe) / w_safe) # phi1(-w)
341
+ em1 = np.expm1(-np.abs(w)) # (H, NLayers)
342
+ exp_x1 = exp_scaled_tau_arr_with_0[1:][None, :] # (1, NLayers)
343
+ exp_x0 = decay * exp_scaled_tau_arr_with_0[:-1][None, :] # (H, NLayers)
344
+ coef = M_inv_h[:, None] * np.where(
345
+ near_singularity,
346
+ exp_x1 * scaled_thickness_arr[None, :] * phi1_neg_w,
347
+ np.where(w >= 0.0, -em1 * exp_x1, em1 * exp_x0) / y_safe,
348
+ ) # (H, NLayers)
349
+
350
+ return decay, coef
351
+
352
+
353
+ def _TMS_correction(
354
+ mu_pos, abs_mu_neg, # Positive-hemisphere `mu` values; absolute values of negative-hemisphere `mu`
355
+ nu, # Scattering angle cosines (positive-hemisphere rows first)
356
+ l, scaled_tau, # Layer indices and delta-scaled optical depths of the evaluation points
357
+ is_antiderivative_wrt_tau, # Switch to an antiderivative of the function?
358
+ Leg_coeffs_diff_T, # Transposed weighted Legendre coefficients of the phase function difference in `mathscr_B`
359
+ scaled_omega_arr, # Delta-scaled single-scattering albedos
360
+ I0_div_4pi, # (Rescaled) intensity of the direct beam divided by 4 pi
361
+ mu0, # Cosine of polar angle of the incident beam
362
+ scale_tau, # Delta-scale factor for tau
363
+ scaled_thickness_arr, # Delta-scaled thickness of each layer
364
+ scaled_tau_arr_with_0, # Delta-scaled lower boundary of layers with 0 inserted in front
365
+ exp_scaled_tau_arr_with_0, # exp(-scaled_tau_arr_with_0 / mu0)
366
+ NLayers, # Number of layers
367
+ is_atmos_multilayered, # Is the atmosphere multilayered?
368
+ autograd_compatible, # Should the output functions be compatible with autograd?
369
+ statics_pos=None, # Optional precomputed `_TMS_mu_statics` for `mu_pos` (multi-layer only)
370
+ statics_neg=None, # Optional precomputed `_TMS_mu_statics` for `abs_mu_neg` (multi-layer only)
371
+ ):
372
+ """TMS correction to the intensity at arbitrary `mu` evaluation points.
373
+ Refer to section 3.7.2 of the Comprehensive Documentation.
374
+ Returns an ndarray of shape (Npos + Nneg, Ntau, Nphi) with the positive-hemisphere rows first.
375
+ This function is private and should be called only through the closures that `_NT_corrections` creates.
376
+
377
+ Arguments of _TMS_correction
378
+ | Variable | Type / Shape |
379
+ | ---------------------------- | ---------------------------- |
380
+ | `mu_pos` | `Npos` |
381
+ | `abs_mu_neg` | `Nneg` |
382
+ | `nu` | `(Npos + Nneg) x Nphi` |
383
+ | `l` | `Ntau` |
384
+ | `scaled_tau` | `Ntau` |
385
+ | `Leg_coeffs_diff_T` | `NLeg_all x NLayers` |
386
+ | `scaled_omega_arr` | `NLayers` |
387
+ | `scale_tau` | `NLayers` |
388
+ | `scaled_thickness_arr` | `NLayers` |
389
+ | `scaled_tau_arr_with_0` | `NLayers + 1` |
390
+ | `exp_scaled_tau_arr_with_0` | `NLayers + 1` |
391
+ | `statics_pos` | 2-tuple of `Npos x NLayers` |
392
+ | `statics_neg` | 2-tuple of `Nneg x NLayers` |
393
+ """
394
+ if autograd_compatible:
395
+ import autograd.numpy as np
396
+ else:
397
+ import numpy as np
398
+
399
+ Nmu_pos = len(mu_pos)
400
+ Nmu_neg = len(abs_mu_neg)
401
+ Nphi = np.shape(nu)[-1]
402
+
403
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
404
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
405
+
406
+ # mathscr_B
407
+ # --------------------------------------------------------------------------------------------------------------------------
408
+ # `legval` is linear in its coefficients, so a single call gives the phase function difference of every layer
409
+ p_diff = np.moveaxis(legval(nu, Leg_coeffs_diff_T, tensor=True), 0, 1) # (Npos + Nneg, NLayers, Nphi)
410
+ # For downward `mu` the beam factor mu0 / (mu0 - mu) = 1 / (mu * y_neg) is singular at mu = mu0 and is
411
+ # instead folded into the square-bracketed differences of exponentials (below and in `_TMS_mu_statics`)
412
+ mathscr_B_fac = np.concatenate([mu0 / (mu0 + mu_pos), np.ones(Nmu_neg)])
413
+ mathscr_B = (
414
+ (scaled_omega_arr * I0_div_4pi)[None, :, None]
415
+ * mathscr_B_fac[:, None, None]
416
+ * p_diff
417
+ )
418
+ Bpos = mathscr_B[:Nmu_pos, :, :] # (Npos, NLayers, Nphi)
419
+ Bneg = mathscr_B[Nmu_pos:, :, :] # (Nneg, NLayers, Nphi)
420
+ # --------------------------------------------------------------------------------------------------------------------------
421
+
422
+ exp_neg_scaled_tau_div_mu0 = np.exp(-scaled_tau / mu0) # (Ntau,)
423
+
424
+ # Downward `mu`: near the removable singularity (|w| < 1) the brackets are evaluated through phi1(-w),
425
+ # which is exact at w = 0; elsewhere the direct form is used
426
+ y_neg = 1 / abs_mu_neg - 1 / mu0 # (Nneg,)
427
+ Delta = scaled_tau - scaled_tau_arr_lm1 # (Ntau,)
428
+ w = Delta[None, :] * y_neg[:, None] # (Nneg, Ntau)
429
+ near_singularity = np.abs(w) < 1.0
430
+ y_safe = np.where(y_neg == 0.0, 1.0, y_neg)[:, None]
431
+ w_safe = np.where(w == 0.0, 1.0, w)
432
+ phi1_neg_w = np.where(w == 0.0, 1.0, -np.expm1(-w_safe) / w_safe) # phi1(-w)
433
+ E1 = exp_neg_scaled_tau_div_mu0[None, :]
434
+ E2 = np.exp(
435
+ (scaled_tau_arr_lm1 - scaled_tau)[None, :] / abs_mu_neg[:, None]
436
+ - scaled_tau_arr_lm1[None, :] / mu0
437
+ )
438
+
439
+ # The square-bracketed differences of exponentials for the layer containing each `tau`
440
+ if is_antiderivative_wrt_tau:
441
+ # Antiderivatives with respect to the unscaled tau, hence the divisions by d tau* / d tau = `scale_tau`
442
+ scale_tau_l = scale_tau[l] # (Ntau,)
443
+ common_term = (exp_neg_scaled_tau_div_mu0 / (-scale_tau_l / mu0))[None, :]
444
+
445
+ TMS_correction_pos = common_term - np.exp(
446
+ (scaled_tau - scaled_tau_arr_l)[None, :] / mu_pos[:, None]
447
+ - scaled_tau_arr_l[None, :] / mu0
448
+ ) * (mu_pos[:, None] / scale_tau_l[None, :])
449
+ TMS_correction_neg = np.where(
450
+ near_singularity,
451
+ -(E1 / scale_tau_l[None, :]) * (Delta[None, :] * phi1_neg_w + mu0),
452
+ (abs_mu_neg[:, None] * E2 - mu0 * E1)
453
+ / (scale_tau_l[None, :] * abs_mu_neg[:, None] * y_safe),
454
+ )
455
+ else:
456
+ TMS_correction_pos = exp_neg_scaled_tau_div_mu0[None, :] - np.exp(
457
+ (scaled_tau - scaled_tau_arr_l)[None, :] / mu_pos[:, None]
458
+ - scaled_tau_arr_l[None, :] / mu0
459
+ )
460
+ TMS_correction_neg = np.where(
461
+ near_singularity,
462
+ (Delta[None, :] / abs_mu_neg[:, None]) * phi1_neg_w * E1,
463
+ (E1 - E2) / (abs_mu_neg[:, None] * y_safe),
464
+ )
465
+
466
+ # Contribution from the layer containing each `tau`
467
+ sol_pos = Bpos[:, l, :] * TMS_correction_pos[:, :, None] # (Npos, Ntau, Nphi)
468
+ sol_neg = Bneg[:, l, :] * TMS_correction_neg[:, :, None] # (Nneg, Ntau, Nphi)
469
+
470
+ if not is_atmos_multilayered:
471
+ return np.concatenate((sol_pos, sol_neg), axis=0)
472
+
473
+ # Contributions from the other layers, accumulated by a single scan over the layers for each hemisphere.
474
+ # The antiderivative factor `mu / scale_tau` belongs to the layer containing `tau`, not to the contributing
475
+ # layer, and is therefore applied after the scan.
476
+ # --------------------------------------------------------------------------------------------------------------------------
477
+ any_pos = (l.min() < NLayers - 1) # Some `tau` has layers below
478
+ any_neg = (l.max() > 0) # Some `tau` has layers above
479
+
480
+ # Upward `mu`: contributions from the layers below
481
+ if any_pos and Nmu_pos > 0:
482
+ M_inv_pos = 1 / mu_pos
483
+ if statics_pos is None:
484
+ decay_pos, coef_pos = _TMS_mu_statics(
485
+ M_inv_pos, True, scaled_thickness_arr, mu0, exp_scaled_tau_arr_with_0
486
+ )
487
+ else:
488
+ decay_pos, coef_pos = statics_pos
489
+
490
+ l_min = int(l.min()) # Only layers >= l_min are needed; layer r is stored at index r - l_min
491
+ Nacc = NLayers - l_min
492
+
493
+ # Acc_r is the contribution of the layers below layer r, referenced to the bottom of layer r:
494
+ # Acc_{r-1} = coef_pos[:, r] * Bpos[:, r, :] + decay_pos[:, r] * Acc_r
495
+ if autograd_compatible:
496
+ acc0 = np.zeros((Nmu_pos, Nphi))
497
+ acc = acc0
498
+ cols_rev = [acc0] # Acc_{NLayers-1} = 0
499
+ for r in range(NLayers - 1, l_min, -1):
500
+ term = coef_pos[:, r][:, None] * Bpos[:, r, :] # (Npos, Nphi)
501
+ acc = term + decay_pos[:, r][:, None] * acc
502
+ cols_rev.append(acc)
503
+ Acc_pos = np.stack(cols_rev[::-1], axis=1) # (Npos, Nacc, Nphi)
504
+ else:
505
+ Acc_pos = np.empty((Nmu_pos, Nacc, Nphi))
506
+ Acc_pos[:, Nacc - 1, :] = 0.0 # Acc_{NLayers-1}
507
+ for r in range(NLayers - 1, l_min, -1):
508
+ i_dest = r - 1 - l_min
509
+ i_source = r - l_min
510
+ np.multiply(coef_pos[:, r:r+1], Bpos[:, r, :],
511
+ out=Acc_pos[:, i_dest, :])
512
+ Acc_pos[:, i_dest, :] += decay_pos[:, r:r+1] * Acc_pos[:, i_source, :]
513
+
514
+ # Transmit from the bottom of the layer containing `tau` to `tau`
515
+ expfac_pos = np.exp(M_inv_pos[:, None] * (scaled_tau - scaled_tau_arr_l)[None, :]) # (Npos, Ntau)
516
+ contrib_pos = Acc_pos[:, l - l_min, :] * expfac_pos[:, :, None] # (Npos, Ntau, Nphi)
517
+
518
+ if is_antiderivative_wrt_tau:
519
+ contrib_pos = contrib_pos * (mu_pos[:, None] / scale_tau_l[None, :])[:, :, None]
520
+
521
+ sol_pos = sol_pos + contrib_pos
522
+
523
+ # Downward `mu`: contributions from the layers above
524
+ if any_neg and Nmu_neg > 0:
525
+ M_inv_neg = 1 / abs_mu_neg
526
+ if statics_neg is None:
527
+ decay_neg, coef_neg = _TMS_mu_statics(
528
+ M_inv_neg, False, scaled_thickness_arr, mu0, exp_scaled_tau_arr_with_0
529
+ )
530
+ else:
531
+ decay_neg, coef_neg = statics_neg
532
+
533
+ l_max = int(l.max()) # Only layers <= l_max are needed
534
+ Nacc = l_max + 1
535
+
536
+ # Acc_r is the contribution of the layers above layer r, referenced to the top of layer r:
537
+ # Acc_{r+1} = coef_neg[:, r] * Bneg[:, r, :] + decay_neg[:, r] * Acc_r
538
+ if autograd_compatible:
539
+ acc0 = np.zeros((Nmu_neg, Nphi))
540
+ acc = acc0
541
+ cols = [acc0] # Acc_0 = 0
542
+ for r in range(0, l_max):
543
+ term = coef_neg[:, r][:, None] * Bneg[:, r, :] # (Nneg, Nphi)
544
+ acc = term + decay_neg[:, r][:, None] * acc
545
+ cols.append(acc)
546
+ Acc_neg = np.stack(cols, axis=1) # (Nneg, Nacc, Nphi)
547
+ else:
548
+ Acc_neg = np.empty((Nmu_neg, Nacc, Nphi))
549
+ Acc_neg[:, 0, :] = 0.0 # Acc_0
550
+ for r in range(0, l_max):
551
+ np.multiply(coef_neg[:, r:r+1], Bneg[:, r, :],
552
+ out=Acc_neg[:, r + 1, :])
553
+ Acc_neg[:, r + 1, :] += decay_neg[:, r:r+1] * Acc_neg[:, r, :]
554
+
555
+ # Transmit from the top of the layer containing `tau` to `tau`
556
+ expfac_neg = np.exp(M_inv_neg[:, None] * (scaled_tau_arr_lm1 - scaled_tau)[None, :]) # (Nneg, Ntau)
557
+ contrib_neg = Acc_neg[:, l, :] * expfac_neg[:, :, None] # (Nneg, Ntau, Nphi)
558
+
559
+ if is_antiderivative_wrt_tau:
560
+ contrib_neg = -contrib_neg * (abs_mu_neg[:, None] / scale_tau_l[None, :])[:, :, None]
561
+
562
+ sol_neg = sol_neg + contrib_neg
563
+ # --------------------------------------------------------------------------------------------------------------------------
564
+
565
+ return np.concatenate((sol_pos, sol_neg), axis=0)
566
+
567
+
568
+ # 16 coefficients of the Taylor series of phi2(z) = (e^z - 1 - z)/z^2 = sum_k z^k / (k + 2)!
569
+ _PHI2_TAYLOR_COEFFS = 1.0 / np.cumprod(np.arange(2, 19))
570
+
571
+ def _IMS_correction(
572
+ abs_mu_neg, # Absolute values of negative-hemisphere `mu`
573
+ nu_neg, # Scattering angle cosines for the negative hemisphere
574
+ tau, # Optical depths of the evaluation points (NOT delta-scaled)
575
+ l, # Atmospheric layer indices of the evaluation points
576
+ is_antiderivative_wrt_tau, # Switch to an antiderivative of the function?
577
+ scaled_mu0, # mu0' at each layer boundary
578
+ IMS_scalar, # Scalar factor of the IMS correction at each layer boundary
579
+ IMS_Leg_coeffs, # Weighted Legendre coefficients of the square-bracketed series at each layer boundary
580
+ tau_arr_with_0, # Lower boundary of layers with 0 inserted in front
581
+ autograd_compatible, # Should the output functions be compatible with autograd?
582
+ ):
583
+ """IMS correction to the intensity at arbitrary downward `mu` evaluation points.
584
+ Refer to section 3.7.2 of the Comprehensive Documentation.
585
+ Returns an ndarray of shape (Nneg, Ntau, Nphi).
586
+ This function is private and should be called only through the closures that `_NT_corrections` creates.
587
+ The boundary statics are indexed k = 0, ..., NLayers where the k = 0 (top of atmosphere) set
588
+ equals the k = 1 set. Layers are 0-indexed, so layer `l` is layer l + 1 of section 3.7.2
589
+ and its correction interpolates between the boundary sets k = l and k = l + 1.
590
+
591
+ Arguments of _IMS_correction
592
+ | Variable | Type / Shape |
593
+ | ---------------------------- | ---------------------------- |
594
+ | `abs_mu_neg` | `Nneg` |
595
+ | `nu_neg` | `Nneg x Nphi` |
596
+ | `tau` | `Ntau` |
597
+ | `l` | `Ntau` |
598
+ | `scaled_mu0` | `NLayers + 1` |
599
+ | `IMS_scalar` | `NLayers + 1` |
600
+ | `IMS_Leg_coeffs` | `(NLayers + 1) x NLeg_all` |
601
+ | `tau_arr_with_0` | `NLayers + 1` |
602
+ """
603
+ if autograd_compatible:
604
+ import autograd.numpy as np
605
+ else:
606
+ import numpy as np
607
+
608
+ exp_tau_mu = np.exp(-tau[None, :] / abs_mu_neg[:, None]) # (Nneg, Ntau)
609
+
610
+ def chi_kernels(smu0, need_chichi_anti=True):
611
+ """chi or, on the antiderivative branch, chi_anti and optionally chichi_anti (the first and
612
+ second tau-antiderivatives of chi), each of shape (Nneg, Ntau), for the `scaled_mu0` value
613
+ `smu0` (Ntau,) of each evaluation point. Near the removable singularity at mu = smu0 (|z| < 1)
614
+ each is evaluated through phi2(z), and phi2 through its Taylor series."""
615
+ x = 1 / abs_mu_neg[:, None] - 1 / smu0[None, :] # (Nneg, Ntau)
616
+ z = -tau[None, :] * x
617
+ near_singularity = np.abs(z) < 1.0
618
+ x_safe = np.where(x == 0.0, 1.0, x)
619
+ exp_tau_smu0 = np.exp(-tau / smu0)[None, :]
620
+ phi2 = _PHI2_TAYLOR_COEFFS[-1]
621
+ for c in _PHI2_TAYLOR_COEFFS[-2::-1]: # Horner evaluation; written to be autograd-compatible
622
+ phi2 = phi2 * z + c
623
+
624
+ if is_antiderivative_wrt_tau:
625
+ chi_anti = np.where(
626
+ near_singularity,
627
+ -exp_tau_smu0
628
+ * (tau[None, :] ** 2 * phi2 + (smu0 * (smu0 + tau))[None, :])
629
+ / smu0[None, :],
630
+ (
631
+ (smu0[None, :] - x_safe * (smu0 * (smu0 + tau))[None, :]) * exp_tau_smu0
632
+ - abs_mu_neg[:, None] * exp_tau_mu
633
+ ) / (abs_mu_neg[:, None] * smu0[None, :] * x_safe**2),
634
+ )
635
+ if not need_chichi_anti:
636
+ return chi_anti, None
637
+ chichi_anti = np.where(
638
+ near_singularity,
639
+ exp_tau_smu0
640
+ * (
641
+ tau[None, :] ** 2 * phi2
642
+ + (2 * smu0 * tau + 3 * smu0**2)[None, :]
643
+ + x * (smu0**2 * (tau + 2 * smu0))[None, :]
644
+ )
645
+ / (1 + x * smu0[None, :]), # 1 + x * smu0 = smu0 / mu > 0
646
+ (
647
+ abs_mu_neg[:, None] ** 2 * exp_tau_mu
648
+ - (smu0**2)[None, :] * (1 - x * (tau + 2 * smu0)[None, :]) * exp_tau_smu0
649
+ ) / (abs_mu_neg[:, None] * smu0[None, :] * x_safe**2),
650
+ )
651
+ return chi_anti, chichi_anti
652
+ else:
653
+ chi = np.where(
654
+ near_singularity,
655
+ tau[None, :] ** 2 * exp_tau_smu0 * phi2 / (abs_mu_neg[:, None] * smu0[None, :]),
656
+ (
657
+ (tau[None, :] - 1 / x_safe) * exp_tau_smu0 + exp_tau_mu / x_safe
658
+ ) / (abs_mu_neg[:, None] * smu0[None, :] * x_safe),
659
+ )
660
+ return chi
661
+
662
+ if len(IMS_scalar) == 2:
663
+ # Single-layer atmosphere: the two boundary sets are identical so there is nothing to interpolate
664
+ smu0 = scaled_mu0[np.full(len(tau), 1)]
665
+ scalP = (IMS_scalar[1] * legval(nu_neg, IMS_Leg_coeffs[1]))[:, None, :]
666
+ if is_antiderivative_wrt_tau:
667
+ return scalP * chi_kernels(smu0, need_chichi_anti=False)[0][:, :, None]
668
+ else:
669
+ return scalP * chi_kernels(smu0)[:, :, None]
670
+
671
+ # Interpolate between the correction functions of the top ("lo", k = l) and bottom ("hi", k = l + 1)
672
+ # boundaries of the layer containing each `tau`
673
+ t0 = tau_arr_with_0[l] # (Ntau,)
674
+ thickness_l = tau_arr_with_0[l + 1] - t0 # (Ntau,)
675
+
676
+ # The square-bracketed Legendre series (DISORT's `PSPIKE`) of each boundary set that is needed
677
+ sets = np.unique(np.concatenate([l, l + 1]))
678
+ P_sets = legval(nu_neg, np.ascontiguousarray(IMS_Leg_coeffs[sets].T), tensor=True) # (Nsets, Nneg, Nphi)
679
+ scalP_lo = IMS_scalar[l][None, :, None] * np.moveaxis(
680
+ P_sets[np.searchsorted(sets, l)], 0, 1
681
+ ) # (Nneg, Ntau, Nphi)
682
+ scalP_hi = IMS_scalar[l + 1][None, :, None] * np.moveaxis(
683
+ P_sets[np.searchsorted(sets, l + 1)], 0, 1
684
+ ) # (Nneg, Ntau, Nphi)
685
+
686
+ if is_antiderivative_wrt_tau:
687
+ # Per-layer antiderivative of the interpolated correction (refer to section 3.7.2),
688
+ # where Y1 and Y2 are the first and second antiderivatives of the boundary correction functions
689
+ chi_anti_lo, chichi_anti_lo = chi_kernels(scaled_mu0[l])
690
+ chi_anti_hi, chichi_anti_hi = chi_kernels(scaled_mu0[l + 1])
691
+ Y1_lo = scalP_lo * chi_anti_lo[:, :, None]
692
+ Y1_hi = scalP_hi * chi_anti_hi[:, :, None]
693
+ Y2_diff = scalP_hi * chichi_anti_hi[:, :, None] - scalP_lo * chichi_anti_lo[:, :, None]
694
+ return Y1_lo + (
695
+ (tau - t0)[None, :, None] * (Y1_hi - Y1_lo) - Y2_diff
696
+ ) / thickness_l[None, :, None]
697
+ else:
698
+ w = (tau - t0) / thickness_l # (Ntau,)
699
+ y_lo = scalP_lo * chi_kernels(scaled_mu0[l])[:, :, None]
700
+ y_hi = scalP_hi * chi_kernels(scaled_mu0[l + 1])[:, :, None]
701
+ return y_lo + w[None, :, None] * (y_hi - y_lo)
702
+
703
+ ##############################################################################################################################