PythonicDISORT 0.9.1__tar.gz → 0.9.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PythonicDISORT
3
- Version: 0.9.1
3
+ Version: 0.9.3
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
@@ -8,7 +8,7 @@ dependencies = [
8
8
  "scipy>=1.8.0",
9
9
  ]
10
10
  name = "PythonicDISORT"
11
- version = "0.9.1"
11
+ version = "0.9.3"
12
12
  authors = [
13
13
  { name="Dion HO Jia Xu", email="dh3065@columbia.edu" },
14
14
  ]
@@ -26,7 +26,7 @@ def _assemble_intensity_and_fluxes(
26
26
  scale_tau, # Delta-scale factor for tau
27
27
  only_flux, # Only compute fluxes?
28
28
  use_banded_solver_NLayers, # Number of layers above or equal which to use `scipy.linalg.solve_banded`
29
- _autograd_compatible, # Should the output functions be compatible with autograd?
29
+ autograd_compatible, # Should the output functions be compatible with autograd?
30
30
  ):
31
31
  """Assembles the solution functions: intensity `u`, upward flux `flux_up`, downward flux `flux_down`
32
32
  from the previously solved eigenpairs and coefficients from the boundary conditions.
@@ -73,7 +73,7 @@ def _assemble_intensity_and_fluxes(
73
73
  | `scale_tau` | `NLayers` |
74
74
  | `only_flux` | boolean |
75
75
  | `use_banded_solver_NLayers` | scalar |
76
- | `_autograd_compatible` | boolean |
76
+ | `autograd_compatible` | boolean |
77
77
 
78
78
  Notable internal variables of _assemble_intensity_and_fluxes
79
79
  | Variable | Type / Shape |
@@ -92,7 +92,7 @@ def _assemble_intensity_and_fluxes(
92
92
  | `GC_pos` | `NLayers x NQuad/2 x NQuad` |
93
93
  | `GC_neg` | `NLayers x NQuad/2 x NQuad` |
94
94
  """
95
- if _autograd_compatible:
95
+ if autograd_compatible:
96
96
  import autograd.numpy as np
97
97
  else:
98
98
  import numpy as np
@@ -114,20 +114,21 @@ def _assemble_intensity_and_fluxes(
114
114
  Nscoeffs,
115
115
  there_is_iso_source,
116
116
  )
117
- if there_is_beam_source:
118
- if there_is_iso_source:
119
- G_collect, K_collect, B_collect, G_inv_collect_0 = outputs
120
- else:
121
- G_collect, K_collect, B_collect = outputs
122
- G_inv_collect_0 = None
117
+ if there_is_beam_source and there_is_iso_source:
118
+ G_collect, K_collect, B_collect, G_inv_collect_0 = outputs
119
+ B_collect_0 = B_collect[0, :, :]
120
+ elif there_is_beam_source and not there_is_iso_source:
121
+ G_collect, K_collect, B_collect = outputs
123
122
  B_collect_0 = B_collect[0, :, :]
123
+ G_inv_collect_0 = None
124
+ elif not there_is_beam_source and there_is_iso_source:
125
+ G_collect, K_collect, G_inv_collect_0 = outputs
126
+ B_collect = None
124
127
  else:
125
- if there_is_iso_source:
126
- G_collect, K_collect, G_inv_collect_0 = outputs
127
- else:
128
- G_collect, K_collect = outputs
129
- G_inv_collect_0 = None
128
+ G_collect, K_collect = outputs
130
129
  B_collect = None
130
+ G_inv_collect_0 = None
131
+
131
132
 
