PythonicDISORT 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,9 +28,9 @@ def _assemble_intensity_and_fluxes(
28
28
  NLayers_to_use_sparse, # Number of layers above or equal which to use sparse matrices
29
29
  _autograd_compatible, # Should the output functions be compatible with autograd?
30
30
  ):
31
- """In this function the solution functions: intensity `u`, upward flux `flux_up`, downward flux `flux_down`
32
- are assembled from the eigenpairs and the coefficients which have been solved from the boundary conditions.
33
- These solution functions are then returned.
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
34
  This function is wrapped by the `pydisort` function.
35
35
  It should be called through `pydisort` and never directly.
36
36
  It has many seemingly redundant arguments, which are described below,
@@ -161,7 +161,15 @@ def _assemble_intensity_and_fluxes(
161
161
 
162
162
  # Construct the intensity function (refer to Section 3.7 of the Comprehensive Documentation)
163
163
  # --------------------------------------------------------------------------------------------------------------------------
164
- def u(tau, phi, return_Fourier_error=False):
164
+ def u(tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
165
+ """
166
+ Intensity function with arguments `(tau, phi)` of types `(array or float, array or float)`.
167
+ Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
168
+ Pass `return_Fourier_error = True` (defaults to `False`) to return the
169
+ Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
170
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
171
+ to switch to an antiderivative of the function with respect to `tau`.
172
+ """
165
173
  tau = np.atleast_1d(tau)
166
174
  phi = np.atleast_1d(phi)
167
175
  assert np.all(tau >= 0)
@@ -179,7 +187,7 @@ def _assemble_intensity_and_fluxes(
179
187
  scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
180
188
  else:
181
189
  scaled_tau = tau
182
-
190
+
183
191
  exponent = np.concatenate(
184
192
  [
185
193
  K_collect[:, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[None, :, None],
@@ -187,14 +195,33 @@ def _assemble_intensity_and_fluxes(
187
195
  ],
188
196
  axis=-1,
189
197
  )
190
- if there_is_beam_source:
191
- um = np.einsum(
192
- "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
193
- ) + B_collect[:, l, :] * np.exp(-scaled_tau[None, :, None] / mu0)
198
+
199
+ if is_antiderivative_wrt_tau:
200
+ if there_is_beam_source:
201
+ um = np.einsum(
202
+ "mtij, mtj -> mti",
203
+ GC_collect[:, l, :, :],
204
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect[:, l, :]),
205
+ optimize=True,
206
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau[None, :, None] / mu0) / (
207
+ -scale_tau[None, l, None] / mu0
208
+ )
209
+ else:
210
+ um = np.einsum(
211
+ "mtij, mtj -> mti",
212
+ GC_collect[:, l, :, :],
213
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect[:, l, :]),
214
+ optimize=True,
215
+ )
194
216
  else:
195
- um = np.einsum(
196
- "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
197
- )
217
+ if there_is_beam_source:
218
+ um = np.einsum(
219
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
220
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau[None, :, None] / mu0)
221
+ else:
222
+ um = np.einsum(
223
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
224
+ )
198
225
 
199
226
  # Contribution from particular solution for isotropic internal sources
200
227
  if there_is_iso_source:
@@ -206,11 +233,15 @@ def _assemble_intensity_and_fluxes(
206
233
  K_collect_0,
207
234
  G_inv_collect_0,
208
235
  mu_arr,
209
- _autograd_compatible,
236
+ is_antiderivative_wrt_tau,
237
+ _autograd_compatible
210
238
  )
211
239
 
212
240
  if _autograd_compatible:
213
- um = um + np.concatenate((_mathscr_v_contribution.T, np.zeros((NFourier - 1, len(tau), NQuad))))
241
+ if NFourier == 1:
242
+ um = um + _mathscr_v_contribution.T
243
+ else:
244
+ um = um + np.concatenate((_mathscr_v_contribution.T, np.zeros((NFourier - 1, len(tau), NQuad))))
214
245
  else:
215
246
  um[0, :, :] += _mathscr_v_contribution.T
216
247
 
@@ -229,20 +260,43 @@ def _assemble_intensity_and_fluxes(
229
260
  ],
230
261
  axis=-1,
231
262
  )
232
- if there_is_beam_source:
233
- ulast = np.einsum(
234
- "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
235
- ) + B_collect[-1, l, :].T * np.exp(-scaled_tau[None, :] / mu0)
263
+ if is_antiderivative_wrt_tau:
264
+ if there_is_beam_source:
265
+ ulast = np.einsum(
266
+ "tij, tj -> it",
267
+ GC_collect[-1, l, :, :],
268
+ np.exp(exponent) / (scale_tau[l, None] * K_collect[-1, l, :]),
269
+ optimize=True,
270
+ ) + B_collect[-1, l, :].T * np.exp(-scaled_tau[None, :] / mu0) / (
271
+ -scale_tau[None, l] / mu0
272
+ )
273
+ else:
274
+ ulast = np.einsum(
275
+ "tij, tj -> it",
276
+ GC_collect[-1, l, :, :],
277
+ np.exp(exponent) / (scale_tau[l, None] * K_collect[-1, l, :]),
278
+ optimize=True,
279
+ )
236
280
  else:
237
- ulast = np.einsum(
238
- "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
239
- )
281
+ if there_is_beam_source:
282
+ ulast = np.einsum(
283
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
284
+ ) + B_collect[-1, l, :].T * np.exp(-scaled_tau[None, :] / mu0)
285
+ else:
286
+ ulast = np.einsum(
287
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
288
+ )
289
+
240
290
  intensities_abs = np.abs(intensities)
241
291
  Fourier_error = np.max(
242
- np.abs(ulast[:, :, None] * np.cos((NFourier - 1) * (phi0 - phi))[None, None, :]),
243
- intensities_abs,
244
- out=np.zeros_like(intensities_abs),
245
- where=intensities_abs > 1e-8,
292
+ np.divide(
293
+ np.abs(
294
+ ulast[:, :, None] * np.cos((NFourier - 1) * (phi0 - phi))[None, None, :]
295
+ ),
296
+ intensities_abs,
297
+ out=np.zeros_like(intensities_abs),
298
+ where=intensities_abs > 1e-8,
299
+ )
246
300
  )
247
301
  return I0_orig * np.squeeze(intensities), Fourier_error
248
302
  else:
@@ -251,7 +305,16 @@ def _assemble_intensity_and_fluxes(
251
305
 
252
306
  # Construct u0
253
307
  # --------------------------------------------------------------------------------------------------------------------------
254
- def u0(tau, return_act_dscale_reclassification=False):
308
+ def u0(tau, is_antiderivative_wrt_tau=False, _return_act_dscale_for_reclass=False):
309
+ """
310
+ Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
311
+ Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
312
+ This function is useful for calculating actinic fluxes and other quantities of interest,
313
+ but reclassification of delta-scaled flux and other corrections must be done manually
314
+ (for actinic flux `generate_diff_act_flux_funcs` will automatically perform the reclassification).
315
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
316
+ to switch to an antiderivative of the function with respect to `tau`.
317
+ """
255
318
  tau = np.atleast_1d(tau)
256
319
  assert np.all(tau >= 0)
257
320
  assert np.all(tau <= tau_arr[-1])
@@ -268,11 +331,14 @@ def _assemble_intensity_and_fluxes(
268
331
  scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
269
332
  # The following complements the function `subroutines.generate_diff_act_flux_funcs`
270
333
  # in performing the reclassification of delta-scaled actinic flux
271
- if return_act_dscale_reclassification:
272
- act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
334
+ if _return_act_dscale_for_reclass:
335
+ if is_antiderivative_wrt_tau:
336
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0) - I0 * np.exp(-tau / mu0) * -mu0
337
+ else:
338
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
273
339
  else:
274
340
  scaled_tau = tau
275
- if return_act_dscale_reclassification:
341
+ if _return_act_dscale_for_reclass:
276
342
  act_dscale_reclassification = 0
277
343
 
278
344
  exponent = np.concatenate(
@@ -282,14 +348,33 @@ def _assemble_intensity_and_fluxes(
282
348
  ],
283
349
  axis=-1,
284
350
  )
285
- if there_is_beam_source:
286
- u0 = np.einsum(
287
- "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
288
- ) + B_collect_0[l, :].T * np.exp(-scaled_tau[None, :] / mu0)
351
+
352
+ if is_antiderivative_wrt_tau:
353
+ if there_is_beam_source:
354
+ u0 = np.einsum(
355
+ "tij, tj -> it",
356
+ GC_collect_0[l, :, :],
357
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
358
+ optimize=True,
359
+ ) + B_collect_0[l, :].T * np.exp(-scaled_tau[None, :] / mu0) / (
360
+ -scale_tau[None, l] / mu0
361
+ )
362
+ else:
363
+ u0 = np.einsum(
364
+ "tij, tj -> it",
365
+ GC_collect_0[l, :, :],
366
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
367
+ optimize=True,
368
+ )
289
369
  else:
290
- u0 = np.einsum(
291
- "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
292
- )
370
+ if there_is_beam_source:
371
+ u0 = np.einsum(
372
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
373
+ ) + B_collect_0[l, :].T * np.exp(-scaled_tau[None, :] / mu0)
374
+ else:
375
+ u0 = np.einsum(
376
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
377
+ )
293
378
 
294
379
  # Contribution from particular solution for isotropic internal sources
295
380
  if there_is_iso_source:
@@ -301,11 +386,12 @@ def _assemble_intensity_and_fluxes(
301
386
  K_collect_0,
302
387
  G_inv_collect_0,
303
388
  mu_arr,
304
- _autograd_compatible,
389
+ is_antiderivative_wrt_tau,
390
+ _autograd_compatible
305
391
  )
306
- u0 += _mathscr_v_contribution
392
+ u0 = u0 + _mathscr_v_contribution
307
393
 
308
- if return_act_dscale_reclassification:
394
+ if _return_act_dscale_for_reclass:
309
395
  return I0_orig * np.squeeze(u0), act_dscale_reclassification
310
396
  else:
311
397
  return I0_orig * np.squeeze(u0)
@@ -320,7 +406,13 @@ def _assemble_intensity_and_fluxes(
320
406
  B_neg = B_collect_0[:, N:].T
321
407
 
322
408
 
323
- def flux_up(tau):
409
+ def flux_up(tau, is_antiderivative_wrt_tau=False):
410
+ """
411
+ (Energetic) Flux function with argument `tau` (type: array or float) for positive (upward) `mu` values.
412
+ Returns the diffuse flux magnitudes (same type and size as `tau`).
413
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
414
+ to switch to an antiderivative of the function with respect to `tau`.
415
+ """
324
416
  tau = np.atleast_1d(tau)
325
417
  assert np.all(tau >= 0)
326
418
  assert np.all(tau <= tau_arr[-1])
@@ -347,13 +439,19 @@ def _assemble_intensity_and_fluxes(
347
439
  K_collect_0,
348
440
  G_inv_collect_0,
349
441
  mu_arr,
350
- _autograd_compatible,
442
+ is_antiderivative_wrt_tau,
443
+ _autograd_compatible
351
444
  )[:N, :]
352
445
  else:
353
446
  _mathscr_v_contribution = 0
354
-
447
+
355
448
  if there_is_beam_source:
356
- direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau[None, :] / mu0)
449
+ if is_antiderivative_wrt_tau:
450
+ direct_beam_contribution = (
451
+ B_pos[:, l] * np.exp(-scaled_tau[None, :] / mu0) / (-scale_tau[None, l] / mu0)
452
+ )
453
+ else:
454
+ direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau[None, :] / mu0)
357
455
  else:
358
456
  direct_beam_contribution = 0
359
457
 
@@ -364,17 +462,36 @@ def _assemble_intensity_and_fluxes(
364
462
  ],
365
463
  axis=-1,
366
464
  )
367
- u0_pos = (
368
- np.einsum("tij, tj -> it", GC_pos[l, :, :], np.exp(exponent))
369
- + direct_beam_contribution
370
- + _mathscr_v_contribution
371
- )
465
+
466
+ if is_antiderivative_wrt_tau:
467
+ u0_pos = (
468
+ np.einsum(
469
+ "tij, tj -> it",
470
+ GC_pos[l, :, :],
471
+ np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]),
472
+ )
473
+ + direct_beam_contribution
474
+ + _mathscr_v_contribution
475
+ )
476
+ else:
477
+ u0_pos = (
478
+ np.einsum("tij, tj -> it", GC_pos[l, :, :], np.exp(exponent))
479
+ + direct_beam_contribution
480
+ + _mathscr_v_contribution
481
+ )
372
482
 
373
483
  flux = 2 * pi * (mu_arr_pos * W) @ u0_pos
374
484
  return I0_orig * np.squeeze(flux)[()]
375
485
 
376
486
 
377
- def flux_down(tau):
487
+ def flux_down(tau, is_antiderivative_wrt_tau=False):
488
+ """
489
+ (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
490
+ Returns a tuple of the diffuse and direct flux magnitudes respectively where each entry is of the
491
+ same type and size as `tau`.
492
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
493
+ to switch to an antiderivative of the function with respect to `tau`.
494
+ """
378
495
  tau = np.atleast_1d(tau)
379
496
  assert np.all(tau >= 0)
380
497
  assert np.all(tau <= tau_arr[-1])
@@ -401,15 +518,21 @@ def _assemble_intensity_and_fluxes(
401
518
  K_collect_0,
402
519
  G_inv_collect_0,
403
520
  mu_arr,
404
- _autograd_compatible,
521
+ is_antiderivative_wrt_tau,
522
+ _autograd_compatible
405
523
  )[N:, :]
406
524
  else:
407
525
  _mathscr_v_contribution = 0
408
526
 
409
527
  if there_is_beam_source:
410
- direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau[None, :] / mu0)
411
- direct_beam = I0 * mu0 * np.exp(-tau / mu0)
412
- direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
528
+ if is_antiderivative_wrt_tau:
529
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau[None, :] / mu0) / (-scale_tau[None, l] / mu0)
530
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0) * -mu0
531
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau[None, l] / mu0)
532
+ else:
533
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau[None, :] / mu0)
534
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0)
535
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
413
536
  else:
414
537
  direct_beam_contribution = 0
415
538
  direct_beam = 0
@@ -422,11 +545,19 @@ def _assemble_intensity_and_fluxes(
422
545
  ],
423
546
  axis=-1,
424
547
  )
425
- u0_neg = (
426
- np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent))
427
- + direct_beam_contribution
428
- + _mathscr_v_contribution
429
- )
548
+
549
+ if is_antiderivative_wrt_tau:
550
+ u0_neg = (
551
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent) / (scale_tau[l, None] * K_collect_0[l, :]))
552
+ + direct_beam_contribution
553
+ + _mathscr_v_contribution
554
+ )
555
+ else:
556
+ u0_neg = (
557
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent))
558
+ + direct_beam_contribution
559
+ + _mathscr_v_contribution
560
+ )
430
561
 
431
562
  diffuse_flux = 2 * pi * (mu_arr_pos * W) @ u0_neg + direct_beam_scaled - direct_beam
432
563
  return (
@@ -29,9 +29,9 @@ def _solve_for_coeffs(
29
29
  NLayers_to_use_sparse, # Number of layers above or equal which to use sparse matrices
30
30
  ):
31
31
  """
32
- In this function the unknown coefficients of the general solution to the system
33
- of ODEs for each Fourier mode is solved using the boundary conditions
34
- and the product of the coefficients and the eigenvectors is returned.
32
+ Uses the boundary conditions to solve for the unknown coefficients
33
+ of the general solution to the system of ordinary differential equations for each Fourier mode.
34
+ Returns the product of the coefficients and the eigenvectors.
35
35
  This function is wrapped by the `_assemble_intensity_and_fluxes` function.
36
36
  It has many seemingly redundant arguments to maximize precomputation in the `pydisort` function.
37
37
  See the Jupyter Notebook, especially section 3, for documentation, explanation and derivation.
@@ -124,7 +124,8 @@ def _solve_for_coeffs(
124
124
  # _mathscr_v_contribution
125
125
  if there_is_iso_source and m_equals_0:
126
126
  _mathscr_v_contribution_top = -_mathscr_v(
127
- np.array([0]), np.array([0]),
127
+ np.array([0]),
128
+ np.array([0]),
128
129
  s_poly_coeffs[0:1, :],
129
130
  Nscoeffs,
130
131
  G_collect_m[0:1, N:, :],
@@ -139,38 +140,51 @@ def _solve_for_coeffs(
139
140
  _mathscr_v_contribution_middle = (
140
141
  _mathscr_v(
141
142
  tau_arr[:-1],
142
- indices,
143
- s_poly_coeffs[:-1, :],
143
+ indices + 1,
144
+ s_poly_coeffs,
144
145
  Nscoeffs,
145
- G_collect_m[:-1, :, :],
146
- K_collect_m[:-1, :],
147
- G_inv_collect_0[:-1, :, :],
146
+ G_collect_m,
147
+ K_collect_m,
148
+ G_inv_collect_0,
148
149
  mu_arr,
149
150
  )
150
151
  - _mathscr_v(
151
152
  tau_arr[:-1],
152
153
  indices,
153
- s_poly_coeffs[1:, :],
154
+ s_poly_coeffs,
154
155
  Nscoeffs,
155
- G_collect_m[1:, :, :],
156
- K_collect_m[1:, :],
157
- G_inv_collect_0[1:, :, :],
156
+ G_collect_m,
157
+ K_collect_m,
158
+ G_inv_collect_0,
158
159
  mu_arr,
159
160
  )
160
- ).ravel()
161
+ ).ravel(order='F')
161
162
 
162
- _mathscr_v_contribution_bottom = _mathscr_v(
163
- tau_arr[-1:], np.array([0]),
163
+ _mathscr_v_contribution_bottom = -_mathscr_v(
164
+ tau_arr[-1:],
165
+ np.array([0]),
164
166
  s_poly_coeffs[-1:, :],
165
167
  Nscoeffs,
166
- G_collect_m[-1:, N:, :],
168
+ G_collect_m[-1:, :N, :],
167
169
  K_collect_m[-1:, :],
168
170
  G_inv_collect_0[-1:, :, :],
169
- mu_arr).ravel()
171
+ mu_arr
172
+ ).ravel()
170
173
  if NBDRF > 0:
171
174
  _mathscr_v_contribution_bottom = (
172
- R + np.eye(N)
173
- ) @ _mathscr_v_contribution_bottom
175
+ _mathscr_v_contribution_bottom
176
+ + R
177
+ @ _mathscr_v(
178
+ tau_arr[-1:],
179
+ np.array([0]),
180
+ s_poly_coeffs[-1:, :],
181
+ Nscoeffs,
182
+ G_collect_m[-1:, N:, :],
183
+ K_collect_m[-1:, :],
184
+ G_inv_collect_0[-1:, :, :],
185
+ mu_arr
186
+ ).ravel()
187
+ )
174
188
 
175
189
  _mathscr_v_contribution = np.concatenate(
176
190
  [
@@ -17,9 +17,9 @@ def _solve_for_gen_and_part_sols(
17
17
  there_is_iso_source, # Is there an isotropic source?
18
18
  ):
19
19
  """
20
- In this function the coefficient matrix of the system of ODEs for each Fourier mode is diagonalized
21
- and the eigenpairs are returned; the general solution to each system of ODEs is determined up to unknown coefficients.
22
- The particular solutions to each system of ODEs is also determined and its coefficient vector is returned.
20
+ Diagonalizes the coefficient matrix of the system of ordinary differential equations (ODEs)
21
+ for each Fourier mode and returns the eigenpairs which give the general solution up to unknown coefficients.
22
+ Also solves for the particular solution to each system of ODEs and returns its coefficient vector.
23
23
  This function is wrapped by the `_assemble_intensity_and_fluxes` function.
24
24
  It has many seemingly redundant arguments to maximize precomputation in the `pydisort` function.
25
25
  See the Jupyter Notebook, especially section 3, for documentation, explanation and derivation.
@@ -46,16 +46,18 @@ def _solve_for_gen_and_part_sols(
46
46
  | `there_is_iso_source` | boolean |
47
47
 
48
48
  Notable internal variables of _solve_for_gen_and_part_sols
49
- | Variable | Type / Shape |
50
- | ----------------- | -------------------------------------- |
51
- | `ells_all` | `NLeg` |
52
- | `G_collect` | `NFourier*NLayers x NQuad x NQuad` | Reshaped to NFourier x NLayers x NQuad x NQuad
53
- | `K_collect` | `NFourier*NLayers x NQuad` | Reshaped to NFourier x NLayers x NQuad
54
- | `alpha_arr` | `NFourier*NLayers x NQuad/2 x NQuad/2` | Reshaped to NFourier x NLayers x NQuad/2 x NQuad/2
55
- | `beta_arr` | `NFourier*NLayers x NQuad/2 x NQuad/2` | Reshaped to NFourier x NLayers x NQuad/2 x NQuad/2
56
- | `X_tilde_arr` | `NFourier*NLayers x NQuad` | Reshaped to NFourier x NLayers x NQuad
57
- | `B_collect` | `NFourier*NLayers x NQuad` or `None` | Reshaped to NFourier x NLayers x NQuad
58
- | `G_inv_collect_0` | `NLayers x NQuad x NQuad` or `None` |
49
+ | Variable | Type / Shape |
50
+ | ------------------- | -------------------------------------- |
51
+ | `ells_all` | `NLeg` |
52
+ | `G_collect` | `NFourier*NLayers x NQuad x NQuad` | Reshaped to NFourier x NLayers x NQuad x NQuad
53
+ | `K_collect` | `NFourier*NLayers x NQuad` | Reshaped to NFourier x NLayers x NQuad
54
+ | `alpha_arr` | `NFourier*NLayers x NQuad/2 x NQuad/2` | Reshaped to NFourier x NLayers x NQuad/2 x NQuad/2
55
+ | `beta_arr` | `NFourier*NLayers x NQuad/2 x NQuad/2` | Reshaped to NFourier x NLayers x NQuad/2 x NQuad/2
56
+ | `X_tilde_arr` | `NFourier*NLayers x NQuad` | Reshaped to NFourier x NLayers x NQuad
57
+ | `B_collect` | `NFourier*NLayers x NQuad` or `None` | Reshaped to NFourier x NLayers x NQuad
58
+ | `eigenvecs_GpG_arr` | `NFourier*NLayers x NQuad/2 x NQuad |
59
+ | `eigenvecs_GmG_arr` | `NFourier*NLayers x NQuad/2 x NQuad |
60
+ | `G_inv_collect_0` | `NLayers x NQuad x NQuad` or `None` |
59
61
 
60
62
  """
61
63
  ############################### Assemble system and diagonalize coefficient matrix #########################################
@@ -109,7 +111,7 @@ def _solve_for_gen_and_part_sols(
109
111
  if np.any(weighted_asso_Leg_coeffs_l > 0) and np.all(
110
112
  np.isfinite(asso_leg_term_pos)
111
113
  ):
112
- # Generate mathscr_D and mathscr_X (BDRF terms)
114
+ # Generate D and X (phase function terms)
113
115
  # --------------------------------------------------------------------------------------------------------------------------
114
116
  D_temp = weighted_asso_Leg_coeffs_l[None, :] * asso_leg_term_pos.T
115
117
  D_pos = (scaled_omega_l / 2) * D_temp @ asso_leg_term_pos
@@ -190,7 +192,7 @@ def _solve_for_gen_and_part_sols(
190
192
  G_inv_collect_0[no_shortcut_indices_0, :, :] = G_inv_arr[: len(no_shortcut_indices_0), :, :]
191
193
  # --------------------------------------------------------------------------------------------------------------------------
192
194
 
193
- # Particular solution for the sunbeam source (refer to Section 3.6.1 of the Comprehensive Documentation)
195
+ # Particular solution for the direct beam source (refer to Section 3.6.1 of the Comprehensive Documentation)
194
196
  # --------------------------------------------------------------------------------------------------------------------------
195
197
  if there_is_beam_source:
196
198
  X_tilde_arr = X_tilde_arr[: len(no_shortcut_indices), :]
@@ -28,6 +28,7 @@ def pydisort(
28
28
  """Solves the 1D RTE for the fluxes, and optionally intensity,
29
29
  of a multi-layer atmosphere with the specified optical properties, boundary conditions
30
30
  and sources. Optionally performs delta-M scaling and NT corrections.
31
+ Refer to the `*_test.ipynb` Jupyter Notebooks in the `pydisotest` directory for examples of use.
31
32
 
32
33
  See https://pythonic-disort.readthedocs.io/en/latest/Pythonic-DISORT.html#1.-USER-INPUT-REQUIRED:-Choose-parameters
33
34
  for a more detailed explanation of each parameter.
@@ -64,7 +65,7 @@ def pydisort(
64
65
  Dirichlet boundary condition for the downward direction.
65
66
  only_flux : optional, bool
66
67
  Do NOT compute the intensity function?
67
- f_arr : optional, array
68
+ f_arr : optional, array or float
68
69
  Fractional scattering into peak for each atmospheric layer.
69
70
  NT_cor : optional, bool
70
71
  Perform Nakajima-Tanaka intensity corrections?
@@ -79,25 +80,36 @@ def pydisort(
79
80
 
80
81
  Returns
81
82
  -------
82
- mu_arr : array
83
+ mu_arr : array or float
83
84
  All `mu` (cosine of polar angle) quadrature nodes.
84
- Fp(tau) : function, function
85
- (Energetic) Flux function with argument `tau` (type: array) for positive (upward) `mu` values.
86
- Returns the diffuse flux magnitudes (type: array).
85
+ Fp(tau) : function
86
+ (Energetic) Flux function with argument `tau` (type: array or float) for positive (upward) `mu` values.
87
+ Returns the diffuse flux magnitudes (same type and size as `tau`).
88
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
89
+ to switch to an antiderivative of the function with respect to `tau`.
90
+ For example, `Fp(tau0, is_antiderivative_wrt_tau=True) - Fp(0, is_antiderivative_wrt_tau=True)`
91
+ will give the tau-integral of `Fp` from `tau0` to `0`.
87
92
  Fm(tau) : function
88
- (Energetic) Flux function with argument `tau` (type: array) for negative (downward) `mu` values.
89
- Returns a tuple of the diffuse and direct flux magnitudes respectively (type: (array, array)).
93
+ (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
94
+ Returns a tuple of the diffuse and direct flux magnitudes respectively where each entry is of the
95
+ same type and size as `tau`.
96
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
97
+ to switch to an antiderivative of the function with respect to `tau`.
90
98
  u0(tau) : function
91
- Zeroth Fourier mode of the intensity with argument `tau` (type: array).
99
+ Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
92
100
  Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
93
101
  This function is useful for calculating actinic fluxes and other quantities of interest,
94
102
  but reclassification of delta-scaled flux and other corrections must be done manually
95
103
  (for actinic flux `generate_diff_act_flux_funcs` will automatically perform the reclassification).
104
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
105
+ to switch to an antiderivative of the function with respect to `tau`.
96
106
  u(tau, phi) : function, optional
97
- Intensity function with arguments `(tau, phi, return_Fourier_error=False)` of types `(array, array, bool)`.
107
+ Intensity function with arguments `(tau, phi)` each of type array or float.
98
108
  Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
99
- The optional flag `return_Fourier_error` determines whether the function will also return
100
- the Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
109
+ Pass `return_Fourier_error = True` (defaults to `False`) to return the
110
+ Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
111
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
112
+ to switch to an antiderivative of the function with respect to `tau`.
101
113
 
102
114
  """
103
115
 
@@ -310,9 +322,9 @@ def pydisort(
310
322
  _autograd_compatible,
311
323
  )
312
324
 
313
- # TMS correction
325
+ # TMS correction for the intensity (see Section 3.7.2)
314
326
  # --------------------------------------------------------------------------------------------------------------------------
315
- def TMS_correction(tau, phi):
327
+ def TMS_correction(tau, phi, is_antiderivative_wrt_tau):
316
328
  Ntau = len(tau)
317
329
  Nphi = len(phi)
318
330
  # Atmospheric layer indices
@@ -357,20 +369,36 @@ def pydisort(
357
369
  )
358
370
  # --------------------------------------------------------------------------------------------------------------------------
359
371
 
360
- TMS_correction_pos = (
361
- np.exp(-scaled_tau / mu0)[None, :]
362
- - np.exp(
363
- (scaled_tau - scaled_tau_arr_l)[None, :] / mu_arr_pos[:, None]
364
- - scaled_tau_arr_l[None, :] / mu0
372
+ if is_antiderivative_wrt_tau:
373
+ TMS_correction_pos = (
374
+ np.exp(-scaled_tau / mu0)[None, :] / (-scale_tau[None, l] / mu0)
375
+ - np.exp(
376
+ (scaled_tau - scaled_tau_arr_l)[None, :] / mu_arr_pos[:, None]
377
+ - scaled_tau_arr_l[None, :] / mu0
378
+ ) / (scale_tau[None, l] / mu_arr_pos[:, None])
365
379
  )
366
- )
367
- TMS_correction_neg = (
368
- np.exp(-scaled_tau / mu0)[None, :]
369
- - np.exp(
370
- (scaled_tau_arr_lm1 - scaled_tau)[None, :] / mu_arr_pos[:, None]
371
- - scaled_tau_arr_lm1[None, :] / mu0
380
+ TMS_correction_neg = (
381
+ np.exp(-scaled_tau / mu0)[None, :] / (-scale_tau[None, l] / mu0)
382
+ - np.exp(
383
+ (scaled_tau_arr_lm1 - scaled_tau)[None, :] / mu_arr_pos[:, None]
384
+ - scaled_tau_arr_lm1[None, :] / mu0
385
+ ) / (-scale_tau[None, l] / mu_arr_pos[:, None])
386
+ )
387
+ else:
388
+ TMS_correction_pos = (
389
+ np.exp(-scaled_tau / mu0)[None, :]
390
+ - np.exp(
391
+ (scaled_tau - scaled_tau_arr_l)[None, :] / mu_arr_pos[:, None]
392
+ - scaled_tau_arr_l[None, :] / mu0
393
+ )
394
+ )
395
+ TMS_correction_neg = (
396
+ np.exp(-scaled_tau / mu0)[None, :]
397
+ - np.exp(
398
+ (scaled_tau_arr_lm1 - scaled_tau)[None, :] / mu_arr_pos[:, None]
399
+ - scaled_tau_arr_lm1[None, :] / mu0
400
+ )
372
401
  )
373
- )
374
402
 
375
403
  ## Contribution from other layers
376
404
  # TODO: Despite being vectorized and despite many attempts at further optimization,
@@ -392,72 +420,114 @@ def pydisort(
392
420
 
393
421
  layers_arr_repeat = np.repeat(layers_arr, Ntau)
394
422
  scaled_tau_tile = np.tile(scaled_tau, NLayers)
395
- scaled_tau_arr_with_0_repeat = np.repeat(scaled_tau_arr_with_0, Ntau)
396
423
 
397
424
  if any_pos_contribution:
398
425
  contribution_from_each_other_layer_pos = np.zeros((N, NLayers * Ntau, Nphi))
399
- scaled_tau_l_tile_pos = scaled_tau_tile[pos_contribution_mask]
400
- mathscr_B_l_repeat_posmasked = mathscr_B[
401
- :N, layers_arr_repeat[pos_contribution_mask], :
402
- ]
403
- scaled_tau_arr_with_0_l_tile_posmasked = scaled_tau_arr_with_0_repeat[:-Ntau][
404
- pos_contribution_mask
405
- ]
406
- scaled_tau_arr_with_0_lp1_tile_posmasked = scaled_tau_arr_with_0_repeat[Ntau:][
407
- pos_contribution_mask
408
- ]
409
-
410
- contribution_from_each_other_layer_pos[:, pos_contribution_mask, :] = (
411
- mathscr_B_l_repeat_posmasked
412
- * (
413
- np.exp(
414
- (scaled_tau_l_tile_pos - scaled_tau_arr_with_0_l_tile_posmasked)
415
- / mu_arr_pos[:, None]
416
- - scaled_tau_arr_with_0_l_tile_posmasked / mu0
417
- )
418
- - np.exp(
419
- (scaled_tau_l_tile_pos - scaled_tau_arr_with_0_lp1_tile_posmasked)
420
- / mu_arr_pos[:, None]
421
- - scaled_tau_arr_with_0_lp1_tile_posmasked / mu0
422
- )
423
- )[:, :, None]
424
- )
425
- contribution_from_other_layers_pos = np.sum(
426
- contribution_from_each_other_layer_pos.reshape((N, NLayers, Ntau, Nphi)),
427
- axis=1,
428
- )
426
+ scaled_tau_tile_pos = scaled_tau_tile[pos_contribution_mask]
427
+
428
+ layers_arr_repeat_pos = layers_arr_repeat[pos_contribution_mask]
429
+ mathscr_B_repeat_pos = mathscr_B[:N, layers_arr_repeat_pos, :]
430
+ scaled_tau_arr_with_0_l_repeat_pos = scaled_tau_arr_with_0[:-1][layers_arr_repeat_pos]
431
+ scaled_tau_arr_with_0_lp1_repeat_pos = scaled_tau_arr_with_0[1:][layers_arr_repeat_pos]
432
+
433
+ if is_antiderivative_wrt_tau:
434
+ scale_tau_repeat_pos = scale_tau[layers_arr_repeat_pos]
435
+ contribution_from_each_other_layer_pos[:, pos_contribution_mask, :] = (
436
+ mathscr_B_repeat_pos
437
+ * (
438
+ (
439
+ np.exp(
440
+ (scaled_tau_tile_pos - scaled_tau_arr_with_0_l_repeat_pos)
441
+ / mu_arr_pos[:, None]
442
+ - scaled_tau_arr_with_0_l_repeat_pos / mu0
443
+ )
444
+ - np.exp(
445
+ (scaled_tau_tile_pos - scaled_tau_arr_with_0_lp1_repeat_pos)
446
+ / mu_arr_pos[:, None]
447
+ - scaled_tau_arr_with_0_lp1_repeat_pos / mu0
448
+ )
449
+ )
450
+ / (scale_tau_repeat_pos / mu_arr_pos[:, None])
451
+ )[:, :, None]
452
+ )
453
+ contribution_from_other_layers_pos = np.sum(
454
+ contribution_from_each_other_layer_pos.reshape((N, NLayers, Ntau, Nphi)),
455
+ axis=1,
456
+ )
457
+ else:
458
+ contribution_from_each_other_layer_pos[:, pos_contribution_mask, :] = (
459
+ mathscr_B_repeat_pos
460
+ * (
461
+ np.exp(
462
+ (scaled_tau_tile_pos - scaled_tau_arr_with_0_l_repeat_pos)
463
+ / mu_arr_pos[:, None]
464
+ - scaled_tau_arr_with_0_l_repeat_pos / mu0
465
+ )
466
+ - np.exp(
467
+ (scaled_tau_tile_pos - scaled_tau_arr_with_0_lp1_repeat_pos)
468
+ / mu_arr_pos[:, None]
469
+ - scaled_tau_arr_with_0_lp1_repeat_pos / mu0
470
+ )
471
+ )[:, :, None]
472
+ )
473
+ contribution_from_other_layers_pos = np.sum(
474
+ contribution_from_each_other_layer_pos.reshape((N, NLayers, Ntau, Nphi)),
475
+ axis=1,
476
+ )
477
+
429
478
  if any_neg_contribution:
430
479
  contribution_from_each_other_layer_neg = np.zeros((N, NLayers * Ntau, Nphi))
431
- scaled_tau_l_tile_neg = scaled_tau_tile[neg_contribution_mask]
432
- mathscr_B_l_repeat_negmasked = mathscr_B[
433
- N:, layers_arr_repeat[neg_contribution_mask], :
434
- ]
435
- scaled_tau_arr_with_0_l_tile_negmasked = scaled_tau_arr_with_0_repeat[:-Ntau][
436
- neg_contribution_mask
437
- ]
438
- scaled_tau_arr_with_0_lp1_tile_negmasked = scaled_tau_arr_with_0_repeat[Ntau:][
439
- neg_contribution_mask
440
- ]
480
+ scaled_tau_tile_neg = scaled_tau_tile[neg_contribution_mask]
481
+
482
+ layers_arr_repeat_neg = layers_arr_repeat[neg_contribution_mask]
483
+ mathscr_B_repeat_neg = mathscr_B[N:, layers_arr_repeat_neg, :]
484
+ scaled_tau_arr_with_0_l_repeat_neg = scaled_tau_arr_with_0[:-1][layers_arr_repeat_neg]
485
+ scaled_tau_arr_with_0_lp1_repeat_neg = scaled_tau_arr_with_0[1:][layers_arr_repeat_neg]
441
486
 
442
- contribution_from_each_other_layer_neg[:, neg_contribution_mask, :] = (
443
- mathscr_B_l_repeat_negmasked
444
- * (
445
- np.exp(
446
- (scaled_tau_arr_with_0_lp1_tile_negmasked - scaled_tau_l_tile_neg)
447
- / mu_arr_pos[:, None]
448
- - scaled_tau_arr_with_0_lp1_tile_negmasked / mu0
449
- )
450
- - np.exp(
451
- (scaled_tau_arr_with_0_l_tile_negmasked - scaled_tau_l_tile_neg)
452
- / mu_arr_pos[:, None]
453
- - scaled_tau_arr_with_0_l_tile_negmasked / mu0
454
- )
455
- )[:, :, None]
456
- )
457
- contribution_from_other_layers_neg = np.sum(
458
- contribution_from_each_other_layer_neg.reshape((N, NLayers, Ntau, Nphi)),
459
- axis=1,
460
- )
487
+ if is_antiderivative_wrt_tau:
488
+ scale_tau_repeat_neg = scale_tau[layers_arr_repeat_neg]
489
+ contribution_from_each_other_layer_neg[:, neg_contribution_mask, :] = (
490
+ mathscr_B_repeat_neg
491
+ * (
492
+ (
493
+ np.exp(
494
+ (scaled_tau_arr_with_0_lp1_repeat_neg - scaled_tau_tile_neg)
495
+ / mu_arr_pos[:, None]
496
+ - scaled_tau_arr_with_0_lp1_repeat_neg / mu0
497
+ )
498
+ - np.exp(
499
+ (scaled_tau_arr_with_0_l_repeat_neg - scaled_tau_tile_neg)
500
+ / mu_arr_pos[:, None]
501
+ - scaled_tau_arr_with_0_l_repeat_neg / mu0
502
+ )
503
+ )
504
+ / (-scale_tau_repeat_pos / mu_arr_pos[:, None])
505
+ )[:, :, None]
506
+ )
507
+ contribution_from_other_layers_neg = np.sum(
508
+ contribution_from_each_other_layer_neg.reshape((N, NLayers, Ntau, Nphi)),
509
+ axis=1,
510
+ )
511
+ else:
512
+ contribution_from_each_other_layer_neg[:, neg_contribution_mask, :] = (
513
+ mathscr_B_repeat_neg
514
+ * (
515
+ np.exp(
516
+ (scaled_tau_arr_with_0_lp1_repeat_neg - scaled_tau_tile_neg)
517
+ / mu_arr_pos[:, None]
518
+ - scaled_tau_arr_with_0_lp1_repeat_neg / mu0
519
+ )
520
+ - np.exp(
521
+ (scaled_tau_arr_with_0_l_repeat_neg - scaled_tau_tile_neg)
522
+ / mu_arr_pos[:, None]
523
+ - scaled_tau_arr_with_0_l_repeat_neg / mu0
524
+ )
525
+ )[:, :, None]
526
+ )
527
+ contribution_from_other_layers_neg = np.sum(
528
+ contribution_from_each_other_layer_neg.reshape((N, NLayers, Ntau, Nphi)),
529
+ axis=1,
530
+ )
461
531
 
462
532
  if _autograd_compatible:
463
533
  if not any_pos_contribution:
@@ -481,7 +551,7 @@ def pydisort(
481
551
  return solution
482
552
  # --------------------------------------------------------------------------------------------------------------------------
483
553
 
484
- # IMS correction
554
+ # IMS correction for the intensity (see Section 3.7.2)
485
555
  # --------------------------------------------------------------------------------------------------------------------------
486
556
  sum1 = np.sum(omega_arr * tau_arr)
487
557
  omega_avg = sum1 / np.sum(tau_arr)
@@ -495,15 +565,22 @@ def pydisort(
495
565
  )
496
566
  scaled_mu0 = mu0 / (1 - omega_avg * f_avg)
497
567
 
498
- def IMS_correction(tau, phi):
568
+ def IMS_correction(tau, phi, is_antiderivative_wrt_tau):
499
569
  nu = subroutines.atleast_2d_append(
500
570
  subroutines.calculate_nu(-mu_arr_pos, phi, -mu0, phi0)
501
571
  )
502
572
  x = 1 / mu_arr_pos - 1 / scaled_mu0
503
- chi = (1 / (mu_arr_pos * scaled_mu0 * x))[:, None] * (
504
- (tau[None, :] - 1 / x[:, None]) * np.exp(-tau / scaled_mu0)[None, :]
505
- + np.exp(-tau[None, :] / mu_arr_pos[:, None]) / x[:, None]
506
- )
573
+ if is_antiderivative_wrt_tau:
574
+ chi = (
575
+ (scaled_mu0 - x[:, None] * scaled_mu0 * (scaled_mu0 + tau)[None, :])
576
+ * np.exp(-tau / scaled_mu0)[None, :]
577
+ - mu_arr_pos[:, None] * np.exp(-tau[None, :] / mu_arr_pos[:, None])
578
+ ) / (mu_arr_pos * scaled_mu0 * x**2)[:, None]
579
+ else:
580
+ chi = (
581
+ (tau[None, :] - 1 / x[:, None]) * np.exp(-tau / scaled_mu0)[None, :]
582
+ + np.exp(-tau[None, :] / mu_arr_pos[:, None]) / x[:, None]
583
+ ) / (mu_arr_pos * scaled_mu0 * x)[:, None]
507
584
  return (
508
585
  I0
509
586
  / (4 * pi)
@@ -518,26 +595,26 @@ def pydisort(
518
595
 
519
596
  # The corrected intensity
520
597
  # --------------------------------------------------------------------------------------------------------------------------
521
- def u_corrected(tau, phi, return_Fourier_error=False):
598
+ def u_corrected(tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
522
599
  tau = np.atleast_1d(tau)
523
600
  phi = np.atleast_1d(phi)
524
- NT_corrections = TMS_correction(tau, phi)
601
+ NT_corrections = TMS_correction(tau, phi, is_antiderivative_wrt_tau)
525
602
 
526
603
  if _autograd_compatible:
527
604
  NT_corrections = NT_corrections + np.concatenate(
528
- [np.zeros((N, len(tau), len(phi))), IMS_correction(tau, phi)], axis=0
605
+ [np.zeros((N, len(tau), len(phi))), IMS_correction(tau, phi, is_antiderivative_wrt_tau)], axis=0
529
606
  )
530
607
  else:
531
- NT_corrections[N:, :, :] += IMS_correction(tau, phi)
608
+ NT_corrections[N:, :, :] += IMS_correction(tau, phi, is_antiderivative_wrt_tau)
532
609
 
533
610
  if return_Fourier_error:
534
- u_star_outputs = u_star(tau, phi, True)
611
+ u_star_outputs = u_star(tau, phi, True, is_antiderivative_wrt_tau)
535
612
  return (
536
613
  u_star_outputs[0] + I0_orig * np.squeeze(NT_corrections),
537
614
  u_star_outputs[1],
538
615
  )
539
616
  else:
540
- return u_star(tau, phi, False) + I0_orig * np.squeeze(NT_corrections)
617
+ return u_star(tau, phi, False, is_antiderivative_wrt_tau) + I0_orig * np.squeeze(NT_corrections)
541
618
  # --------------------------------------------------------------------------------------------------------------------------
542
619
 
543
620
  return mu_arr, flux_up, flux_down, u0, u_corrected
@@ -1,7 +1,6 @@
1
1
  import numpy as np
2
2
  import scipy as sc
3
3
  from math import pi
4
- from numpy.polynomial.legendre import leggauss
5
4
 
6
5
 
7
6
  def transform_interval(arr, c, d, a, b):
@@ -71,7 +70,7 @@ def calculate_nu(mu, phi, mu_p, phi_p):
71
70
 
72
71
  Returns
73
72
  -------
74
- ndarray
73
+ nu : ndarray
75
74
  Cosine of scattering angles which axes capture variation with `mu, phi, mu_p, phi_p` respectively.
76
75
 
77
76
  """
@@ -104,7 +103,7 @@ def Gauss_Legendre_quad(N, c=0, d=1):
104
103
  Quadrature weights.
105
104
 
106
105
  """
107
- mu_arr_pos, W = leggauss(int(N))
106
+ mu_arr_pos, W = np.polynomial.legendre.leggauss(int(N))
108
107
 
109
108
  return transform_interval(mu_arr_pos, c, d, -1, 1), transform_weights(W, c, d, -1, 1)
110
109
 
@@ -224,8 +223,10 @@ def atleast_2d_append(*arys):
224
223
 
225
224
  def generate_diff_act_flux_funcs(u0):
226
225
  """Generates the up and down diffuse actinic flux functions respectively.
227
- This is a use case of the ``u0`` function that is an output of ``pydisort``.
226
+ This is a use case of the ``u0`` function which is an output of ``pydisort``.
228
227
  The reclassification of delta-scaled actinic flux is automatically performed.
228
+ The computation of actinic fluxes is similar to that of energetic fluxes
229
+ and the latter is discussed in section 3.8 of the Comprehensive Documentation.
229
230
 
230
231
  Parameters
231
232
  ----------
@@ -235,36 +236,102 @@ def generate_diff_act_flux_funcs(u0):
235
236
 
236
237
  Returns
237
238
  -------
238
- function
239
+ Fp_act(tau) : function
239
240
  Actinic flux function with argument `tau` (type: array) for positive (upward) `mu` values.
240
241
  Returns the diffuse flux magnitudes (type: array).
241
- function
242
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
243
+ to switch to an antiderivative of the function with respect to `tau`.
244
+ Fm_act(tau) : function
242
245
  Actinic flux function with argument `tau` (type: array) for negative (downward) `mu` values.
243
246
  Returns the diffuse flux magnitudes (type: array).
247
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
248
+ to switch to an antiderivative of the function with respect to `tau`.
244
249
 
245
250
  """
246
251
  N = np.shape(u0(0))[0] // 2
247
252
  GL_weights = Gauss_Legendre_quad(N, 0, 1)[1]
248
253
 
249
254
  # Note that the zeroth axis of the array u0(tau) captures variation with mu
250
- flux_act_up = lambda tau: 2 * pi * GL_weights @ u0(tau)[:N]
251
- def flux_act_down_diffuse(tau):
252
- u0_cache, act_dscale_reclassification = u0(tau, True)
255
+ def flux_act_up(tau, is_antiderivative_wrt_tau=False):
256
+ return np.squeeze(2 * pi * GL_weights @ u0(tau, is_antiderivative_wrt_tau)[:N])[()]
257
+
258
+ def flux_act_down_diffuse(tau, is_antiderivative_wrt_tau=False):
259
+ u0_cache, act_dscale_reclassification = u0(tau, is_antiderivative_wrt_tau, _return_act_dscale_for_reclass=True)
253
260
  result_without_reclassification = 2 * pi * GL_weights @ u0_cache[N:]
254
- return result_without_reclassification + act_dscale_reclassification
261
+ return np.squeeze(result_without_reclassification + act_dscale_reclassification)[()]
255
262
 
256
263
  return flux_act_up, flux_act_down_diffuse
257
264
 
258
265
 
259
- def _mathscr_v(tau, # Input optical depths
260
- l, # Layer index of each input optical depth
261
- s_poly_coeffs, # Polynomial coefficients of isotropic source
262
- Nscoeffs, # Number of isotropic source polynomial coefficients
263
- G, # Eigenvector matrices
264
- K, # Eigenvalues
265
- G_inv, # Inverse of eigenvector matrix
266
- mu_arr, # Quadrature nodes for both hemispheres
267
- _autograd_compatible=False # Should the output functions be compatible with autograd?
266
+ def interpolate(mu_arr_pos, u):
267
+ """Polynomial (Barycentric) interpolation with respect to mu. The output
268
+ is a function that is continuous and variable in all three arguments: mu, tau and phi.
269
+ Discussed in sections 3.7 and 6.3 in the Comprehensive Documentation.
270
+
271
+ Parameters
272
+ ----------
273
+ mu_arr_pos : array
274
+ Positive `mu` (cosine of polar angle) quadrature nodes.
275
+ Equivalent to `mu_arr[:N]`.
276
+ u : function
277
+ Non-interpolated intensity function as outputted by `pydisort`.
278
+
279
+ Returns
280
+ -------
281
+ u_interpol : function
282
+ Intensity function with arguments `(mu, tau, phi)` each of type array or float.
283
+ Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
284
+ Pass `return_Fourier_error = True` (defaults to `False`) to return the
285
+ Cauchy / Fourier convergence evaluation (type: float) for the last Fourier term.
286
+ Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
287
+ to switch to an antiderivative of the function with respect to `tau`.
288
+
289
+ """
290
+ N = len(mu_arr_pos)
291
+ u_pos_interpol = sc.interpolate.BarycentricInterpolator(mu_arr_pos)
292
+ u_neg_interpol = sc.interpolate.BarycentricInterpolator(-mu_arr_pos)
293
+
294
+ def u_interpol(mu, tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
295
+ assert np.all(np.abs(mu) <= 1)
296
+ mu = np.atleast_1d(mu)
297
+
298
+ if return_Fourier_error:
299
+ u_cache, Fourier_error = u(tau, phi, True, is_antiderivative_wrt_tau)
300
+ else:
301
+ u_cache = u(tau, phi, False, is_antiderivative_wrt_tau)
302
+
303
+ u_shape = np.shape(u_cache)
304
+ assert N == u_shape[0] // 2 # Check that mu_arr_pos (of length N) matches the function u
305
+
306
+ results = np.empty((len(mu),) + u_shape[1:])
307
+ mask_pos = mu > 0
308
+ mask_else = ~mask_pos
309
+
310
+ if np.any(mask_pos):
311
+ u_pos_interpol.set_yi(u_cache[:N])
312
+ results[mask_pos] = u_pos_interpol(mu[mask_pos])
313
+ if np.any(mask_else):
314
+ u_neg_interpol.set_yi(u_cache[N:])
315
+ results[mask_else] = u_neg_interpol(mu[mask_else])
316
+
317
+ if return_Fourier_error:
318
+ return np.squeeze(results)[()], Fourier_error
319
+ else:
320
+ return np.squeeze(results)[()]
321
+
322
+ return u_interpol
323
+
324
+
325
+ def _mathscr_v(tau, # Input optical depths
326
+ l, # Layer index of each input optical depth
327
+ s_poly_coeffs, # Polynomial coefficients of isotropic source
328
+ Nscoeffs, # Number of isotropic source polynomial coefficients
329
+ G, # Eigenvector matrices
330
+ K, # Eigenvalues
331
+ G_inv, # Inverse of eigenvector matrix
332
+ mu_arr, # Quadrature nodes for both hemispheres
333
+ is_antiderivative_wrt_tau=False, # Switch to an antiderivative of the function?
334
+ _autograd_compatible=False, # Should the output functions be compatible with autograd?
268
335
  ):
269
336
  """Particular solution for isotropic internal sources.
270
337
  Refer to Section 3.6.1 of the Comprehensive Documentation.
@@ -273,16 +340,17 @@ def _mathscr_v(tau, # Input optical depths
273
340
  and `_solve_for_coeffs` functions which call it.
274
341
 
275
342
  Arguments of _mathscr_v
276
- | Variable | Shape |
277
- | ---------------------- | ------------------------------------- |
278
- | `tau` | `Ntau` |
279
- | `l` | `Ntau` |
280
- | `s_poly_coeffs` | `NLayers x Nscoeffs` or `Nscoeffs` |
281
- | `G` | `NLayers<= x NQuad x NQuad` |
282
- | `K` | `NLayers<= x NQuad` |
283
- | `G_inv` | `NLayers<= x NQuad x NQuad` or `None` |
284
- | `mu_arr` | `NQuad` |
285
- | `_autograd_compatible` | boolean |
343
+ | Variable | Shape |
344
+ | --------------------------- | ------------------------------------- |
345
+ | `tau` | `Ntau` |
346
+ | `l` | `Ntau` |
347
+ | `s_poly_coeffs` | `NLayers x Nscoeffs` or `Nscoeffs` |
348
+ | `G` | `NLayers<= x NQuad x NQuad` |
349
+ | `K` | `NLayers<= x NQuad` |
350
+ | `G_inv` | `NLayers<= x NQuad x NQuad` or `None` |
351
+ | `mu_arr` | `NQuad` |
352
+ | `is_antiderivative_wrt_tau` | boolean |
353
+ | `_autograd_compatible` | boolean |
286
354
 
287
355
  Notable internal variables of _mathscr_v
288
356
  | Variable | Shape |
@@ -340,11 +408,17 @@ def _mathscr_v(tau, # Input optical depths
340
408
  0,
341
409
  ),
342
410
  )
343
-
411
+
412
+ powers = np.flip(np.arange(Nscoeffs))[None, :]
413
+ if is_antiderivative_wrt_tau:
414
+ tau_poly = tau[:, None] ** (powers + 1) / (powers + 1)
415
+ else:
416
+ tau_poly = tau[:, None] ** powers
417
+
344
418
  return np.einsum(
345
419
  "tik, tc, ctk, tkj, j -> it",
346
420
  G[l, :, :],
347
- tau[:, None] ** np.flip(np.arange(Nscoeffs))[None, :],
421
+ tau_poly,
348
422
  mathscr_v_coeffs[:, l, :],
349
423
  G_inv[l, :, :],
350
424
  1 / mu_arr,
@@ -365,6 +439,7 @@ def _compare(results, mu_to_compare, reorder_mu, flux_up, flux_down, u):
365
439
  which are stored in .npz files, against results from PythonicDISORT. Used in our PyTests.
366
440
 
367
441
  """
442
+ # Load saved results from Stamnes' DISORT
368
443
  uu = results["uu"]
369
444
  flup = results["flup"]
370
445
  rfldn = results["rfldn"]
@@ -438,6 +513,7 @@ def _compare(results, mu_to_compare, reorder_mu, flux_up, flux_down, u):
438
513
  diff_tau_pt = tau_test_arr[max_diff_tau_index]
439
514
  ratio_tau_pt = tau_test_arr[max_ratio_tau_index]
440
515
  print("Intensities")
516
+ print()
441
517
  print("At tau = " + str(diff_tau_pt))
442
518
  print("Max pointwise difference =", np.max(diff[:, max_diff_tau_index, :]))
443
519
  print("At tau = " + str(ratio_tau_pt))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PythonicDISORT
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Discrete Ordinates Solver for the (1D) Radiative Transfer Equation in a single or multi-layer atmosphere.
5
5
  Author-email: Dion HO Jia Xu <dh3065@columbia.edu>
6
6
  Project-URL: Homepage, https://github.com/LDEO-CREW/Pythonic-DISORT
@@ -20,42 +20,45 @@ Provides-Extra: pytest
20
20
  Requires-Dist: pytest >=6.2.5 ; extra == 'pytest'
21
21
 
22
22
  # Introduction
23
- The PythonicDISORT package is a Discrete Ordinates Solver for the (1D) Radiative Transfer Equation in a single or multi-layer plane-parallel atmosphere.
23
+ The PythonicDISORT package is a Discrete Ordinates Solver for the (1D) Radiative Transfer Equation
24
+ in a plane-parallel, horizontally homogeneous atmosphere.
24
25
  It is coded entirely in Python 3 and is a reimplementation instead of a wrapper.
25
26
  While PythonicDISORT has been optimized for speed, it will naturally be slower than similar FORTRAN algorithms.
26
27
  On the other hand, PythonicDISORT should be easier to install, use, and modify than FORTRAN-based Discrete Ordinates Solvers.
27
28
 
28
- PythonicDISORT is based on Stamnes' FORTRAN DISORT (see References, in particular [2, 3, 8]) and has its main features:
29
- delta-M scaling, Nakajima-Tanaka (NT) corrections, only flux option, isotropic internal sources (thermal sources),
30
- Dirichlet boundary conditions (diffuse flux boundary sources), Bi-Directional Reflectance Function (BDRF) for surface reflection, and more.
31
- In addition, upon the request of a user, we added a subroutine to calculate actinic fluxes and we are open to further feature requests as well as feedback.
29
+ PythonicDISORT is based on Stamnes' FORTRAN DISORT (see References, in particular [2, 3, 8]) and has its main features: multi-layer solver,
30
+ delta-M scaling, Nakajima-Tanaka (NT) corrections, only flux option, direct beam source, isotropic internal source (blackbody emission),
31
+ Dirichlet boundary conditions (diffuse flux boundary sources), Bi-Directional Reflectance Function (BDRF) for surface reflection,
32
+ interpolation with respect to polar angle and more.
33
+ In addition, we added a subroutine to calculate actinic fluxes to satisfy a user request, and integration with respect to tau was also added.
34
+ Further feature requests as well as feedback are welcome.
32
35
 
33
36
  You may contact me, Dion, through dh3065@columbia.edu.
34
37
 
35
- Our **GitHub repository:** https://github.com/LDEO-CREW/Pythonic-DISORT also includes our F2PY-wrapped Stamnes DISORT (version 4.0.99) in the `disort4.0.99_f2py` directory.
36
- The original was downloaded from http://www.rtatmocn.com/disort/. The wrapper is inspired by https://github.com/kconnour/pyRT_DISORT.
38
+ The **GitHub repository** is https://github.com/LDEO-CREW/Pythonic-DISORT.
37
39
 
38
40
  # Documentation
39
41
  https://pythonic-disort.readthedocs.io/en/latest/
40
42
 
41
43
  Also see the accompanying Jupyter Notebook `Pythonic-DISORT.ipynb` in the `docs` directory
42
44
  of our GitHub repository.
43
- The Jupyter Notebook provides comprehensive documentation, suggested inputs, explanations,
45
+ This Jupyter Notebook provides comprehensive documentation, suggested inputs, explanations,
44
46
  mathematical derivations and verification tests.
45
- We highly recommend reading the non-optional parts of sections 1 and 2 before use.
47
+ It is highly recommended that new users read the non-optional parts of sections 1 and 2.
46
48
 
47
49
  ## PyTest and examples of how to use PythonicDISORT
48
50
 
49
- Not only do we have verification tests in the Jupyter Notebook, we have also recreated most of the test problems in Stamnes' `disotest.f90`;
50
- `disotest.f90` is included in the `disort4.0.99_f2py` directory of our GitHub repository. In these tests, the solutions from PythonicDISORT are compared
51
- against solutions from our F2PY-wrapped Stamnes' DISORT (version 4.0.99). With PyTest installed, execute the console command `pytest`
52
- in the `pydisotest` directory to run these tests. The `pydisotest` directory also contains Jupyter Notebooks, one for each test,
53
- to show how the tests were implemented. These notebooks double up as examples of how to use PythonicDISORT.
51
+ Not only are there verification tests in `Pythonic-DISORT.ipynb`,
52
+ most of the test problems in Stamnes' `disotest.f90` (download DISORT 4.0.99 from http://www.rtatmocn.com/disort/) have also been recreated.
53
+ In these tests, the solutions from PythonicDISORT are compared against solutions
54
+ from a F2PY-wrapped Stamnes' DISORT (version 4.0.99; wrapper inspired by https://github.com/kconnour/pyRT_DISORT). With PyTest installed, execute the console command `pytest`
55
+ in the `pydisotest` directory to run these tests. The `pydisotest` directory also contains Jupyter Notebooks to show the implementation of each test.
56
+ These notebooks double up as examples of how to use PythonicDISORT.
54
57
 
55
58
  # Installation
56
59
 
57
60
  * From PyPI: `pip install PythonicDISORT`
58
- * From Conda-forge: (TODO: we need to first publish on Conda-forge)
61
+ * From Conda-forge: (TODO: need to first publish on Conda-forge)
59
62
  * By cloning repository: `pip install .` in the `Pythonic-DISORT` directory; `pip install -r all_optional_dependencies.txt` to install all optional dependencies (see *Requirements to run PythonicDISORT*)
60
63
 
61
64
  ## Requirements to run PythonicDISORT
@@ -64,21 +67,18 @@ to show how the tests were implemented. These notebooks double up as examples of
64
67
  * `scipy >= 1.8.0`
65
68
  * (OPTIONAL) `pytest >= 6.2.5` (Required to use the command `pytest`, see *PyTest and examples of how to use PythonicDISORT*)
66
69
 
67
- ## Additional requirements to run the Jupyter Notebook
70
+ ## (OPTIONAL) Additional requirements to run the Jupyter Notebook
68
71
  * `autograd >= 1.5`
69
72
  * `jupyter > 1.0.0`
70
73
  * `notebook > 6.5.2`
74
+ * `matplotlib >= 3.6.0`
71
75
 
72
- In addition, our F2PY-wrapped Stamnes' DISORT (in the `disort4.0.99_f2py` directory), or equivalent,
73
- must be set up to run the last section (section 6).
76
+ In addition, a F2PY-wrapped Stamnes' DISORT, or equivalent, is required to properly run the last section (section 6).
74
77
 
75
78
  ## Compatibility
76
79
 
77
- The PythonicDISORT package *should* be system agnostic given its minimal dependencies and pure Python code.
78
- We do not guarantee that our Jupyter Notebook and F2PY-wrapped Stamnes' DISORT will work on other systems though.
79
- The latter will almost certainly need user edits to work and in any case it requires FORTRAN compilers which
80
- are not included in our GitHub repository. Everything in the repository was built and tested on Windows 11 but
81
- not yet tested on other systems.
80
+ The PythonicDISORT package should be system agnostic given its minimal dependencies and pure Python code.
81
+ Everything in the repository was built and tested on Windows 11.
82
82
 
83
83
  # Acknowledgements
84
84
 
@@ -106,6 +106,8 @@ Science and Technology Center (STC) (Award #2019625) under which this package wa
106
106
 
107
107
  10) Trefethen, L. N.. 1996. *Finite difference and spectral methods for ordinary and partial differential equations.* https://people.maths.ox.ac.uk/trefethen/pdetext.html.
108
108
 
109
- 11) T. Nakajima and M. Tanaka. 1988. *Algorithms for radiative intensity calculations in moderately thick atmospheres using a truncation approximation.* https://www.sciencedirect.com/science/article/pii/0022407388900313.
109
+ 11) Knut Stamnes. 1982. *On the computation of angular distributions of radiation in planetary atmospheres.* https://www.sciencedirect.com/science/article/pii/0022407382900966.
110
110
 
111
- 12) K. Connour and A. Stcherbinine. 2022. *pyRT_DISORT.* https://github.com/kconnour/pyRT_DISORT.
111
+ 12) T. Nakajima and M. Tanaka. 1988. *Algorithms for radiative intensity calculations in moderately thick atmospheres using a truncation approximation.* https://www.sciencedirect.com/science/article/pii/0022407388900313.
112
+
113
+ 13) Connour, Kyle and Wolff, Michael. 2020. *pyRT_DISORT: A pre-processing front-end to help make DISORT simulations easier in Python.* https://github.com/kconnour/pyRT_DISORT.
@@ -0,0 +1,11 @@
1
+ PythonicDISORT/__init__.py,sha256=V1obNzaFgqInxqYJ5h_r6JHbZRxx4VM5VgzCIS6EEzI,84
2
+ PythonicDISORT/_assemble_intensity_and_fluxes.py,sha256=SFX5SMUhKNt_MU-0t3kv5lLXxc1EzVK2EIDLnggoIEw,27206
3
+ PythonicDISORT/_solve_for_coeffs.py,sha256=ZGThwif73zAlhzXYdVzV2nJxdxRioq4MIFsni3_5-Hc,19594
4
+ PythonicDISORT/_solve_for_gen_and_part_sols.py,sha256=9ScaxY7SH3lYaE2UBnEIsmsDeXdjcf74_QWaeqyOIRs,12973
5
+ PythonicDISORT/pydisort.py,sha256=HmAdvbDHQoHQy1xXToJmT3X5MGRudMKoI0L7zAaOd-8,33188
6
+ PythonicDISORT/subroutines.py,sha256=2cJLQc46ctAiqY_qARlsxQZPYVcBOwkXf0-diCtnGXo,19446
7
+ PythonicDISORT-0.9.0.dist-info/LICENSE.md,sha256=q1i1QVh0OLzK0fj6kaNRNWsM_KZca66JwWgWwbuCPlA,1092
8
+ PythonicDISORT-0.9.0.dist-info/METADATA,sha256=dLzNm79C3rWzVMzC2GIboGpMloxs4Fiy3RpQgOMIMP4,7102
9
+ PythonicDISORT-0.9.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
10
+ PythonicDISORT-0.9.0.dist-info/top_level.txt,sha256=aX6kCeI1E7naZz3xfw4_mIbWh5KBh9dmF3FIvgP5m3Y,15
11
+ PythonicDISORT-0.9.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- PythonicDISORT/__init__.py,sha256=V1obNzaFgqInxqYJ5h_r6JHbZRxx4VM5VgzCIS6EEzI,84
2
- PythonicDISORT/_assemble_intensity_and_fluxes.py,sha256=IH1OYHfMLh1LrNiwSCnaJduELvHpStJgXqTycQ869X8,20410
3
- PythonicDISORT/_solve_for_coeffs.py,sha256=8x7bDARYjS_fnoFGt5LRnm6yUtjyPa_YQ2Mj0LA2AFY,19162
4
- PythonicDISORT/_solve_for_gen_and_part_sols.py,sha256=okJFq60712DN48-jdodipvP5S11yYK_yobDVpg0kTi8,12841
5
- PythonicDISORT/pydisort.py,sha256=xjhiRnwmVIPGKJqEuapwLwGLicL3mjizO-w3r5kaP1M,27880
6
- PythonicDISORT/subroutines.py,sha256=oqPIluJaFF88d7N7iS72wFOKfiYAmT3uA33tpPjuLP0,15857
7
- PythonicDISORT-0.7.0.dist-info/LICENSE.md,sha256=q1i1QVh0OLzK0fj6kaNRNWsM_KZca66JwWgWwbuCPlA,1092
8
- PythonicDISORT-0.7.0.dist-info/METADATA,sha256=1V3vyKC8Qa9uEH_WvCfdUnpU28HqOEnb3PdPCIR7Ij4,7230
9
- PythonicDISORT-0.7.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10
- PythonicDISORT-0.7.0.dist-info/top_level.txt,sha256=aX6kCeI1E7naZz3xfw4_mIbWh5KBh9dmF3FIvgP5m3Y,15
11
- PythonicDISORT-0.7.0.dist-info/RECORD,,