PythonicDISORT 1.7__py3-none-any.whl → 1.8__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.
@@ -29,7 +29,8 @@ def _assemble_intensity_and_fluxes(
29
29
  only_flux, # Only compute fluxes?
30
30
  use_banded_solver_NLayers, # Number of layers above or equal which to use `scipy.linalg.solve_banded`
31
31
  autograd_compatible, # Should the output functions be compatible with autograd?
32
- ):
32
+ cache_asso_leg, # Assoc. Legendre table cache mode: None, "no_mu0" or "mu0"
33
+ ):
33
34
  """Assembles the solution functions: intensity `u`, upward flux `flux_up`, downward flux `flux_down`
34
35
  from the previously solved eigenpairs and coefficients from the boundary conditions.
35
36
  Returns these solution functions.
@@ -78,7 +79,8 @@ def _assemble_intensity_and_fluxes(
78
79
  | `only_flux` | boolean |
79
80
  | `use_banded_solver_NLayers` | scalar |
80
81
  | `autograd_compatible` | boolean |
81
-
82
+ | `cache_asso_leg` | None, "no_mu0" or "mu0" |
83
+
82
84
  Notable internal variables of _assemble_intensity_and_fluxes
83
85
  | Variable | Type / Shape |
84
86
  | ----------------- | -------------------------------------- |
@@ -117,6 +119,7 @@ def _assemble_intensity_and_fluxes(
117
119
  mu0, I0_div_4pi,
118
120
  there_is_beam_source,
119
121
  there_is_iso_source,
122
+ cache_asso_leg,
120
123
  )
121
124
  if there_is_beam_source and there_is_iso_source:
122
125
  G_collect, K_collect, B_collect, G_inv_collect_0 = outputs
@@ -161,8 +164,66 @@ def _assemble_intensity_and_fluxes(
161
164
  G_collect_0 = G_collect[0, :, :, :]
162
165
  K_collect_0 = K_collect[0, :, :]
163
166
  GC_collect_0 = GC_collect[0, :, :, :]
164
- # --------------------------------------------------------------------------------------------------------------------------
165
-
167
+ # --------------------------------------------------------------------------------------------------------------------------
168
+
169
+ # Value-keyed cache for the most recent `tau` evaluation grid, shared across the output
170
+ # functions `u`, `u0`, `flux_up` and `flux_down` (evaluating several outputs on the same
171
+ # grid, e.g. `flux_up(tau); flux_down(tau)`, then recomputes nothing).
172
+ # Stores a copy of `tau` (compared by value like the cache in `subroutines.interpolate`)
173
+ # with the derived layer indices and delta-scaled `tau`, and, lazily, the exponentials of
174
+ # the homogeneous-solution exponents for the 0th mode ("exp0") and for all modes ("expm").
175
+ # The cache is bypassed when `autograd_compatible` as it would break gradient tracing.
176
+ # --------------------------------------------------------------------------------------------------------------------------
177
+ is_delta_scaled = np.any(scale_tau != 1)
178
+ _tau_cache = {"tau": None, "l": None, "scaled_tau": None, "exp0": None, "expm": None}
179
+
180
+ def _process_tau(tau):
181
+ """Layer indices and delta-scaled `tau` for the evaluation grid `tau`, cached."""
182
+ if (not autograd_compatible and _tau_cache["tau"] is not None
183
+ and np.array_equal(tau, _tau_cache["tau"])):
184
+ return _tau_cache["l"], _tau_cache["scaled_tau"]
185
+ # Atmospheric layer indices: smallest l with tau <= tau_arr[l]
186
+ l = np.searchsorted(tau_arr, tau, side="left")
187
+ if is_delta_scaled:
188
+ scaled_tau = scaled_tau_arr_with_0[1:][l] - (tau_arr[l] - tau) * scale_tau[l]
189
+ else:
190
+ scaled_tau = tau
191
+ if not autograd_compatible:
192
+ _tau_cache.update(
193
+ tau=np.array(tau, copy=True), l=l, scaled_tau=scaled_tau, exp0=None, expm=None
194
+ )
195
+ return l, scaled_tau
196
+
197
+ def _exp_exponent(l, scaled_tau, all_modes):
198
+ """exp of the homogeneous-solution exponents for the (cached) evaluation grid.
199
+ Returns shape (Ntau, NQuad) for the 0th mode, or (NFourier, Ntau, NQuad) for all modes."""
200
+ key = "expm" if all_modes else "exp0"
201
+ if not autograd_compatible and _tau_cache[key] is not None:
202
+ return _tau_cache[key]
203
+ scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
204
+ scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
205
+ if all_modes:
206
+ exponent = np.concatenate(
207
+ [
208
+ K_collect[:, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[None, :, None],
209
+ K_collect[:, l, N:] * (scaled_tau - scaled_tau_arr_l)[None, :, None],
210
+ ],
211
+ axis=-1,
212
+ )
213
+ else:
214
+ exponent = np.concatenate(
215
+ [
216
+ K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
217
+ K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
218
+ ],
219
+ axis=-1,
220
+ )
221
+ result = np.exp(exponent)
222
+ if not autograd_compatible:
223
+ _tau_cache[key] = result
224
+ return result
225
+ # --------------------------------------------------------------------------------------------------------------------------
226
+
166
227
  if not only_flux:
167
228
 
168
229
  # Construct the intensity function (refer to section 3.7 of the Comprehensive Documentation)
@@ -181,27 +242,10 @@ def _assemble_intensity_and_fluxes(
181
242
  if np.any(tau < 0) or np.any(tau > tau_arr[-1]):
182
243
  raise ValueError("tau input outside the tau range specified for the atmosphere (check `tau_arr`).")
183
244
 
184
- # Atmospheric layer indices
185
- l = np.searchsorted(tau_arr, tau, side="left")
186
- scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
187
- scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
245
+ # Atmospheric layer indices, delta-M scaling and exponentials (cached)
246
+ l, scaled_tau = _process_tau(tau)
247
+ exp_exponent = _exp_exponent(l, scaled_tau, all_modes=True)
188
248
 
189
- # Delta-M scaling
190
- if np.any(scale_tau != 1):
191
- tau_dist_from_top = tau_arr[l] - tau
192
- scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
193
- scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
194
- else:
195
- scaled_tau = tau
196
-
197
- exponent = np.concatenate(
198
- [
199
- K_collect[:, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[None, :, None],
200
- K_collect[:, l, N:] * (scaled_tau - scaled_tau_arr_l)[None, :, None],
201
- ],
202
- axis=-1,
203
- )
204
-
205
249
  direct_beam_contribution = 0
206
250
  if is_antiderivative_wrt_tau:
207
251
  if there_is_beam_source:
@@ -213,7 +257,7 @@ def _assemble_intensity_and_fluxes(
213
257
  np.einsum(
214
258
  "mtij, mtj -> mit",
215
259
  GC_collect[:, l, :, :],
216
- np.exp(exponent) / (scale_tau[None, :, None] * K_collect)[:, l, :],
260
+ exp_exponent / (scale_tau[None, :, None] * K_collect)[:, l, :],
217
261
  optimize=True,
218
262
  )
219
263
  + direct_beam_contribution
@@ -227,7 +271,7 @@ def _assemble_intensity_and_fluxes(
227
271
 
228
272
  um = (
229
273
  np.einsum(
230
- "mtij, mtj -> mit", GC_collect[:, l, :, :], np.exp(exponent), optimize=True
274
+ "mtij, mtj -> mit", GC_collect[:, l, :, :], exp_exponent, optimize=True
231
275
  )
232
276
  + direct_beam_contribution
233
277
  )
@@ -254,49 +298,52 @@ def _assemble_intensity_and_fluxes(
254
298
  else:
255
299
  um[0, :, :] += _mathscr_v_contribution
256
300
 
257
- u = np.sum(
258
- um[:, :, :, None]
259
- * np.cos(np.arange(NFourier)[:, None] * (phi0 - phi)[None, :])[:, None, None, :],
260
- axis=0,
301
+ # Contract over the Fourier modes directly: materializing the
302
+ # (NFourier, NQuad, Ntau, Nphi) product only to sum over its first axis
303
+ # is substantially slower and heavier for large NFourier * Nphi
304
+ u = np.einsum(
305
+ "mit, mp -> itp",
306
+ um,
307
+ np.cos(np.arange(NFourier)[:, None] * (phi0 - phi)[None, :]),
308
+ optimize=True,
261
309
  )
262
310
 
263
311
  outputs = (rescale_factor * np.squeeze(u),)
264
312
  output_is_tuple = False
265
313
 
266
314
  if return_Fourier_error:
267
- exponent = np.concatenate(
268
- [
269
- K_collect[-1, l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
270
- K_collect[-1, l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
271
- ],
272
- axis=-1,
273
- )
274
-
315
+ # The last Fourier mode's exponentials are the last slice of the cached tensor
316
+ exp_exponent_last = exp_exponent[-1]
317
+
275
318
  direct_beam_contribution = 0
276
319
  if is_antiderivative_wrt_tau:
277
320
  if there_is_beam_source:
321
+ # `B_collect[-1].T` has shape (NQuad, NLayers): the last Fourier mode's
322
+ # particular-solution coefficients (`B_collect.T[-1]` would wrongly
323
+ # index the last quadrature node)
278
324
  direct_beam_contribution = (
279
- B_collect.T[-1, :, :] / (-scale_tau / mu0)[None, :]
325
+ B_collect[-1].T / (-scale_tau / mu0)[None, :]
280
326
  )[:, l] * np.exp(-scaled_tau / mu0)[None, :]
281
327
 
282
328
  ulast = (
283
329
  np.einsum(
284
330
  "tij, tj -> it",
285
331
  GC_collect[-1, l, :, :],
286
- np.exp(exponent) / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
332
+ exp_exponent_last / (scale_tau[:, None] * K_collect[-1, :, :])[l, :],
287
333
  optimize=True,
288
334
  )
289
335
  + direct_beam_contribution
290
336
  )
291
337
  else:
292
338
  if there_is_beam_source:
339
+ # See the shape note in the `is_antiderivative_wrt_tau` branch above
293
340
  direct_beam_contribution = (
294
- B_collect.T[-1, :, l] * np.exp(-scaled_tau / mu0)[None, :]
341
+ B_collect[-1].T[:, l] * np.exp(-scaled_tau / mu0)[None, :]
295
342
  )
296
343
 
297
344
  ulast = (
298
345
  np.einsum(
299
- "tij, tj -> it", GC_collect[-1, l, :, :], np.exp(exponent), optimize=True
346
+ "tij, tj -> it", GC_collect[-1, l, :, :], exp_exponent_last, optimize=True
300
347
  )
301
348
  + direct_beam_contribution
302
349
  )
@@ -339,41 +386,25 @@ def _assemble_intensity_and_fluxes(
339
386
  if np.any(tau < 0) or np.any(tau > tau_arr[-1]):
340
387
  raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
341
388
 
342
- # Atmospheric layer indices
343
- l = np.searchsorted(tau_arr, tau, side="left")
344
- scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
345
- scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
389
+ # Atmospheric layer indices, delta-M scaling and exponentials (cached)
390
+ l, scaled_tau = _process_tau(tau)
391
+ exp_exponent = _exp_exponent(l, scaled_tau, all_modes=False)
346
392
 
347
- # Delta-M scaling
348
- if np.any(scale_tau != 1):
349
- tau_dist_from_top = tau_arr[l] - tau
350
- scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
351
- scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
352
- # The following complements the function `subroutines.generate_diff_act_flux_funcs`
353
- # in performing the reclassification of delta-scaled actinic flux
354
- if _return_act_dscale_for_reclass:
355
- if is_antiderivative_wrt_tau:
356
- act_dscale_reclassification = (
357
- I0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0)
358
- - I0 * np.exp(-tau / mu0) * -mu0
359
- )
360
- else:
361
- act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
362
- else:
363
- scaled_tau = tau
364
- if _return_act_dscale_for_reclass:
393
+ # The following complements the function `subroutines.generate_diff_act_flux_funcs`
394
+ # in performing the reclassification of delta-scaled actinic flux
395
+ if _return_act_dscale_for_reclass:
396
+ if not is_delta_scaled:
365
397
  act_dscale_reclassification = 0
398
+ elif is_antiderivative_wrt_tau:
399
+ act_dscale_reclassification = (
400
+ I0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0)
401
+ - I0 * np.exp(-tau / mu0) * -mu0
402
+ )
403
+ else:
404
+ act_dscale_reclassification = I0 * np.exp(-scaled_tau / mu0) - I0 * np.exp(-tau / mu0)
366
405
 
367
- exponent = np.concatenate(
368
- [
369
- K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
370
- K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
371
- ],
372
- axis=-1,
373
- )
374
-
375
406
  direct_beam_contribution = 0
376
- if is_antiderivative_wrt_tau and there_is_beam_source:
407
+ if is_antiderivative_wrt_tau:
377
408
  if there_is_beam_source:
378
409
  direct_beam_contribution = (B_collect_0.T / (-scale_tau / mu0)[None, :])[
379
410
  :, l
@@ -383,7 +414,7 @@ def _assemble_intensity_and_fluxes(
383
414
  np.einsum(
384
415
  "tij, tj -> it",
385
416
  GC_collect_0[l, :, :],
386
- np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
417
+ exp_exponent / (scale_tau[:, None] * K_collect_0)[l, :],
387
418
  optimize=True,
388
419
  )
389
420
  + direct_beam_contribution
@@ -396,7 +427,7 @@ def _assemble_intensity_and_fluxes(
396
427
 
397
428
  u0 = (
398
429
  np.einsum(
399
- "tij, tj -> it", GC_collect_0[l, :, :], np.exp(exponent), optimize=True
430
+ "tij, tj -> it", GC_collect_0[l, :, :], exp_exponent, optimize=True
400
431
  )
401
432
  + direct_beam_contribution
402
433
  )
@@ -449,18 +480,9 @@ def _assemble_intensity_and_fluxes(
449
480
  if np.any(tau < 0) or np.any(tau > tau_arr[-1]):
450
481
  raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
451
482
 
452
- # Atmospheric layer indices
453
- l = np.searchsorted(tau_arr, tau, side="left")
454
- scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
455
- scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
456
-
457
- # Delta-M scaling
458
- if np.any(scale_tau != 1):
459
- tau_dist_from_top = tau_arr[l] - tau
460
- scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
461
- scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
462
- else:
463
- scaled_tau = tau
483
+ # Atmospheric layer indices, delta-M scaling and exponentials (cached)
484
+ l, scaled_tau = _process_tau(tau)
485
+ exp_exponent = _exp_exponent(l, scaled_tau, all_modes=False)
464
486
 
465
487
  if there_is_iso_source:
466
488
  l_uniq, l_inv = np.unique(l, return_inverse=True)
@@ -478,14 +500,6 @@ def _assemble_intensity_and_fluxes(
478
500
  else:
479
501
  _mathscr_v_contribution = 0
480
502
 
481
- exponent = np.concatenate(
482
- [
483
- K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
484
- K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
485
- ],
486
- axis=-1,
487
- )
488
-
489
503
  direct_beam_contribution = 0
490
504
  if is_antiderivative_wrt_tau:
491
505
  if there_is_beam_source:
@@ -497,7 +511,7 @@ def _assemble_intensity_and_fluxes(
497
511
  np.einsum(
498
512
  "tij, tj -> it",
499
513
  GC_pos[l, :, :],
500
- np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :],
514
+ exp_exponent / (scale_tau[:, None] * K_collect_0)[l, :],
501
515
  )
502
516
  + direct_beam_contribution
503
517
  + _mathscr_v_contribution
@@ -507,7 +521,7 @@ def _assemble_intensity_and_fluxes(
507
521
  direct_beam_contribution = B_pos[:, l] * np.exp(-scaled_tau / mu0)[None, :]
508
522
 
509
523
  u0_pos = (
510
- np.einsum("tij, tj -> it", GC_pos[l, :, :], np.exp(exponent))
524
+ np.einsum("tij, tj -> it", GC_pos[l, :, :], exp_exponent)
511
525
  + direct_beam_contribution
512
526
  + _mathscr_v_contribution
513
527
  )
@@ -532,18 +546,9 @@ def _assemble_intensity_and_fluxes(
532
546
  if np.any(tau < 0) or np.any(tau > tau_arr[-1]):
533
547
  raise ValueError("tau input outside the tau range given for the atmosphere (check `tau_arr`).")
534
548
 
535
- # Atmospheric layer indices
536
- l = np.searchsorted(tau_arr, tau, side="left")
537
- scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
538
- scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
539
-
540
- # Delta-M scaling
541
- if np.any(scale_tau != 1):
542
- tau_dist_from_top = tau_arr[l] - tau
543
- scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
544
- scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
545
- else:
546
- scaled_tau = tau
549
+ # Atmospheric layer indices, delta-M scaling and exponentials (cached)
550
+ l, scaled_tau = _process_tau(tau)
551
+ exp_exponent = _exp_exponent(l, scaled_tau, all_modes=False)
547
552
 
548
553
  if there_is_iso_source:
549
554
  l_uniq, l_inv = np.unique(l, return_inverse=True)
@@ -561,14 +566,6 @@ def _assemble_intensity_and_fluxes(
561
566
  else:
562
567
  _mathscr_v_contribution = 0
563
568
 
564
- exponent = np.concatenate(
565
- [
566
- K_collect_0[l, :N] * (scaled_tau - scaled_tau_arr_lm1)[:, None],
567
- K_collect_0[l, N:] * (scaled_tau - scaled_tau_arr_l)[:, None],
568
- ],
569
- axis=-1,
570
- )
571
-
572
569
  direct_beam_contribution = 0
573
570
  direct_beam = 0
574
571
  direct_beam_scaled = 0
@@ -579,7 +576,7 @@ def _assemble_intensity_and_fluxes(
579
576
  direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0) / (-scale_tau[l] / mu0)
580
577
 
581
578
  u0_neg = (
582
- np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent) / (scale_tau[:, None] * K_collect_0)[l, :])
579
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], exp_exponent / (scale_tau[:, None] * K_collect_0)[l, :])
583
580
  + direct_beam_contribution
584
581
  + _mathscr_v_contribution
585
582
  )
@@ -590,7 +587,7 @@ def _assemble_intensity_and_fluxes(
590
587
  direct_beam_scaled = I0 * mu0 * np.exp(-scaled_tau / mu0)
591
588
 
592
589
  u0_neg = (
593
- np.einsum("tij, tj -> it", GC_neg[l, :, :], np.exp(exponent))
590
+ np.einsum("tij, tj -> it", GC_neg[l, :, :], exp_exponent)
594
591
  + direct_beam_contribution
595
592
  + _mathscr_v_contribution
596
593
  )
@@ -122,7 +122,12 @@ def _solve_for_coeffs(
122
122
 
123
123
  else:
124
124
  LHS = np.zeros((dim, dim), order="F")
125
- sR, sC = LHS.strides
125
+ sR, sC = LHS.strides
126
+ # Stacks for a single batched `np.linalg.solve` over all Fourier modes after the
127
+ # assembly loop; this removes the per-mode LAPACK dispatch overhead. The banded
128
+ # path keeps its per-mode loop as `scipy.linalg.solve_banded` cannot batch.
129
+ LHS_batch = np.empty((NFourier, dim, dim))
130
+ RHS_batch = np.empty((NFourier, dim))
126
131
  def _dense_slab(r0, c0, nrows, ncols):
127
132
  """Batched (NLayers-1, nrows, ncols) view across interlayer blocks of `LHS`.