132
133
  GC_collect = _solve_for_coeffs(
133
134
  NFourier,
@@ -161,7 +162,7 @@ def _assemble_intensity_and_fluxes(
161
162
 
162
163
  # Construct the intensity function (refer to Section 3.7 of the Comprehensive Documentation)
163
164
  # --------------------------------------------------------------------------------------------------------------------------
164
- def u(tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
165
+ def u(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
165
166
  """
166
167
  Intensity function with arguments `(tau, phi)` of types `(array or float, array or float)`.
167
168
  Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
@@ -172,8 +173,8 @@ def _assemble_intensity_and_fluxes(
172
173
  """
173
174
  tau = np.atleast_1d(tau)
174
175
  phi = np.atleast_1d(phi)
175
- assert np.all(tau >= 0)
176
- assert np.all(tau <= tau_arr[-1])
176
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
177
+ raise ValueError("tau input outside the tau range specified for the atmosphere (check `tau_arr`).")
177
178
 
178
179
  # Atmospheric layer indices
179
180
  l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
@@ -196,34 +197,32 @@ def _assemble_intensity_and_fluxes(
196
197
  axis=-1,
197
198
  )
198
199
 
199
- if is_antiderivative_wrt_tau:
200
- if there_is_beam_source:
201
- um = (
202
- np.einsum(
203
- "mtij, mtj -> mti",
204
- GC_collect[:, l, :, :],
205
- np.exp(exponent) / (scale_tau[None, :, None] * K_collect)[:, l, :],
206
- optimize=True,
207
- )
208
- + (B_collect / (-scale_tau / mu0)[None, :, None])[:, l, :]
209
- * np.exp(-scaled_tau / mu0)[None, :, None]
210
- )
211
- else:
212
- um = np.einsum(
200
+ if is_antiderivative_wrt_tau and there_is_beam_source:
201
+ um = (
202
+ np.einsum(
213
203
  "mtij, mtj -> mti",
214
204
  GC_collect[:, l, :, :],
215
- np.exp(exponent) / (scale_tau[None, l, None] * K_collect)[:, l, :],
205
+ np.exp(exponent) / (scale_tau[None, :, None] * K_collect)[:, l, :],
216
206
  optimize=True,
217
207
  )
208
+ + (B_collect / (-scale_tau / mu0)[None, :, None])[:, l, :]
209
+ * np.exp(-scaled_tau / mu0)[None, :, None]
210
+ )
211
+ elif is_antiderivative_wrt_tau and not there_is_beam_source:
212
+ um = np.einsum(
213
+ "mtij, mtj -> mti",
214
+ GC_collect[:, l, :, :],
215
+ np.exp(exponent) / (scale_tau[None, l, None] * K_collect)[:, l, :],
216
+ optimize=True,
217
+ )
218
+ elif not is_antiderivative_wrt_tau and there_is_beam_source:
219
+ um = np.einsum(
220
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
221
+ ) + B_collect[:, l, :] * np.exp(-scaled_tau / mu0)[None, :, None]
218
222
  else:
219
- if there_is_beam_source:
220
- um = np.einsum(
221
- "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
222
- ) + B_collect[:, l, :] * np.exp(-scaled_tau / mu0)[None, :, None]
223
- else:
224
- um = np.einsum(
225
- "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
226
- )
223
+ um = np.einsum(
224
+ "mtij, mtj -> mti", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
225
+ )
227
226
 
228
227
  # Contribution from particular solution for isotropic internal sources
229
228
  if there_is_iso_source:
@@ -236,18 +235,17 @@ def _assemble_intensity_and_fluxes(
236
235
  G_inv_collect_0[l_uniq],
237
236
  mu_arr,
238
237
  is_antiderivative_wrt_tau,
239
- _autograd_compatible
238
+ autograd_compatible
240
239
  )
241
240
 
242
- if _autograd_compatible:
243
- if NFourier == 1:
244
- um = um + _mathscr_v_contribution.T
245
- else:
246
- 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
+ elif autograd_compatible:
244
+ um = um + np.concatenate((_mathscr_v_contribution.T, np.zeros((NFourier - 1, len(tau), NQuad))))
247
245
  else:
248
246
  um[0, :, :] += _mathscr_v_contribution.T
249
247
 
250
- intensities = np.einsum(
248
+ u = np.einsum(
251
249
  "mti, mp -> itp",
252
250
  um,
253
251
  np.cos(np.arange(NFourier)[:, None] * (phi0 - phi)[None, :]),
@@ -262,54 +260,59 @@ def _assemble_intensity_and_fluxes(
262
260
  ],
263
261
  axis=-1,
264
262
  )
265
- if is_antiderivative_wrt_tau:
266
- if there_is_beam_source:
267
- ulast = (
268
- np.einsum(
269
- "tij, tj -> it",
270
- GC_collect[-1, l, :, :],
271
- np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
272
- optimize=True,
273
- )
274
- + (B_collect.T[-1, :, :] / (-scale_tau / mu0)[None, :])[:, l]
275
- * np.exp(-scaled_tau / mu0)[None, :]
276
- )
277
- else:
278
- ulast = np.einsum(
263
+ if is_antiderivative_wrt_tau and there_is_beam_source:
264
+ ulast = (
265
+ np.einsum(
279
266
  "tij, tj -> it",
280
267
  GC_collect[-1, l, :, :],
281
268
  np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
282
269
  optimize=True,
283
270
  )
284
- else:
285
- if there_is_beam_source:
286
- ulast = np.einsum(
287
- "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
288
- ) + B_collect.T[-1, :, l] * np.exp(-scaled_tau / mu0)[None, :]
289
- else:
290
- ulast = np.einsum(
291
- "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
271
+ + (B_collect.T[-1, :, :] / (-scale_tau / mu0)[None, :])[:, l]
272
+ * np.exp(-scaled_tau / mu0)[None, :]
273
+ )
274
+ elif is_antiderivative_wrt_tau and not there_is_beam_source:
275
+ ulast = np.einsum(
276
+ "tij, tj -> it",
277
+ GC_collect[-1, l, :, :],
278
+ np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
279
+ optimize=True,
292
280
  )
281
+ elif not is_antiderivative_wrt_tau and not there_is_beam_source:
282
+ ulast = np.einsum(
283
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
284
+ ) + B_collect.T[-1, :, l] * np.exp(-scaled_tau / mu0)[None, :]
285
+ else:
286
+ ulast = np.einsum(
287
+ "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
288
+ )
293
289
 
294
- intensities_abs = np.abs(intensities)
290
+ u_abs = np.abs(u)
295
291
  Fourier_error = np.max(
296
292
  np.divide(
297
293
  np.abs(
298
294
  ulast[:, :, None] * np.cos((NFourier - 1) * (phi0 - phi))[None, None, :]
299
295
  ),
300
- intensities_abs,
301
- out=np.zeros_like(intensities_abs),
302
- where=intensities_abs > 1e-8,
296
+ u_abs,
297
+ out=np.zeros_like(u_abs),
298
+ where=u_abs > 1e-15,
303
299
  )
304
300
  )
305
- return I0_orig * np.squeeze(intensities), Fourier_error
301
+
302
+ if return_tau_arr:
303
+ return I0_orig * np.squeeze(u), Fourier_error, tau_arr
304
+ else:
305
+ return I0_orig * np.squeeze(u), Fourier_error
306
306
  else:
307
- return I0_orig * np.squeeze(intensities)
307
+ if return_tau_arr:
308
+ return I0_orig * np.squeeze(u), tau_arr
309
+ else:
310
+ return I0_orig * np.squeeze(u)
308
311
  # --------------------------------------------------------------------------------------------------------------------------
309
312
 
310
313
  # Construct u0
311
314
  # --------------------------------------------------------------------------------------------------------------------------
312
- def u0(tau, is_antiderivative_wrt_tau=False, _return_act_dscale_for_reclass=False):
315
+ def u0(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False, _return_act_dscale_for_reclass=False):
313
316
  """
314
317
  Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
315
318
  Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
@@ -320,8 +323,8 @@ def _assemble_intensity_and_fluxes(
320
323
  to switch to an antiderivative of the function with respect to `tau`.
321
324
  """
322
325
  tau = np.atleast_1d(tau)
323
- assert np.all(tau >= 0)
324
- assert np.all(tau <= tau_arr[-1])
326
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
327
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
325
328
 
326
329
  # Atmospheric layer indices
327
330
  l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
@@ -356,34 +359,32 @@ def _assemble_intensity_and_fluxes(
356
359
  axis=-1,
357
360
  )
358
361
 
359
- if is_antiderivative_wrt_tau:
360
- if there_is_beam_source:
361
- u0 = (
362
- np.einsum(
363
- "tij, tj -> it",
364
- GC_collect_0[l, :, :],
365
- np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
366
- optimize=True,
367
- )
368
- + (B_collect_0.T / (-scale_tau / mu0)[None, :])[:, l]
369
- * np.exp(-scaled_tau / mu0)[None, :]
370
- )
371
- else:
372
- u0 = np.einsum(
362
+ if is_antiderivative_wrt_tau and there_is_beam_source:
363
+ u0 = (
364
+ np.einsum(
373
365
  "tij, tj -> it",
374
366
  GC_collect_0[l, :, :],
375
367
  np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
376
368
  optimize=True,
377
369
  )
370
+ + (B_collect_0.T / (-scale_tau / mu0)[None, :])[:, l]
371
+ * np.exp(-scaled_tau / mu0)[None, :]
372
+ )
373
+ if is_antiderivative_wrt_tau and not there_is_beam_source:
374
+ u0 = np.einsum(
375
+ "tij, tj -> it",
376
+ GC_collect_0[l, :, :],
377
+ np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
378
+ optimize=True,
379
+ )
380
+ if not is_antiderivative_wrt_tau and there_is_beam_source:
381
+ u0 = np.einsum(
382
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
383
+ ) + B_collect_0.T[:, l] * np.exp(-scaled_tau / mu0)[None, :]
378
384
  else:
379
- if there_is_beam_source:
380
- u0 = np.einsum(
381
- "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
382
- ) + B_collect_0.T[:, l] * np.exp(-scaled_tau / mu0)[None, :]
383
- else:
384
- u0 = np.einsum(
385
- "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
386
- )
385
+ u0 = np.einsum(
386
+ "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
387
+ )
387
388
 
388
389
  # Contribution from particular solution for isotropic internal sources
389
390
  if there_is_iso_source:
@@ -396,11 +397,15 @@ def _assemble_intensity_and_fluxes(
396
397
  G_inv_collect_0[l_uniq],
397
398
  mu_arr,
398
399
  is_antiderivative_wrt_tau,
399
- _autograd_compatible
400
+ autograd_compatible
400
401
  )
401
402
  u0 = u0 + _mathscr_v_contribution
402
-
403
- if _return_act_dscale_for_reclass:
403
+
404
+ if return_tau_arr and _return_act_dscale_for_reclass:
405
+ return I0_orig * np.squeeze(u0), tau_arr, act_dscale_reclassification
406
+ elif return_tau_arr and not _return_act_dscale_for_reclass:
407
+ return I0_orig * np.squeeze(u0), tau_arr
408
+ elif not return_tau_arr and _return_act_dscale_for_reclass:
404
409
  return I0_orig * np.squeeze(u0), act_dscale_reclassification
405
410
  else:
406
411
  return I0_orig * np.squeeze(u0)
@@ -415,7 +420,7 @@ def _assemble_intensity_and_fluxes(
415
420
  B_neg = B_collect_0[:, N:].T
416
421
 
417
422
 
418
- def flux_up(tau, is_antiderivative_wrt_tau=False):
423
+ def flux_up(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
419
424
  """
420
425
  (Energetic) Flux function with argument `tau` (type: array or float) for positive (upward) `mu` values.
421
426
  Returns the diffuse flux magnitudes (same type and size as `tau`).
@@ -423,8 +428,8 @@ def _assemble_intensity_and_fluxes(
423
428
  to switch to an antiderivative of the function with respect to `tau`.
424
429
  """
425
430
  tau = np.atleast_1d(tau)
426
- assert np.all(tau >= 0)
427
- assert np.all(tau <= tau_arr[-1])
431
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
432
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
428
433
 
429
434
  # Atmospheric layer indices
430
435
  l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
@@ -449,18 +454,17 @@ def _assemble_intensity_and_fluxes(
449
454
  G_inv_collect_0[l_uniq],
450
455
  mu_arr,
451
456
  is_antiderivative_wrt_tau,
452
- _autograd_compatible
457
+ autograd_compatible
453
458
  )
454
459
  else:
455
460
  _mathscr_v_contribution = 0
456
461
 
457
- if there_is_beam_source:
458
- if is_antiderivative_wrt_tau:
459
- direct_beam_contribution = (
460
- (B_pos / (-scale_tau / mu0)[None, :])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
461
- )
462
- else:
463
- direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau / mu0)[None, :]
462
+ if there_is_beam_source and is_antiderivative_wrt_tau:
463
+ direct_beam_contribution = (
464
+ (B_pos / (-scale_tau / mu0)[None, :])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
465
+ )
466
+ elif there_is_beam_source and not is_antiderivative_wrt_tau:
467
+ direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau / mu0)[None, :]
464
468
  else:
465
469
  direct_beam_contribution = 0
466
470
 
@@ -488,12 +492,15 @@ def _assemble_intensity_and_fluxes(
488
492
  + direct_beam_contribution
489
493
  + _mathscr_v_contribution
490
494
  )
491
-
492
495
  flux = 2 * pi * (mu_arr_pos * W) @ u0_pos
493
- return I0_orig * np.squeeze(flux)[()]
496
+
497
+ if return_tau_arr:
498
+ return I0_orig * np.squeeze(flux)[()], tau_arr
499
+ else:
500
+ return I0_orig * np.squeeze(flux)[()]
494
501
 
495
502
 
496
- def flux_down(tau, is_antiderivative_wrt_tau=False):
503
+ def flux_down(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
497
504
  """
498
505
  (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
499
506
  Returns a tuple of the diffuse and direct flux magnitudes respectively where each entry is of the
@@ -502,8 +509,8 @@ def _assemble_intensity_and_fluxes(
502
509
  to switch to an antiderivative of the function with respect to `tau`.
503
510
  """
504
511
  tau = np.atleast_1d(tau)
505
- assert np.all(tau >= 0)
506
- assert np.all(tau <= tau_arr[-1])
512
+ if np.all(tau < 0) or np.all(tau > tau_arr[-1]):
513
+ raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
507
514
 
508
515
  # Atmospheric layer indices
509
516
  l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
@@ -528,20 +535,19 @@ def _assemble_intensity_and_fluxes(
528
535
  G_inv_collect_0[l_uniq],
529
536
  mu_arr,
530
537
  is_antiderivative_wrt_tau,
531
- _autograd_compatible
538
+ autograd_compatible
532
539
  )
533
540
  else:
534
541
  _mathscr_v_contribution = 0
535
542
 
536
- if there_is_beam_source:
537
- if is_antiderivative_wrt_tau:
538
- direct_beam_contribution = (B_neg / (-scale_tau / mu0)[None, l])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
539
- direct_beam = I0 * mu0 * np.exp(-tau / mu0) * -mu0
540
- direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau / mu0)[None, l]
541
- else:
542
- direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau / mu0)[None, :]
543
- direct_beam = I0 * mu0 * np.exp(-tau / mu0)
544
- direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
543
+ if there_is_beam_source and is_antiderivative_wrt_tau:
544
+ direct_beam_contribution = (B_neg / (-scale_tau / mu0)[None, l])[:, l] * np.exp(-scaled_tau / mu0)[None, :]
545
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0) * -mu0
546
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau / mu0)[None, l]
547
+ elif there_is_beam_source and not is_antiderivative_wrt_tau:
548
+ direct_beam_contribution = B_neg[:, l] * np.exp(-scaled_tau / mu0)[None, :]
549
+ direct_beam = I0 * mu0 * np.exp(-tau / mu0)
550
+ direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
545
551
  else:
546
552
  direct_beam_contribution = 0
547
553
  direct_beam = 0
@@ -567,12 +573,19 @@ def _assemble_intensity_and_fluxes(
567
573
  + direct_beam_contribution
568
574
  + _mathscr_v_contribution
569
575
  )
570
-
571
576
  diffuse_flux = 2 * pi * (mu_arr_pos * W) @ u0_neg + direct_beam_scaled - direct_beam
572
- return (
573
- I0_orig * np.squeeze(diffuse_flux)[()],
574
- I0_orig * I0 * np.squeeze(direct_beam)[()],
575
- )
577
+
578
+ if return_tau_arr:
579
+ return (
580
+ I0_orig * np.squeeze(diffuse_flux)[()],
581
+ I0_orig * I0 * np.squeeze(direct_beam)[()],
582
+ tau_arr
583
+ )
584
+ else:
585
+ return (
586
+ I0_orig * np.squeeze(diffuse_flux)[()],
587
+ I0_orig * I0 * np.squeeze(direct_beam)[()],
588
+ )
576
589
  # --------------------------------------------------------------------------------------------------------------------------
577
590
 
578
591
  if only_flux:
@@ -109,18 +109,17 @@ def _solve_for_coeffs(
109
109
  # Assemble RHS
110
110
  # --------------------------------------------------------------------------------------------------------------------------
111
111
  # Ensure the BCs are of the correct shape
112
- if b_pos_is_scalar:
113
- if m_equals_0:
114
- b_pos_m = np.full(N, b_pos)
115
- else:
116
- b_pos_m = np.zeros(N)
112
+ if b_pos_is_scalar and m_equals_0:
113
+ b_pos_m = np.full(N, b_pos)
114
+ elif b_pos_is_scalar and not m_equals_0:
115
+ b_pos_m = np.zeros(N)
117
116
  else:
118
117
  b_pos_m = b_pos[:, m]
119
- if b_neg_is_scalar:
120
- if m_equals_0:
121
- b_neg_m = np.full(N, b_neg)
122
- else:
123
- b_neg_m = np.zeros(N)
118
+
119
+ if b_neg_is_scalar and m_equals_0:
120
+ b_neg_m = np.full(N, b_neg)
121
+ elif b_neg_is_scalar and not m_equals_0:
122
+ b_neg_m = np.zeros(N)
124
123
  else:
125
124
  b_neg_m = b_neg[:, m]
126
125
 
@@ -205,7 +204,7 @@ def _solve_for_coeffs(
205
204
  l_range = np.arange(1, NLayers)
206
205
  RHS_middle = (
207
206
  (B_collect_m[l_range, :] - B_collect_m[l_range - 1, :])
208
- * np.exp(-mu0 * scaled_tau_arr_with_0)[l_range, None]
207
+ * np.exp(-scaled_tau_arr_with_0 / mu0)[l_range, None]
209
208
  ).ravel()
210
209
 
211
210
  if BDRF_bool:
@@ -103,9 +103,7 @@ def _solve_for_gen_and_part_sols(
103
103
  for l in range(NLayers):
104
104
  weighted_asso_Leg_coeffs_l = weighted_scaled_Leg_coeffs[l, :][ells] * fac
105
105
 
106
- if np.any(weighted_asso_Leg_coeffs_l > 0) and np.all( # Due to `fac` or the coeffs being zero
107
- np.isfinite(asso_leg_term_pos) # We take precautions against overflow and underflow (this shouldn't happen though)
108
- ):
106
+ if np.any(weighted_asso_Leg_coeffs_l > 0): # There are shortcuts if the coefficients are all zero
109
107
  scaled_omega_l = scaled_omega_arr[l]
110
108
 
111
109
  # Generate D
@@ -213,29 +211,27 @@ def _solve_for_gen_and_part_sols(
213
211
  # --------------------------------------------------------------------------------------------------------------------------
214
212
 
215
213
 
216
- if there_is_beam_source:
217
- if there_is_iso_source:
218
- return (
219
- G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
220
- K_collect.reshape((NFourier, NLayers, NQuad)),
221
- B_collect.reshape((NFourier, NLayers, NQuad)),
222
- G_inv_collect_0,
223
- )
224
- else:
225
- return (
226
- G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
227
- K_collect.reshape((NFourier, NLayers, NQuad)),
228
- B_collect.reshape((NFourier, NLayers, NQuad)),
229
- )
214
+ if there_is_beam_source and there_is_iso_source:
215
+ return (
216
+ G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
217
+ K_collect.reshape((NFourier, NLayers, NQuad)),
218
+ B_collect.reshape((NFourier, NLayers, NQuad)),
219
+ G_inv_collect_0,
220
+ )
221
+ elif there_is_beam_source and not there_is_iso_source:
222
+ return (
223
+ G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
224
+ K_collect.reshape((NFourier, NLayers, NQuad)),
225
+ B_collect.reshape((NFourier, NLayers, NQuad)),
226
+ )
227
+ elif not there_is_beam_source and there_is_iso_source:
228
+ return (
229
+ G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
230
+ K_collect.reshape((NFourier, NLayers, NQuad)),
231
+ G_inv_collect_0,
232
+ )
230
233
  else:
231
- if there_is_iso_source:
232
- return (
233
- G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
234
- K_collect.reshape((NFourier, NLayers, NQuad)),
235
- G_inv_collect_0,
236
- )
237
- else:
238
- return (
239
- G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
240
- K_collect.reshape((NFourier, NLayers, NQuad)),
241
- )
234
+ return (
235
+ G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),
236
+ K_collect.reshape((NFourier, NLayers, NQuad)),
237
+ )
@@ -1,5 +1,6 @@
1
1
  from PythonicDISORT import subroutines
2
2
  from PythonicDISORT._assemble_intensity_and_fluxes import _assemble_intensity_and_fluxes
3
+ import warnings
3
4
 
4
5
  import numpy as np
5
6
  import scipy as sc
@@ -23,12 +24,12 @@ def pydisort(
23
24
  BDRF_Fourier_modes=[],
24
25
  s_poly_coeffs=np.array([[]]),
25
26
  use_banded_solver_NLayers=13,
26
- _autograd_compatible = False,
27
+ autograd_compatible = False,
27
28
  ):
28
29
  """Solves the 1D RTE for the fluxes, and optionally intensity,
29
30
  of a multi-layer atmosphere with the specified optical properties, boundary conditions
30
31
  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.
32
+ Refer to the ``*_test.ipynb`` Jupyter Notebooks in the ``pydisotest`` directory for examples of use.
32
33
 
33
34
  See https://pythonic-disort.readthedocs.io/en/latest/Pythonic-DISORT.html#1.-USER-INPUT-REQUIRED:-Choose-parameters
34
35
  for a more detailed explanation of each parameter.
@@ -43,7 +44,7 @@ def pydisort(
43
44
  omega_arr : array or float
44
45
  Single-scattering albedo of each atmospheric layer.
45
46
  NQuad : int
46
- Number of `mu` quadrature nodes.
47
+ Number of ``mu`` quadrature nodes.
47
48
  Leg_coeffs_all : 2darray
48
49
  All available unweighted phase function Legendre coefficients.
49
50
  Each row of coefficients pertain to an atmospheric layer.
@@ -70,47 +71,55 @@ def pydisort(
70
71
  NT_cor : optional, bool
71
72
  Perform Nakajima-Tanaka intensity corrections?
72
73
  BDRF_Fourier_modes : optional, list
73
- BDRF Fourier modes, each a function with arguments `mu, -mu_p` of type array
74
+ BDRF Fourier modes, each a function with arguments ``mu, -mu_p`` of type array
74
75
  and which output has the same dimensions as the outer product of the two arrays.
75
76
  s_poly_coeffs : optional, array
76
77
  Polynomial coefficients of isotropic internal sources.
77
78
  Arrange coefficients from lowest order term to highest.
78
79
  use_banded_solver_NLayers : optional, int
79
- At or above how many atmospheric layers should `scipy.linalg.solve_banded` be used?
80
+ At or above how many atmospheric layers should ``scipy.linalg.solve_banded`` be used?
81
+ autograd_compatible : optional, bool
82
+ If ``True``, the autograd package: https://github.com/HIPS/autograd can be used to compute
83
+ the tau-derivatives of the function outputs but ``pydisort`` will be less efficient.
80
84
 
81
85
  Returns
82
86
  -------
83
87
  mu_arr : array or float
84
- All `mu` (cosine of polar angle) quadrature nodes.
88
+ All ``mu`` (cosine of polar angle) quadrature nodes.
85
89
  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`.
90
+ (Energetic) Flux function with argument ``tau`` (type: array or float) for positive (upward) ``mu`` values.
91
+ Returns the diffuse flux magnitudes (same type and size as ``tau``).
92
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
93
+ to switch to an antiderivative of the function with respect to ``tau``.
94
+ For example, ``Fp(1, is_antiderivative_wrt_tau=True) - Fp(0, is_antiderivative_wrt_tau=True)``
95
+ will produce the tau-integral over ``[1, 0]`` assuming both values belong to the same layer, and
96
+ ``Fp(tau_arr, is_antiderivative_wrt_tau = True) - Fp(np.insert(tau_arr[:-1] + 1e-15, 0, 0), is_antiderivative_wrt_tau = True)``
97
+ will produce an array of the tau-integral over each layer.
98
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
92
99
  Fm(tau) : function
93
- (Energetic) Flux function with argument `tau` (type: array or float) for negative (downward) `mu` values.
100
+ (Energetic) Flux function with argument ``tau`` (type: array or float) for negative (downward) ``mu`` values.
94
101
  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`.
102
+ same type and size as ``tau``.
103
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
104
+ to switch to an antiderivative of the function with respect to ``tau``.
105
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
98
106
  u0(tau) : function
99
- Zeroth Fourier mode of the intensity with argument `tau` (type: array or float).
100
- Returns an ndarray with axes corresponding to variation with `mu` and `tau` respectively.
107
+ Zeroth Fourier mode of the intensity with argument ``tau`` (type: array or float).
108
+ Returns an ndarray with axes corresponding to variation with ``mu`` and ``tau`` respectively.
101
109
  This function is useful for calculating actinic fluxes and other quantities of interest,
102
110
  but reclassification of delta-scaled flux and other corrections must be done manually
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`.
111
+ (for actinic flux ``subroutines.generate_diff_act_flux_funcs`` will automatically perform the reclassification).
112
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
113
+ to switch to an antiderivative of the function with respect to ``tau``.
114
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
106
115
  u(tau, phi) : function, optional
107
- Intensity function with arguments `(tau, phi)` each of type array or float.
108
- Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
109
- Pass `return_Fourier_error = True` (defaults to `False`) to return the
116
+ Intensity function with arguments ``(tau, phi)`` each of type array or float.
117
+ Returns an ndarray with axes corresponding to variation with ``mu, tau, phi`` respectively.
118
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
119
+ to switch to an antiderivative of the function with respect to ``tau``.
120
+ Pass ``return_Fourier_error = True`` (defaults to ``False``) to return the
110
121
  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`.
113
-
122
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
114
123
  """
115
124
 
116
125
  """
@@ -159,7 +168,7 @@ def pydisort(
159
168
  | `scaled_mu0` | scalar |
160
169
  """
161
170
 
162
- if _autograd_compatible:
171
+ if autograd_compatible:
163
172
  import autograd.numpy as np
164
173
  else:
165
174
  import numpy as np
@@ -201,50 +210,71 @@ def pydisort(
201
210
  # Input checks (refer to Section 1 of the Comprehensive Documentation)
202
211
  # --------------------------------------------------------------------------------------------------------------------------
203
212
  # Optical depths and thickness must be positive
204
- assert np.all(tau_arr > 0)
205
- assert np.all(thickness_arr > 0)
213
+ if not np.all(tau_arr > 0):
214
+ raise ValueError("tau values cannot be negative.")
215
+ if not np.all(thickness_arr > 0):
216
+ raise ValueError("Layer thickness cannot be negative.")
206
217
  # Single-scattering albedo must be between 0 and 1, excluding 1
207
- assert np.all(omega_arr >= 0)
208
- assert np.all(omega_arr < 1)
218
+ if not (np.all(omega_arr >= 0) and np.all(omega_arr < 1)):
219
+ raise ValueError("Single-scattering albedo must be between 0 and 1, excluding 1.")
209
220
  # There must be a positive number of Legendre coefficients each with magnitude <= 1
210
221
  # The user must supply at least as many phase function Legendre coefficients as intended for use
211
- assert NLeg > 0
212
- assert np.all(Leg_coeffs_all[:, 0] == 1)
213
- assert np.all(0 <= Leg_coeffs_all) and np.all(Leg_coeffs_all <= 1)
214
- assert np.all(np.array([f.__code__.co_argcount for f in BDRF_Fourier_modes]) == 2)
215
- assert NLeg <= NLeg_all
222
+ if not NLeg > 0:
223
+ raise ValueError("The number of phase function Legendre coefficients must be positive.")
224
+ if not np.all(Leg_coeffs_all[:, 0] == 1):
225
+ raise ValueError("The first phase function Legendre coefficient must equal 1.")
226
+ if not (np.all(0 <= Leg_coeffs_all) and np.all(Leg_coeffs_all <= 1)):
227
+ raise ValueError("The phase function Legendre coefficients must all be between 0 and 1.")
228
+ if not np.all(np.array([f.__code__.co_argcount for f in BDRF_Fourier_modes]) == 2):
229
+ raise ValueError("Some functions in the list `BDRF_Fourier_modes` are of the wrong form.")
230
+ if not NLeg <= NLeg_all:
231
+ raise ValueError("`NLeg` cannot be larger than the number of phase function Legendre coefficients provided.")
216
232
  # Ensure that the first dimension of the following inputs corresponds to the number of layers
217
- assert np.shape(Leg_coeffs_all)[0] == NLayers
218
- assert len(omega_arr) == NLayers
219
- if len(f_arr) != 1 or f_arr[0] != 0:
220
- assert len(f_arr) == NLayers
221
- if there_is_iso_source:
222
- assert np.shape(s_poly_coeffs)[0] == NLayers
233
+ if not np.shape(Leg_coeffs_all)[0] == NLayers:
234
+ raise ValueError("The zeroth dimension of the shape of `Leg_coeffs_all` does not match the number of layers as deduced from the length of `tau_arr`.")
235
+ if not len(omega_arr) == NLayers:
236
+ raise ValueError("The zeroth dimension of the shape of `omega_arr` does not match the number of layers as deduced from the length of `tau_arr`.")
237
+ if np.any(f_arr != 0) and not len(f_arr) == NLayers:
238
+ raise ValueError("The length of `f_arr` does not match the number of layers as deduced from the length of `tau_arr`.")
239
+ if there_is_iso_source and not np.shape(s_poly_coeffs)[0] == NLayers:
240
+ raise ValueError("The zeroth dimension of the shape of `s_poly_coeffs` does not match the number of layers as deduced from the length of `tau_arr`.")
223
241
  # Conditions on the number of quadrature angles (NQuad), Legendre coefficients (NLeg) and loops (NFourier)
224
- assert NQuad >= 2
225
- assert NQuad % 2 == 0
226
- assert NFourier > 0
227
- assert NFourier <= NLeg
242
+ if not NQuad >= 2:
243
+ raise ValueError("There must be at least two streams.")
244
+ if not NQuad % 2 == 0:
245
+ raise ValueError("The number of streams must be even.")
246
+ if not NFourier > 0:
247
+ raise ValueError("The number of Fourier modes to use in the solution must be positive.")
248
+ if not NFourier <= NLeg:
249
+ raise ValueError("The number of Fourier modes to use in the solution must be less than or equal to the number of phase function Legendre coefficients used.")
250
+ if NFourier > 64 and not only_flux:
251
+ warnings.warn("`NFourier` is large and may cause errors, consider decreasing `NFourier` to 64 and it probably should be even less.")
228
252
  # Not strictly necessary but there will be tremendous inaccuracies if this is violated
229
- assert NLeg <= NQuad
253
+ if not NLeg <= NQuad:
254
+ raise ValueError("There should be more streams than the number of phase function Legendre coefficients used.")
230
255
  # We require principal angles and a downward incident beam
231
- assert I0 >= 0
256
+ if not I0 >= 0:
257
+ raise ValueError("The incident beam must have positive intensity.")
232
258
  if there_is_beam_source:
233
- assert 0 < mu0 and mu0 <= 1
234
- assert 0 <= phi0 and phi0 < 2 * pi
259
+ if not 0 < mu0 and mu0 <= 1:
260
+ raise ValueError("The cosine of the polar angle of the incident beam must be between 0 and 1, excluding 0.")
261
+ if not 0 <= phi0 and phi0 < 2 * pi:
262
+ raise ValueError("Provide the principal azimuthal angle for the incident beam (must be between 0 and 2pi, excluding 2pi).")
235
263
  # Ensure that the BC inputs are of the correct shape
236
264
  if len(np.atleast_1d(b_pos)) == 1:
237
265
  b_pos_is_scalar = True
238
- else:
239
- assert np.shape(b_pos) == (N, NFourier)
266
+ elif not np.shape(b_pos) == (N, NFourier):
267
+ raise ValueError("The shape of the bottom of atmosphere boundary conditions must equal (NQuad // 2, NFourier).")
240
268
  if len(np.atleast_1d(b_neg)) == 1:
241
269
  b_neg_is_scalar = True
242
- else:
243
- assert np.shape(b_neg) == (N, NFourier)
270
+ elif not np.shape(b_neg) == (N, NFourier):
271
+ raise ValueError("The shape of the top of atmosphere boundary conditions must equal (NQuad // 2, NFourier).")
244
272
  # The fractional scattering must be between 0 and 1
245
- assert np.all(0 <= f_arr) and np.all(f_arr <= 1)
273
+ if not (np.all(0 <= f_arr) and np.all(f_arr <= 1)):
274
+ raise ValueError("The fractional scattering must be between 0 and 1.")
246
275
  # The minimum threshold is 3 else the matrix will not be banded
247
- assert(use_banded_solver_NLayers >= 3)
276
+ if not use_banded_solver_NLayers >= 3:
277
+ raise ValueError("The minimum threshold `use_banded_solver_NLayers` is 3, else the matrix will not be banded.")
248
278
  # --------------------------------------------------------------------------------------------------------------------------
249
279
 
250
280
  # Some more setup
@@ -268,7 +298,8 @@ def pydisort(
268
298
  M_inv = 1 / mu_arr_pos
269
299
 
270
300
  # We do not allow mu0 to equal a quadrature / computational angle
271
- assert not np.any(np.abs(mu_arr_pos - mu0) < 1e-8)
301
+ if np.any(np.abs(mu_arr_pos - mu0) < 1e-8):
302
+ raise ValueError("Some quadrature angles come too close to `mu0`. Perturb `NQuad` or `mu0` to rectify this error.")
272
303
  # --------------------------------------------------------------------------------------------------------------------------
273
304
 
274
305
  # Delta-M scaling; there is no scaling if f = 0
@@ -317,7 +348,7 @@ def pydisort(
317
348
  scale_tau,
318
349
  only_flux,
319
350
  use_banded_solver_NLayers,
320
- _autograd_compatible,
351
+ autograd_compatible,
321
352
  )
322
353
 
323
354
  # TMS correction for the intensity (see Section 3.7.2)
@@ -527,7 +558,7 @@ def pydisort(
527
558
  axis=1,
528
559
  )
529
560
 
530
- if _autograd_compatible:
561
+ if autograd_compatible:
531
562
  if not any_pos_contribution:
532
563
  TMS_correction_pos = np.zeros((N, Ntau, Nphi))
533
564
  if not any_neg_contribution:
@@ -579,6 +610,7 @@ def pydisort(
579
610
  (tau[None, :] - 1 / x[:, None]) * np.exp(-tau / scaled_mu0)[None, :]
580
611
  + np.exp(-tau[None, :] / mu_arr_pos[:, None]) / x[:, None]
581
612
  ) / (mu_arr_pos * scaled_mu0 * x)[:, None]
613
+
582
614
  return (
583
615
  I0
584
616
  / (4 * pi)
@@ -593,26 +625,25 @@ def pydisort(
593
625
 
594
626
  # The corrected intensity
595
627
  # --------------------------------------------------------------------------------------------------------------------------
596
- def u_corrected(tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
628
+ def u_corrected(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
597
629
  tau = np.atleast_1d(tau)
598
630
  phi = np.atleast_1d(phi)
599
631
  NT_corrections = TMS_correction(tau, phi, is_antiderivative_wrt_tau)
600
632
 
601
- if _autograd_compatible:
633
+ if autograd_compatible:
602
634
  NT_corrections = NT_corrections + np.concatenate(
603
635
  [np.zeros((N, len(tau), len(phi))), IMS_correction(tau, phi, is_antiderivative_wrt_tau)], axis=0
604
636
  )
605
637
  else:
606
638
  NT_corrections[N:, :, :] += IMS_correction(tau, phi, is_antiderivative_wrt_tau)
607
-
608
- if return_Fourier_error:
609
- u_star_outputs = u_star(tau, phi, True, is_antiderivative_wrt_tau)
639
+
640
+ if return_Fourier_error or return_tau_arr:
641
+ u_star_outputs = u_star(tau, phi, is_antiderivative_wrt_tau, return_Fourier_error, return_tau_arr)
610
642
  return (
611
643
  u_star_outputs[0] + I0_orig * np.squeeze(NT_corrections),
612
- u_star_outputs[1],
613
- )
644
+ ) + u_star_outputs[1:]
614
645
  else:
615
- return u_star(tau, phi, False, is_antiderivative_wrt_tau) + I0_orig * np.squeeze(NT_corrections)
646
+ return u_star(tau, phi, is_antiderivative_wrt_tau) + I0_orig * np.squeeze(NT_corrections)
616
647
  # --------------------------------------------------------------------------------------------------------------------------
617
648
 
618
649
  return mu_arr, flux_up, flux_down, u0, u_corrected
@@ -641,5 +672,5 @@ def pydisort(
641
672
  scale_tau,
642
673
  only_flux,
643
674
  use_banded_solver_NLayers,
644
- _autograd_compatible,
675
+ autograd_compatible,
645
676
  )
@@ -28,6 +28,7 @@ def transform_interval(arr, c, d, a, b):
28
28
  return (((arr - a) * (d - c)) / (b - a)) + c
29
29
 
30
30
 
31
+
31
32
  def transform_weights(weights, c, d, a, b):
32
33
  """Transforms an array of quadrature weights from interval [a, b] to [c, d].
33
34
 
@@ -53,9 +54,10 @@ def transform_weights(weights, c, d, a, b):
53
54
  return weights * (d - c) / (b - a)
54
55
 
55
56
 
57
+
56
58
  def calculate_nu(mu, phi, mu_p, phi_p):
57
- """Calculates the cosine of the scattering angle `nu`
58
- between incident angle `(mu_p, phi_p)` and scattering angle `(mu, phi)`.
59
+ """Calculates the cosine of the scattering angle ``nu``
60
+ between incident angle ``(mu_p, phi_p)`` and scattering angle ``(mu, phi)``.
59
61
 
60
62
  Parameters
61
63
  ----------
@@ -71,7 +73,7 @@ def calculate_nu(mu, phi, mu_p, phi_p):
71
73
  Returns
72
74
  -------
73
75
  nu : ndarray
74
- Cosine of scattering angles which axes capture variation with `mu, phi, mu_p, phi_p` respectively.
76
+ Cosine of scattering angles which axes capture variation with ``mu, phi, mu_p, phi_p`` respectively.
75
77
 
76
78
  """
77
79
  mu, phi, mu_p, phi_p = np.atleast_1d(mu, phi, mu_p, phi_p)
@@ -83,12 +85,13 @@ def calculate_nu(mu, phi, mu_p, phi_p):
83
85
  return np.squeeze(nu)
84
86
 
85
87
 
88
+
86
89
  def Gauss_Legendre_quad(N, c=0, d=1):
87
90
  """Generates Gauss-Legendre quadrature zero points and weights for integration from c to d.
88
91
 
89
92
  Parameters
90
93
  ----------
91
- N : int
94
+ N : int (will be converted to int)
92
95
  Number of quadrature nodes.
93
96
  c : float, optional
94
97
  Start of integration interval.
@@ -108,6 +111,7 @@ def Gauss_Legendre_quad(N, c=0, d=1):
108
111
  return transform_interval(mu_arr_pos, c, d, -1, 1), transform_weights(W, c, d, -1, 1)
109
112
 
110
113
 
114
+
111
115
  def Clenshaw_Curtis_quad(Nphi, c=0, d=(2 * pi)):
112
116
  """Generates Clenshaw-Curtis quadrature zero points and weights for integration from c to d.
113
117
 
@@ -129,8 +133,8 @@ def Clenshaw_Curtis_quad(Nphi, c=0, d=(2 * pi)):
129
133
 
130
134
  """
131
135
  # Ensure that the number of nodes is odd and greater than 2
132
- assert Nphi > 2
133
- assert Nphi % 2 == 1
136
+ if not (Nphi > 2 and Nphi % 2 == 1):
137
+ raise ValueError("The number of quadrature nodes must be odd and greater than 2.")
134
138
 
135
139
  Nphi -= 1 # The extra index corresponds to the point 0 which we will add later
136
140
  Nphi_pos = Nphi // 2
@@ -144,9 +148,10 @@ def Clenshaw_Curtis_quad(Nphi, c=0, d=(2 * pi)):
144
148
  return transform_interval(phi_arr, c, d, -1, 1), transform_weights(full_weights_phi, c, d, -1, 1)
145
149
 
146
150
 
151
+
147
152
  def generate_FD_mat(Ntau, a, b):
148
153
  """Generates a sparse first derivative central difference (second-order accuracy)
149
- matrix on [a,b] in `csr` format with `Ntau` grid points.
154
+ matrix on [a,b] in ``csr`` format with ``Ntau`` grid points.
150
155
  Second order forward or backward differences are used at the boundaries.
151
156
 
152
157
  Parameters
@@ -183,6 +188,7 @@ def generate_FD_mat(Ntau, a, b):
183
188
  return tau_arr, first_deriv.tocsr()
184
189
 
185
190
 
191
+
186
192
  def atleast_2d_append(*arys):
187
193
  """View inputs as arrays with at least two dimensions.
188
194
  Dimensions are added as necessary to the back of the shape tuple rather than to the front.
@@ -219,7 +225,8 @@ def atleast_2d_append(*arys):
219
225
  return res[0]
220
226
  else:
221
227
  return res
222
-
228
+
229
+
223
230
 
224
231
  def generate_diff_act_flux_funcs(u0):
225
232
  """Generates the up and down diffuse actinic flux functions respectively.
@@ -237,73 +244,95 @@ def generate_diff_act_flux_funcs(u0):
237
244
  Returns
238
245
  -------
239
246
  Fp_act(tau) : function
240
- Actinic flux function with argument `tau` (type: array) for positive (upward) `mu` values.
247
+ Actinic flux function with argument ``tau`` (type: array) for positive (upward) ``mu`` values.
241
248
  Returns the diffuse flux magnitudes (type: array).
242
- Pass `is_antiderivative_wrt_tau = True` (defaults to `False`)
243
- to switch to an antiderivative of the function with respect to `tau`.
249
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
250
+ to switch to an antiderivative of the function with respect to ``tau``.
251
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
244
252
  Fm_act(tau) : function
245
- Actinic flux function with argument `tau` (type: array) for negative (downward) `mu` values.
253
+ Actinic flux function with argument ``tau`` (type: array) for negative (downward) ``mu`` values.
246
254
  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`.
255
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
256
+ to switch to an antiderivative of the function with respect to ``tau``.
257
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
249
258
 
250
259
  """
251
- N = np.shape(u0(0))[0] // 2
260
+ N = len(u0(0)) // 2
252
261
  GL_weights = Gauss_Legendre_quad(N, 0, 1)[1]
253
262
 
254
263
  # Note that the zeroth axis of the array u0(tau) captures variation with mu
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])[()]
264
+ def flux_act_up(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
265
+ if return_tau_arr:
266
+ (
267
+ u0_cache,
268
+ tau_arr,
269
+ ) = u0(tau, is_antiderivative_wrt_tau, True)
270
+ return np.squeeze(2 * pi * GL_weights @ u0_cache[:N])[()], tau_arr
271
+ else:
272
+ return np.squeeze(2 * pi * GL_weights @ u0(tau, is_antiderivative_wrt_tau)[:N])[()]
257
273
 
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)
260
- result_without_reclassification = 2 * pi * GL_weights @ u0_cache[N:]
261
- return np.squeeze(result_without_reclassification + act_dscale_reclassification)[()]
274
+ def flux_act_down_diffuse(tau, is_antiderivative_wrt_tau=False, return_tau_arr=False):
275
+ if return_tau_arr:
276
+ (
277
+ u0_cache,
278
+ tau_arr,
279
+ act_dscale_reclassification,
280
+ ) = u0(tau, is_antiderivative_wrt_tau, True, _return_act_dscale_for_reclass=True)
281
+ result_without_reclassification = 2 * pi * GL_weights @ u0_cache[N:]
282
+ return np.squeeze(result_without_reclassification + act_dscale_reclassification)[()], tau_arr
283
+ else:
284
+ (
285
+ u0_cache,
286
+ act_dscale_reclassification,
287
+ ) = u0(tau, is_antiderivative_wrt_tau, False, _return_act_dscale_for_reclass=True)
288
+ result_without_reclassification = 2 * pi * GL_weights @ u0_cache[N:]
289
+ return np.squeeze(result_without_reclassification + act_dscale_reclassification)[()]
262
290
 
263
291
  return flux_act_up, flux_act_down_diffuse
264
292
 
265
293
 
266
- def interpolate(mu_arr_pos, u):
294
+
295
+ def interpolate(u):
267
296
  """Polynomial (Barycentric) interpolation with respect to mu. The output
268
297
  is a function that is continuous and variable in all three arguments: mu, tau and phi.
269
298
  Discussed in sections 3.7 and 6.3 in the Comprehensive Documentation.
270
299
 
271
300
  Parameters
272
301
  ----------
273
- mu_arr_pos : array
274
- Positive `mu` (cosine of polar angle) quadrature nodes.
275
- Equivalent to `mu_arr[:N]`.
276
302
  u : function
277
- Non-interpolated intensity function as outputted by `pydisort`.
303
+ Non-interpolated intensity function as given by ``pydisort``.
278
304
 
279
305
  Returns
280
306
  -------
281
307
  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
308
+ Intensity function with arguments ``(mu, tau, phi)`` each of type array or float.
309
+ Returns an ndarray with axes corresponding to variation with ``mu, tau, phi`` respectively.
310
+ Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
311
+ to switch to an antiderivative of the function with respect to ``tau``.
312
+ Pass ``return_Fourier_error = True`` (defaults to ``False``) to return the
285
313
  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`.
314
+ Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
288
315
 
289
316
  """
290
- N = len(mu_arr_pos)
317
+ N = len(u(0, 0)) // 2
318
+ mu_arr_pos = Gauss_Legendre_quad(N)[0]
319
+
291
320
  u_pos_interpol = sc.interpolate.BarycentricInterpolator(mu_arr_pos)
292
321
  u_neg_interpol = sc.interpolate.BarycentricInterpolator(-mu_arr_pos)
293
322
 
294
- def u_interpol(mu, tau, phi, return_Fourier_error=False, is_antiderivative_wrt_tau=False):
295
- assert np.all(np.abs(mu) <= 1)
323
+ def u_interpol(mu, tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
324
+ if not np.all(np.abs(mu) <= 1):
325
+ raise ValueError("mu values must be between -1 and 1.")
326
+
296
327
  mu = np.atleast_1d(mu)
297
328
 
298
- if return_Fourier_error:
299
- u_cache, Fourier_error = u(tau, phi, True, is_antiderivative_wrt_tau)
329
+ if return_Fourier_error or return_tau_arr:
330
+ u_outputs = u(tau, phi, is_antiderivative_wrt_tau, return_Fourier_error, return_tau_arr)
331
+ u_cache = u_outputs[0]
300
332
  else:
301
- u_cache = u(tau, phi, False, is_antiderivative_wrt_tau)
333
+ u_cache = u(tau, phi, is_antiderivative_wrt_tau)
302
334
 
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:])
335
+ results = np.empty((len(mu),) + np.shape(u_cache)[1:])
307
336
  mask_pos = mu > 0
308
337
  mask_else = ~mask_pos
309
338
 
@@ -314,17 +343,18 @@ def interpolate(mu_arr_pos, u):
314
343
  u_neg_interpol.set_yi(u_cache[N:])
315
344
  results[mask_else] = u_neg_interpol(mu[mask_else])
316
345
 
317
- if return_Fourier_error:
318
- return np.squeeze(results)[()], Fourier_error
346
+ if return_Fourier_error or return_tau_arr:
347
+ return (np.squeeze(results)[()],) + u_outputs[1:]
319
348
  else:
320
349
  return np.squeeze(results)[()]
321
350
 
322
351
  return u_interpol
323
-
352
+
353
+
324
354
 
325
355
  def to_diag_ordered_form(A, sym_offset):
326
356
  """
327
- Convert a matrix A to the diagonal ordered form required by `scipy.linalg.solve_banded`.
357
+ Convert a matrix A to the diagonal ordered form required by ``scipy.linalg.solve_banded``.
328
358
  We assume that the matrix has the same number of super- and sub-diagonals.
329
359
 
330
360
  Parameters
@@ -356,6 +386,7 @@ def to_diag_ordered_form(A, sym_offset):
356
386
  axis=0,
357
387
  )
358
388
 
389
+
359
390
 
360
391
  def _mathscr_v(tau, # Input optical depths
361
392
  l, # Layer index of each input optical depth
@@ -366,40 +397,40 @@ def _mathscr_v(tau, # Input optical depths
366
397
  G_inv, # Inverse of eigenvector matrix
367
398
  mu_arr, # Quadrature nodes for both hemispheres
368
399
  is_antiderivative_wrt_tau=False, # Switch to an antiderivative of the function?
369
- _autograd_compatible=False, # Should the output functions be compatible with autograd?
400
+ autograd_compatible=False, # Should the output functions be compatible with autograd?
370
401
  ):
371
402
  """Particular solution for isotropic internal sources.
372
403
  Refer to Section 3.6.1 of the Comprehensive Documentation.
373
404
  It has many seemingly redundant arguments to maximize
374
- precomputation in the `_assemble_intensity_and_fluxes`
375
- and `_solve_for_coeffs` functions which call it.
405
+ precomputation in the ``_assemble_intensity_and_fluxes``
406
+ and ``_solve_for_coeffs`` functions which call it.
376
407
 
377
408
  Arguments of _mathscr_v
378
409
  | Variable | Shape |
379
410
  | --------------------------- | ------------------------------------- |
380
- | `tau` | `Ntau` |
381
- | `l` | `Ntau` |
382
- | `Nscoeffs` | scalar |
383
- | `s_poly_coeffs` | `NLayers x Nscoeffs` or `Nscoeffs` |
384
- | `G` | `NLayers<= x NQuad x NQuad` |
385
- | `K` | `NLayers<= x NQuad` |
386
- | `G_inv` | `NLayers<= x NQuad x NQuad` or `None` |
387
- | `mu_arr` | `NQuad` |
388
- | `is_antiderivative_wrt_tau` | boolean |
389
- | `_autograd_compatible` | boolean |
411
+ | ``tau`` | ``Ntau`` |
412
+ | ``l`` | ``Ntau`` |
413
+ | ``Nscoeffs`` | scalar |
414
+ | ``s_poly_coeffs`` | ``NLayers x Nscoeffs`` or ``Nscoeffs`` |
415
+ | ``G`` | ``NLayers<= x NQuad x NQuad`` |
416
+ | ``K`` | ``NLayers<= x NQuad`` |
417
+ | ``G_inv`` | ``NLayers<= x NQuad x NQuad`` or ``None`` |
418
+ | ``mu_arr`` | ``NQuad`` |
419
+ | ``is_antiderivative_wrt_tau`` | boolean |
420
+ | ``autograd_compatible`` | boolean |
390
421
 
391
422
  Notable internal variables of _mathscr_v
392
423
  | Variable | Shape |
393
424
  | ---------------- | ----------------------------------- |
394
- | i_arr | `Nscoeffs` |
395
- | i_arr_repeat | `Nscoeffs*(Nscoeffs+1)/2` | Using triangular number formula
396
- | j_arr | `Nscoeffs*(Nscoeffs+1)/2` | Using triangular number formula
397
- | s_poly_coeffs_nj | `NLayers x Nscoeffs*(Nscoeffs+1)/2` |
398
- | OUTPUT | `NQuad x Ntau` |
425
+ | i_arr | ``Nscoeffs`` |
426
+ | i_arr_repeat | ``Nscoeffs*(Nscoeffs+1)/2`` | Using triangular number formula
427
+ | j_arr | ``Nscoeffs*(Nscoeffs+1)/2`` | Using triangular number formula
428
+ | s_poly_coeffs_nj | ``NLayers x Nscoeffs*(Nscoeffs+1)/2`` |
429
+ | OUTPUT | ``NQuad x Ntau`` |
399
430
  """
400
431
  n = Nscoeffs - 1
401
432
 
402
- if _autograd_compatible:
433
+ if autograd_compatible:
403
434
  import autograd.numpy as np
404
435
 
405
436
  def mathscr_b(i):
@@ -407,9 +438,9 @@ def _mathscr_v(tau, # Input optical depths
407
438
  Notable internal variables of mathscr_b
408
439
  | Variable | Shape |
409
440
  | ---------------- | ------------------------------------- |
410
- | j_arr | `i + 1` |
411
- | s_poly_coeffs_nj | `i + 1` |
412
- | OUTPUT | `Nscoeffs x (i + 1) x NQuad` |
441
+ | j_arr | ``i + 1`` |
442
+ | s_poly_coeffs_nj | ``i + 1`` |
443
+ | OUTPUT | ``Nscoeffs x (i + 1) x NQuad`` |
413
444
  """
414
445
 
415
446
  j_arr = np.arange(i + 1)
@@ -460,6 +491,7 @@ def _mathscr_v(tau, # Input optical depths
460
491
  )
461
492
 
462
493
 
494
+
463
495
  def _compare(results, mu_to_compare, reorder_mu, flux_up, flux_down, u):
464
496
  """Performs pointwise comparisons between results from Stamnes' DISORT,
465
497
  which are stored in .npz files, against results from PythonicDISORT. Used in our PyTests.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PythonicDISORT
3
- Version: 0.9.1
3
+ Version: 0.9.3
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
File without changes
File without changes