PythonicDISORT 1.8__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.
- PythonicDISORT/_NT_corrections.py +703 -0
- PythonicDISORT/_assemble_intensity_and_fluxes.py +49 -77
- PythonicDISORT/_solve_for_gen_and_part_sols.py +39 -60
- PythonicDISORT/pydisort.py +32 -722
- PythonicDISORT/subroutines.py +66 -95
- {pythonicdisort-1.8.dist-info → pythonicdisort-1.9.dist-info}/METADATA +10 -3
- pythonicdisort-1.9.dist-info/RECORD +12 -0
- pythonicdisort-1.8.dist-info/RECORD +0 -11
- {pythonicdisort-1.8.dist-info → pythonicdisort-1.9.dist-info}/LICENSE.md +0 -0
- {pythonicdisort-1.8.dist-info → pythonicdisort-1.9.dist-info}/WHEEL +0 -0
- {pythonicdisort-1.8.dist-info → pythonicdisort-1.9.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
##############################################################################################################################
|