128
133
  Anchored at LHS coords (r0, c0) for layer 0; layer l advances both row and column
@@ -281,7 +286,7 @@ def _solve_for_coeffs(
281
286
  # --------------------------------------------------------------------------------------------------------------------------
282
287
 
283
288
 
284
- # Assemble LHS and solve (much of this code is replicated in section 4 of the Comprehensive Documentation)
289
+ # Assemble LHS and solve (the old version of this code is replicated in section 4 of the Comprehensive Documentation)
285
290
  # --------------------------------------------------------------------------------------------------------------------------
286
291
  G_0 = G_collect_m[0]
287
292
  G_0_nn = G_0[N:, :N]
@@ -334,7 +339,7 @@ def _solve_for_coeffs(
334
339
  _view_slanted_batched(2 * N, 3 * N, N)[:] = -G_hi[:, N:, N:] * E_llp1
335
340
 
336
341
  # Solve the system
337
- C_m = sc.linalg.solve_banded(
342
+ C_collect[m, :] = sc.linalg.solve_banded(
338
343
  (Nsupsubdiags, Nsupsubdiags),
339
344
  LHS_dof,
340
345
  RHS,
@@ -372,12 +377,15 @@ def _solve_for_coeffs(
372
377
  _dense_slab(N, 3 * N, N, N)[:] = -G_hi[:, :N, N:] * E_llp1
373
378
  _dense_slab(2 * N, 3 * N, N, N)[:] = -G_hi[:, N:, N:] * E_llp1
374
379
 
375
- # Solve the system
376
- C_m = np.linalg.solve(LHS, RHS)
377
-
378
- ##################################################################################################
380
+ # Defer to the single batched solve over all Fourier modes below
381
+ LHS_batch[m] = LHS
382
+ RHS_batch[m] = RHS
383
+
384
+ ##################################################################################################
379
385
  # --------------------------------------------------------------------------------------------------------------------------
380
-
381
- C_collect[m, :] = C_m
382
-
386
+
387
+ if not use_banded_solver:
388
+ # One batched call for all Fourier modes
389
+ C_collect = np.linalg.solve(LHS_batch, RHS_batch[:, :, None])[:, :, 0]
390
+
383
391
  return G_collect * C_collect.reshape((NFourier, NLayers, 1, NQuad))