PythonicDISORT 1.4__py3-none-any.whl → 1.6__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.
@@ -167,7 +167,7 @@ def _assemble_intensity_and_fluxes(
167
167
 
168
168
  # Construct the intensity function (refer to section 3.7 of the Comprehensive Documentation)
169
169
  # --------------------------------------------------------------------------------------------------------------------------
170
- def u(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False, *, _return_l=False):
170
+ def u(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
171
171
  """
172
172
  Intensity function with arguments `(tau, phi)` of types `(array or float, array or float)`.
173
173
  Returns an ndarray with axes corresponding to variation with `mu, tau, phi` respectively.
@@ -236,7 +236,7 @@ def _assemble_intensity_and_fluxes(
236
236
  if there_is_iso_source:
237
237
  l_uniq, l_inv = np.unique(l, return_inverse=True)
238
238
  _mathscr_v_contribution = _mathscr_v(
239
- scaled_tau, l_inv,
239
+ scaled_tau, scale_tau, l_inv,
240
240
  Nscoeffs,
241
241
  scaled_s_poly_coeffs[l_uniq],
242
242
  G_collect_0[l_uniq],
@@ -318,13 +318,6 @@ def _assemble_intensity_and_fluxes(
318
318
  if return_tau_arr:
319
319
  outputs += (tau_arr,)
320
320
  output_is_tuple = True
321
-
322
- if _return_l: # This indicates to the function that NT corrections are activated
323
- output_is_tuple = True
324
- outputs += (l,)
325
- if there_is_iso_source:
326
- # The TMS correction will use these
327
- outputs += (l_uniq, l_inv)
328
321
 
329
322
  return outputs if output_is_tuple else outputs[0]
330
323
  # --------------------------------------------------------------------------------------------------------------------------
@@ -411,7 +404,7 @@ def _assemble_intensity_and_fluxes(
411
404
  if there_is_iso_source:
412
405
  l_uniq, l_inv = np.unique(l, return_inverse=True)
413
406
  _mathscr_v_contribution = _mathscr_v(
414
- scaled_tau, l_inv,
407
+ scaled_tau, scale_tau, l_inv,
415
408
  Nscoeffs,
416
409
  scaled_s_poly_coeffs[l_uniq],
417
410
  G_collect_0[l_uniq],
@@ -470,7 +463,7 @@ def _assemble_intensity_and_fluxes(
470
463
  if there_is_iso_source:
471
464
  l_uniq, l_inv = np.unique(l, return_inverse=True)
472
465
  _mathscr_v_contribution = _mathscr_v(
473
- scaled_tau, l_inv,
466
+ scaled_tau, scale_tau, l_inv,
474
467
  Nscoeffs,
475
468
  scaled_s_poly_coeffs[l_uniq],
476
469
  G_collect_0[l_uniq, :N, :],
@@ -552,7 +545,7 @@ def _assemble_intensity_and_fluxes(
552
545
  if there_is_iso_source:
553
546
  l_uniq, l_inv = np.unique(l, return_inverse=True)
554
547
  _mathscr_v_contribution = _mathscr_v(
555
- scaled_tau, l_inv,
548
+ scaled_tau, scale_tau, l_inv,
556
549
  Nscoeffs,
557
550
  scaled_s_poly_coeffs[l_uniq],
558
551
  G_collect_0[l_uniq, N:, :],
@@ -168,7 +168,8 @@ def _solve_for_coeffs(
168
168
  if m_equals_0 and there_is_iso_source:
169
169
  # Top boundary
170
170
  RHS[:N] -= _mathscr_v(
171
- np.array([0]),
171
+ np.array([0]),
172
+ None,
172
173
  np.array([0]),
173
174
  Nscoeffs,
174
175
  scaled_s_poly_coeffs[[0], :],
@@ -183,6 +184,7 @@ def _solve_for_coeffs(
183
184
  RHS[N:-N] += (
184
185
  _mathscr_v(
185
186
  scaled_tau_arr_with_0[1:-1],
187
+ None,
186
188
  indices,
187
189
  Nscoeffs,
188
190
  scaled_s_poly_coeffs[indicesp1],
@@ -192,6 +194,7 @@ def _solve_for_coeffs(
192
194
  )
193
195
  - _mathscr_v(
194
196
  scaled_tau_arr_with_0[1:-1],
197
+ None,
195
198
  indices,
196
199
  Nscoeffs,
197
200
  scaled_s_poly_coeffs[indices],
@@ -203,7 +206,8 @@ def _solve_for_coeffs(
203
206
 
204
207
  # Bottom boundary
205
208
  RHS[-N:] -= _mathscr_v(
206
- scaled_tau_arr_with_0[[-1]],
209
+ scaled_tau_arr_with_0[[-1]],
210
+ None,
207
211
  np.array([0]),
208
212
  Nscoeffs,
209
213
  scaled_s_poly_coeffs[[-1], :],
@@ -217,6 +221,7 @@ def _solve_for_coeffs(
217
221
  R
218
222
  @ _mathscr_v(
219
223
  scaled_tau_arr_with_0[[-1]],
224
+ None,
220
225
  np.array([0]),
221
226
  Nscoeffs,
222
227
  scaled_s_poly_coeffs[[-1], :],
@@ -198,7 +198,8 @@ def _solve_for_gen_and_part_sols(
198
198
  G_collect[no_shortcut_indices, N:, :N] = GpG_m_GmG_div_2
199
199
 
200
200
  K_collect[no_shortcut_indices, :] = K_arr
201
- if len(no_shortcut_indices_0) > 0: # If there is no isotropic source this list will be empty
201
+ len_no_shortcut_indices_0 = len(no_shortcut_indices_0)
202
+ if len_no_shortcut_indices_0 > 0: # If there is no isotropic source this list will be empty
202
203
  G_inv_collect_0[no_shortcut_indices_0, :, :] = np.linalg.inv(
203
204
  G_collect[no_shortcut_indices_0, :, :]
204
205
  )
@@ -209,10 +210,25 @@ def _solve_for_gen_and_part_sols(
209
210
 
210
211
  # Solve for particular solution w.r.t. direct beam source (refer to section 3.6.1 of the Comprehensive Documentation)
211
212
  # --------------------------------------------------------------------------------------------------------------------------
212
- A_arr[:, :N, N:] = -A_arr[:, N:, :N]
213
- A_arr[:, :N, :N] = -A_arr[:, N:, N:]
214
- A_arr.reshape(-1, NQuad * NQuad)[:, ::NQuad + 1] += 1 / mu0
215
- B_collect[no_shortcut_indices, :] = np.linalg.solve(A_arr, X_arr[no_shortcut_indices, :, None]).squeeze(-1)
213
+ if len_no_shortcut_indices_0 > 0: # In this case we have `G_inv_collect_0`, so use it
214
+ B_collect[no_shortcut_indices_0, :] = np.einsum(
215
+ "bij, bjk, bk -> bi",
216
+ # Note that the minus sign has been absorbed into `X_arr`
217
+ G_collect[no_shortcut_indices_0, :, :]
218
+ / (1 / mu0 + K_collect[no_shortcut_indices_0, None, :]),
219
+ G_inv_collect_0[no_shortcut_indices_0, :, :],
220
+ X_arr[no_shortcut_indices_0, :],
221
+ optimize=True,
222
+ )
223
+ no_shortcut_indices = no_shortcut_indices[len_no_shortcut_indices_0:]
224
+
225
+ if len(no_shortcut_indices) > 0:
226
+ A_arr = A_arr[len_no_shortcut_indices_0:]
227
+ A_arr[:, :N, N:] = -A_arr[:, N:, :N]
228
+ A_arr[:, :N, :N] = -A_arr[:, N:, N:]
229
+ A_arr.reshape(-1, NQuad * NQuad)[:, ::NQuad + 1] += 1 / mu0
230
+ # Note that the minus sign has been absorbed into `X_arr`
231
+ B_collect[no_shortcut_indices, :] = np.linalg.solve(A_arr, X_arr[no_shortcut_indices, :, None]).squeeze(-1)
216
232
 
217
233
  # --------------------------------------------------------------------------------------------------------------------------
218
234
 
@@ -98,7 +98,7 @@ def pydisort(
98
98
  Returns the diffuse flux magnitudes (same type and size as ``tau``).
99
99
  Pass ``is_antiderivative_wrt_tau = True`` (defaults to ``False``)
100
100
  to switch to an antiderivative of this function with respect to ``tau``.
101
- For example, ``Fp(tau_arr, is_antiderivative_wrt_tau = True) - Fp(np.insert(tau_arr[:-1] + 1e-15, 0, 0), is_antiderivative_wrt_tau = True)``
101
+ For example, ``Fp(tau_arr, True) - Fp(np.insert(tau_arr[:-1] + 1e-15, 0, 0), True)``
102
102
  will produce an array of the tau-integral over each layer.
103
103
  Pass ``return_tau_arr`` to return ``tau_arr`` (defaults to ``False``).
104
104
  Fm(tau) : function
@@ -406,19 +406,21 @@ def pydisort(
406
406
 
407
407
  # TMS correction for the intensity (see section 3.7.2)
408
408
  # --------------------------------------------------------------------------------------------------------------------------
409
- def TMS_correction(tau, phi, is_antiderivative_wrt_tau, l, l_uniq, l_inv):
409
+ def TMS_correction(tau, phi, is_antiderivative_wrt_tau):
410
410
  Ntau = len(tau)
411
411
  Nphi = len(phi)
412
412
  # Atmospheric layer indices
413
- if not there_is_iso_source:
414
- l_uniq, l_inv = np.unique(l, return_inverse=True)
415
- scaled_tau_arr_l = scaled_tau_arr_with_0[1:][l]
413
+ l = np.argmax(tau[:, None] <= tau_arr[None, :], axis=1)
414
+ scaled_tau_arr_l = scaled_tau_arr_with_0[l + 1]
416
415
  scaled_tau_arr_lm1 = scaled_tau_arr_with_0[l]
417
416
 
418
417
  # Delta-M scaling
419
- tau_dist_from_top = tau_arr[l] - tau
420
- scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
421
- scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
418
+ if np.any(scale_tau != np.ones(NLayers)):
419
+ tau_dist_from_top = tau_arr[l] - tau
420
+ scaled_tau_dist_from_top = tau_dist_from_top * scale_tau[l]
421
+ scaled_tau = scaled_tau_arr_l - scaled_tau_dist_from_top
422
+ else:
423
+ scaled_tau = tau
422
424
 
423
425
  # mathscr_B
424
426
  # --------------------------------------------------------------------------------------------------------------------------
@@ -429,45 +431,44 @@ def pydisort(
429
431
  [
430
432
  f(nu)[:, None, :]
431
433
  # Iterates over the 0th axis
432
- for f in map(Legendre, weighted_Leg_coeffs_all[l_uniq])
434
+ for f in map(Legendre, weighted_Leg_coeffs_all)
433
435
  ],
434
436
  axis=1,
435
- ) # (NQuad, NLayers_uniq, Nphi)
437
+ )
436
438
  p_trun = np.concatenate(
437
439
  [
438
440
  f(nu)[:, None, :]
439
441
  # Iterates over the 0th axis
440
- for f in map(Legendre, weighted_scaled_Leg_coeffs[l_uniq])
442
+ for f in map(Legendre, weighted_scaled_Leg_coeffs)
441
443
  ],
442
444
  axis=1,
443
- ) # (NQuad, NLayers_uniq, Nphi)
444
-
445
+ )
445
446
  mathscr_B = (
446
- (scaled_omega_arr[l_uniq] * I0_div_4pi)[None, :, None]
447
+ (scaled_omega_arr * I0)[None, :, None]
448
+ / (4 * np.pi)
447
449
  * (mu0 / (mu0 + mu_arr))[:, None, None]
448
- * (p_true / (1 - f_arr)[None, l_uniq, None] - p_trun)
449
- ) # (NQuad, NLayers_uniq, Nphi)
450
+ * (p_true / (1 - f_arr[None, :, None]) - p_trun)
451
+ )
450
452
  # --------------------------------------------------------------------------------------------------------------------------
451
453
 
452
454
  neg_scaled_tau_div_mu0 = -scaled_tau / mu0
453
455
  if is_antiderivative_wrt_tau:
454
456
 
455
- scale_tau_l = scale_tau[l_uniq]
456
- neg_scale_tau_l_div_mu0 = -scale_tau_l / mu0
457
- scale_tau_l_div_mu_arr_pos = scale_tau_l[None, :] / mu_arr_pos[:, None]
457
+ neg_scale_tau_div_mu0 = -scale_tau / mu0
458
+ scale_tau_div_mu_arr_pos = scale_tau[None, :] / mu_arr_pos[:, None]
458
459
 
459
460
  TMS_correction_pos = (
460
- np.exp(neg_scaled_tau_div_mu0) / neg_scale_tau_l_div_mu0[l_inv]
461
+ np.exp(neg_scaled_tau_div_mu0) / neg_scale_tau_div_mu0[l]
461
462
  )[None, :] - np.exp(
462
463
  (scaled_tau - scaled_tau_arr_l)[None, :] / mu_arr_pos[:, None]
463
464
  - scaled_tau_arr_l[None, :] / mu0
464
- ) / scale_tau_l_div_mu_arr_pos[:, l_inv]
465
+ ) / scale_tau_div_mu_arr_pos[:, l]
465
466
  TMS_correction_neg = (
466
- np.exp(neg_scaled_tau_div_mu0) / neg_scale_tau_l_div_mu0[l_inv]
467
+ np.exp(neg_scaled_tau_div_mu0) / neg_scale_tau_div_mu0[l]
467
468
  )[None, :] + np.exp(
468
469
  (scaled_tau_arr_lm1 - scaled_tau)[None, :] / mu_arr_pos[:, None]
469
470
  - scaled_tau_arr_lm1[None, :] / mu0
470
- ) / scale_tau_l_div_mu_arr_pos[:, l_inv]
471
+ ) / scale_tau_div_mu_arr_pos[:, l]
471
472
  else:
472
473
  TMS_correction_pos = np.exp(neg_scaled_tau_div_mu0)[None, :] - np.exp(
473
474
  (scaled_tau - scaled_tau_arr_l)[None, :] / mu_arr_pos[:, None]
@@ -477,123 +478,111 @@ def pydisort(
477
478
  (scaled_tau_arr_lm1 - scaled_tau)[None, :] / mu_arr_pos[:, None]
478
479
  - scaled_tau_arr_lm1[None, :] / mu0
479
480
  )
480
-
481
481
 
482
482
  ## Contribution from other layers
483
483
  # --------------------------------------------------------------------------------------------------------------------------
484
- solution = (
485
- mathscr_B[:, l_inv, :]
486
- * np.concatenate((TMS_correction_pos, TMS_correction_neg), axis=0)[:, :, None]
487
- )
484
+ Bpos = mathscr_B[:N, :, :] # (N, NLayers, Nphi)
485
+ Bneg = mathscr_B[N:, :, :] # (N, NLayers, Nphi)
488
486
 
489
- if is_atmos_multilayered:
490
- any_pos = (l.min() < NLayers - 1)
491
- any_neg = (l.max() > 0)
492
-
493
- mu0_inv = 1 / mu0
487
+ sol_pos = Bpos[:, l, :] * TMS_correction_pos[:, :, None] # (N, Ntau, Nphi)
488
+ sol_neg = Bneg[:, l, :] * TMS_correction_neg[:, :, None] # (N, Ntau, Nphi)
489
+
490
+ if not is_atmos_multilayered:
491
+ return np.concatenate((sol_pos, sol_neg), axis=0)
494
492
 
495
- scaled_tauwith0_front = scaled_tau_arr_with_0[:-1] # (NLayers,)
496
- scaled_tauwith0_back = scaled_tau_arr_with_0[1:] # (NLayers,); i.e. `scaled_tau_arr`
493
+ # ---------------- other-layer contributions ----------------
494
+ any_pos = (l.min() < NLayers - 1) # exists tau with layers below
495
+ any_neg = (l.max() > 0) # exists tau with layers above
497
496
 
498
- exp_taufront_mu0_inv = np.concatenate(
499
- ([1], np.exp(-scaled_tauwith0_front[1:] * mu0_inv))
500
- ) # (Nlayers, )
497
+ # Layer boundaries/thickness in scaled-tau coordinates
498
+ tau_front = scaled_tau_arr_with_0[:-1] # (NLayers,) = tau_r
499
+ tau_back = scaled_tau_arr_with_0[1:] # (NLayers,) = tau_{r+1}
501
500
 
501
+ # decay[n,r] = exp(-scaled_thickness_arr[r]/mu[n]) (argument <= 0; underflow is safe)
502
+ decay = np.exp(-scaled_thickness_arr[None, :] * M_inv[:, None]) # (N, NLayers)
502
503
 
503
- Bpos = mathscr_B[:N, :, :] # (N, NLayers_uniq, Nphi)
504
- Bneg = mathscr_B[N:, :, :] # (N, NLayers_uniq, Nphi)
504
+ exp_taufront_mu0_inv = np.exp(-tau_front / mu0) # (NLayers,)
505
+ exp_tauback_mu0_inv = np.exp(-tau_back / mu0) # (NLayers,)
505
506
 
506
- # decay[n,r] = exp(-scaled_thickness_arr[r]/mu[n]); always <= 1
507
- log_decay = -scaled_thickness_arr[None, :] * M_inv[:, None] # (N, NLayers)
507
+ if is_antiderivative_wrt_tau:
508
+ # (N, NLayers): multiply by this instead of dividing by (scale_tau/mu)
509
+ mu_div_scale_layers = mu_arr_pos[:, None] / scale_tau[None, :]
508
510
 
509
- # decay_prod[n,k] = prod_{m=0}^{k-1} decay[n,m], with decay_prod[:,0]=1
510
- log_decay_prod = np.cumsum(log_decay, axis=1) # The cumsum requires ALL layers; `l_uniq` is inapplicable
511
- decay_prod = np.concatenate((np.ones((N, 1)), np.exp(log_decay_prod)), axis=1) # (N, NLayers + 1)
511
+ # ===== POS: sum over r > l =====
512
+ if any_pos:
513
+ # fac_pos = 1 - exp(-scaled_thickness_arr*(1/mu + 1/mu0)) = -expm1(-scaled_thickness_arr*(...))
514
+ delta_pos = scaled_thickness_arr[None, :] * (M_inv + 1 / mu0)[:, None] # (N, NLayers) >= 0
515
+ fac_pos = -np.expm1(-delta_pos) # (N, NLayers) in [0,1)
512
516
 
513
517
  if is_antiderivative_wrt_tau:
514
- # (N, NLayers)
515
- integration_factor = mu_arr_pos[:, None] / scale_tau
516
-
517
- # ====================================================================
518
- # POS: pre-accumulate "contribution from below" sums Rpos[N, l, Nphi]
519
- # ====================================================================
520
- if any_pos:
521
- thickness_pos = scaled_thickness_arr[None, :] * (M_inv + mu0_inv)[:, None] # (N, NLayers); always > 0
522
- # em1_pos = 1 - exp(-scaled_thickness_arr*(1/mu + 1/mu0)) = -expm1(-thickness_pos)
523
- em1_pos = -np.expm1(-thickness_pos) # (N, NLayers)
524
- if is_antiderivative_wrt_tau:
525
- em1_pos = integration_factor * em1_pos
526
-
527
- layer_term_pos = em1_pos * exp_taufront_mu0_inv[None, :] # (N, NLayers)
528
-
529
- # Reverse-scan via cumsum:
530
- # Rpos[l] = (1/decay_prod[l+1]) * sum_{r=l+1..} (layer_term_pos[r] * decay_prod[r])
531
- weighted_layer_term_pos = layer_term_pos * decay_prod[:, :NLayers] # (N, NLayers)
532
- cumsum_pos = np.cumsum(weighted_layer_term_pos[:, ::-1], axis=1)[:, ::-1] # (N, NLayers)
533
-
534
- Rpos_rest = cumsum_pos[:, 1:] / decay_prod[:, 1 : NLayers] # (N, NLayers - 1)
535
- Rpos = np.concatenate([Rpos_rest, np.zeros((N, 1))], axis=1) # (N, NLayers)
536
-
537
- # Apply to all `tau`
538
- # expfac_pos = exp(M_inv*tau) * exp(-M_inv*scaled_tauwith0_back[l])
539
- expfac_pos = np.exp(M_inv[:, None] * (scaled_tau - scaled_tauwith0_back[l])[None, :]) # (N, Ntau)
540
- tmp = np.take(Rpos, l, axis=1) # (N, Ntau)
541
- tmp = (tmp * expfac_pos)[:, :, None] * Bpos[:, l_inv, :]
542
-
543
- if autograd_compatible:
544
- solution = np.concatenate((solution[:N, :, :] + tmp, solution[N:, :, :]), axis=0)
545
- else:
546
- solution[:N, :, :] += tmp
547
-
548
- # ====================================================================
549
- # NEG: pre-accumulate "contribution from above" sums Rneg[N, l, Nphi]
550
- # ====================================================================
551
- if any_neg:
552
- decay = np.exp(log_decay) # (N, NLayers)
553
-
554
- # Ensure no positive exponents
555
- thickness_neg = scaled_thickness_arr[None, :] * (M_inv - mu0_inv)[:, None] # (N, NLayers)
556
- mask = (thickness_neg >= 0)
518
+ fac_pos = fac_pos * mu_div_scale_layers # (N, NLayers)
557
519
 
558
- exp_x1 = np.exp(-scaled_tauwith0_back * mu0_inv)[None, :] # (1, NLayers)
559
- exp_x0 = decay * exp_taufront_mu0_inv[None, :] # (N, NLayers)
520
+ # coef_pos[n,r] multiplies exp((tau - tau_front[r])/mu - tau_front[r]/mu0) after re-referencing
521
+ # includes exp(-tau_front[r]/mu0)
522
+ coef_pos = fac_pos * exp_taufront_mu0_inv[None, :] # (N, NLayers)
560
523
 
561
- # Let a = |thickness_neg| >= 0. Then expm1(-a) uses only non-positive inputs.
562
- em1_neg = np.expm1(-np.abs(thickness_neg)) # (N, NLayers)
524
+ # Reverse scan to build Acc_l (referenced at tau_back[l]) INCLUDING Bpos:
525
+ # Acc_{r-1} = coef_pos[:,r]*Bpos[:,r,:] + decay[:,r]*Acc_r
526
+ acc0 = np.zeros((N, Nphi))
527
+ acc = acc0
528
+ cols_rev = [acc0] # Acc_{NLayers-1} = 0
563
529
 
564
- # thickness_neg>=0: exp_x1 * (1 - exp(-a)) = exp_x1 * (-expm1(-a)) = exp_x1 * (-em1_neg)
565
- # thickness_neg<0 : exp_x0 * (exp(-a) - 1) = exp_x0 * expm1(-a) = exp_x0 * ( em1_neg)
566
- layer_term_negthick = em1_neg * exp_x0 # (N, NLayers)
567
- layer_term_posthick = -em1_neg * exp_x1 # (N, NLayers)
568
- layer_term_neg = layer_term_posthick * mask + layer_term_negthick * (~mask) # (N, NLayers)
530
+ for r in range(NLayers - 1, 0, -1): # r = NLayers-1 ... 1
531
+ term = coef_pos[:, r][:, None] * Bpos[:, r, :] # (N, Nphi)
532
+ acc = term + decay[:, r][:, None] * acc
533
+ cols_rev.append(acc)
569
534
 
570
- if is_antiderivative_wrt_tau:
571
- layer_term_neg = -integration_factor * layer_term_neg
535
+ Acc_pos = np.stack(cols_rev[::-1], axis=1) # (N, NLayers, Nphi)
572
536
 
573
- # Forward-scan via cumsum:
574
- # Rneg[l] = decay_prod[l] * sum_{r<l} layer_term_neg[r] / decay_prod[r+1]
575
- weighted_layer_term_neg = layer_term_neg / decay_prod[:, 1:] # (N, NLayers)
576
- cumsum_neg = np.cumsum(weighted_layer_term_neg, axis=1) # (N, NLayers)
577
-
578
- Rneg_rest = decay_prod[:, 1 : NLayers] * cumsum_neg[:, :-1] # (N, NLayers - 1)
579
- Rneg = np.concatenate([np.zeros((N, 1)), Rneg_rest], axis=1) # (N, NLayers)
537
+ # Apply to tau: multiply by exp((scaled_tau - tau_back[l]) / mu) (argument <= 0 in exact arithmetic)
538
+ expfac_pos = np.exp(M_inv[:, None] * (scaled_tau - tau_back[l])[None, :]) # (N, Ntau)
539
+ contrib_pos = Acc_pos[:, l, :] * expfac_pos[:, :, None] # (N, Ntau, Nphi)
580
540
 
581
- # Apply to all `tau`
582
- expfac_neg = np.exp(M_inv[:, None] * (scaled_tauwith0_front[l] - scaled_tau)[None, :]) # (N, Ntau)
583
- tmp = np.take(Rneg, l, axis=1) # (N, Ntau)
584
- tmp = (tmp * expfac_neg)[:, :, None] * Bneg[:, l_inv, :]
585
-
586
- if autograd_compatible:
587
- solution = np.concatenate((solution[:N, :, :], solution[N:, :, :] + tmp), axis=0)
588
- else:
589
- solution[N:, :, :] += tmp
541
+ sol_pos = sol_pos + contrib_pos
542
+
543
+ # ===== NEG: sum over r < l =====
544
+ if any_neg:
545
+ # We need term_base[n,r] = exp(-tau_back[r]/mu0) - exp(-scaled_thickness_arr[r]/mu)*exp(-tau_front[r]/mu0)
546
+ # computed stably via expm1 using only non-positive inputs.
547
+ delta_neg = scaled_thickness_arr[None, :] * (M_inv - 1 / mu0)[:, None] # (N, NLayers), can be +/-.
548
+ abs_delta = np.abs(delta_neg)
549
+ em1 = np.expm1(-abs_delta) # (N, NLayers) in [-1,0], argument <= 0
550
+
551
+ exp_x1 = exp_tauback_mu0_inv[None, :] # (1, NLayers)
552
+ exp_x0 = decay * exp_taufront_mu0_inv[None, :] # (N, NLayers)
553
+
554
+ # if delta>=0: term = exp_x1*(1-exp(-delta)) = exp_x1*(-expm1(-delta)) = exp_x1*(-em1)
555
+ # if delta<0 : term = exp_x0*(exp(delta)-1) = exp_x0*expm1(delta) = exp_x0*( em1) (since delta<0 -> em1=expm1(delta))
556
+ term_base = np.where(delta_neg >= 0.0, (-em1) * exp_x1, em1 * exp_x0) # (N, NLayers)
557
+
558
+ if is_antiderivative_wrt_tau:
559
+ # original divides by (-scale_tau/mu) == multiply by (-(mu/scale_tau))
560
+ coef_neg = term_base * -mu_div_scale_layers # (N, NLayers)
561
+ else:
562
+ coef_neg = term_base # (N, NLayers)
563
+
564
+ # Forward scan to build Acc_l (referenced at tau_front[l]) INCLUDING Bneg:
565
+ # Acc_{r+1} = coef_neg[:,r]*Bneg[:,r,:] + decay[:,r]*Acc_r
566
+ acc0 = np.zeros((N, Nphi))
567
+ acc = acc0
568
+ cols = [acc0] # Acc_0 = 0
569
+
570
+ for r in range(0, NLayers - 1): # r = 0 ... NLayers-2
571
+ term = coef_neg[:, r][:, None] * Bneg[:, r, :] # (N, Nphi)
572
+ acc = term + decay[:, r][:, None] * acc
573
+ cols.append(acc)
574
+
575
+ Acc_neg = np.stack(cols, axis=1) # (N, NLayers, Nphi)
576
+
577
+ # Apply to tau: multiply by exp((tau_front[l] - scaled_tau) / mu) (argument <= 0 in exact arithmetic)
578
+ expfac_neg = np.exp(M_inv[:, None] * (tau_front[l] - scaled_tau)[None, :]) # (N, Ntau)
579
+ contrib_neg = Acc_neg[:, l, :] * expfac_neg[:, :, None] # (N, Ntau, Nphi)
590
580
 
591
- return solution
581
+ sol_neg = sol_neg + contrib_neg
592
582
 
583
+ return np.concatenate((sol_pos, sol_neg), axis=0)
584
+
593
585
  # --------------------------------------------------------------------------------------------------------------------------
594
-
595
- else:
596
- return solution
597
586
  # --------------------------------------------------------------------------------------------------------------------------
598
587
 
599
588
  # IMS correction for the intensity (see section 3.7.2)
@@ -628,7 +617,8 @@ def pydisort(
628
617
  ) / (mu_arr_pos * scaled_mu0 * x)[:, None]
629
618
 
630
619
  return (
631
- I0_div_4pi
620
+ I0
621
+ / (4 * pi)
632
622
  * (omega_avg * f_avg) ** 2
633
623
  / (1 - omega_avg * f_avg)
634
624
  * Legendre(
@@ -640,59 +630,25 @@ def pydisort(
640
630
 
641
631
  # The corrected intensity
642
632
  # --------------------------------------------------------------------------------------------------------------------------
643
- def u_corrected(
644
- tau,
645
- phi,
646
- is_antiderivative_wrt_tau=False,
647
- return_Fourier_error=False,
648
- return_tau_arr=False,
649
- ):
633
+ def u_corrected(tau, phi, is_antiderivative_wrt_tau=False, return_Fourier_error=False, return_tau_arr=False):
650
634
  tau = np.atleast_1d(tau)
651
635
  phi = np.atleast_1d(phi)
652
-
653
- u_star_outputs = u_star(
654
- tau,
655
- phi,
656
- is_antiderivative_wrt_tau,
657
- return_Fourier_error,
658
- return_tau_arr,
659
- _return_l=True,
660
- )
661
- if there_is_iso_source:
662
- NT_corrections = TMS_correction(
663
- tau,
664
- phi,
665
- is_antiderivative_wrt_tau,
666
- u_star_outputs[-3],
667
- u_star_outputs[-2],
668
- u_star_outputs[-1],
669
- )
670
- u_star_outputs = u_star_outputs[:-3]
671
- else:
672
- NT_corrections = TMS_correction(
673
- tau, phi, is_antiderivative_wrt_tau, u_star_outputs[-1], None, None
674
- )
675
- u_star_outputs = u_star_outputs[:-1]
676
-
636
+ NT_corrections = TMS_correction(tau, phi, is_antiderivative_wrt_tau)
637
+
677
638
  if autograd_compatible:
678
- NT_corrections = np.concatenate(
679
- (
680
- NT_corrections[:N, :, :],
681
- NT_corrections[N:, :, :]
682
- + IMS_correction(tau, phi, is_antiderivative_wrt_tau),
683
- ),
684
- axis=0,
639
+ NT_corrections = NT_corrections + np.concatenate(
640
+ [np.zeros((N, len(tau), len(phi))), IMS_correction(tau, phi, is_antiderivative_wrt_tau)], axis=0
685
641
  )
686
642
  else:
687
643
  NT_corrections[N:, :, :] += IMS_correction(tau, phi, is_antiderivative_wrt_tau)
688
-
644
+
689
645
  if return_Fourier_error or return_tau_arr:
646
+ u_star_outputs = u_star(tau, phi, is_antiderivative_wrt_tau, return_Fourier_error, return_tau_arr)
690
647
  return (
691
648
  u_star_outputs[0] + rescale_factor * np.squeeze(NT_corrections),
692
649
  ) + u_star_outputs[1:]
693
650
  else:
694
- return u_star_outputs[0] + rescale_factor * np.squeeze(NT_corrections)
695
-
651
+ return u_star(tau, phi, is_antiderivative_wrt_tau) + rescale_factor * np.squeeze(NT_corrections)
696
652
  # --------------------------------------------------------------------------------------------------------------------------
697
653
 
698
654
  return mu_arr, flux_up, flux_down, u0, u_corrected
@@ -744,6 +744,7 @@ def to_diag_ordered_form(A, Nsuperdiags, Nsubdiags):
744
744
 
745
745
 
746
746
  def _mathscr_v(tau, # Input optical depths
747
+ scale_tau, # Delta-scale factor for tau
747
748
  l, # Layer index of each input optical depth
748
749
  Nscoeffs, # Number of isotropic source polynomial coefficients
749
750
  s_poly_coeffs, # Polynomial coefficients of isotropic source
@@ -763,6 +764,7 @@ def _mathscr_v(tau, # Input optical depths
763
764
  | Variable | Shape |
764
765
  | ----------------------------- | ----------------------------------------- |
765
766
  | ``tau`` | ``Ntau`` |
767
+ | ``scale_tau`` | `NLayers` |
766
768
  | ``l`` | ``Ntau`` |
767
769
  | ``Nscoeffs`` | scalar |
768
770
  | ``s_poly_coeffs`` | ``NL x Nscoeffs`` |
@@ -784,13 +786,13 @@ def _mathscr_v(tau, # Input optical depths
784
786
  if Nscoeffs == 1:
785
787
  s0 = s_poly_coeffs[:, 0:1]
786
788
  c0w = s0 * K_inv * G_inv_mu_inv # (NL, 1) * (NL, NQuad) -> (NL, NQuad)
789
+
790
+ d0 = np.einsum("lij, lj -> il", G, c0w, optimize=True)
787
791
 
788
792
  if is_antiderivative_wrt_tau:
789
- p0 = tau
793
+ p0 = tau / scale_tau
790
794
  else:
791
795
  p0 = np.ones_like(tau)
792
-
793
- d0 = np.einsum("lij, lj -> il", G, c0w, optimize=True)
794
796
 
795
797
  return d0[:, l] * p0[None, :]
796
798
 
@@ -803,19 +805,19 @@ def _mathscr_v(tau, # Input optical depths
803
805
 
804
806
  c0w = s1 * K_inv * G_inv_mu_inv
805
807
  c1w = (s0 * K_inv + s1 * K_inv2) * G_inv_mu_inv
808
+
809
+ d0 = np.einsum("lij, lj -> il", G, c0w, optimize=True) # (NQuad, NL)
810
+ d1 = np.einsum("lij, lj -> il", G, c1w, optimize=True) # (NQuad, NL)
806
811
 
807
812
  if is_antiderivative_wrt_tau:
808
813
  p0 = tau * tau / 2
809
814
  p1 = tau
815
+ return (d0[:, l] * p0[None, :] + d1[:, l] * p1[None, :]) / scale_tau
810
816
  else:
811
817
  p0 = tau
812
818
  p1 = np.ones_like(tau)
813
-
814
- d0 = np.einsum("lij, lj -> il", G, c0w, optimize=True) # (NQuad, NL)
815
- d1 = np.einsum("lij, lj -> il", G, c1w, optimize=True) # (NQuad, NL)
816
-
817
- return d0[:, l] * p0[None, :] + d1[:, l] * p1[None, :]
818
-
819
+ return d0[:, l] * p0[None, :] + d1[:, l] * p1[None, :]
820
+
819
821
  # --------------------- Nscoeffs > 2 ----------------------
820
822
  else:
821
823
  if Nscoeffs > 10:
@@ -853,7 +855,7 @@ def _mathscr_v(tau, # Input optical depths
853
855
  powers = np.arange(Nscoeffs - 1, -1, -1)[None, :]
854
856
  if is_antiderivative_wrt_tau:
855
857
  p = powers + 1
856
- tau_poly = (tau[:, None] ** p) / p
858
+ tau_poly = (tau[:, None] ** p) / (p * scale_tau)
857
859
  else:
858
860
  tau_poly = tau[:, None] ** powers
859
861
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PythonicDISORT
3
- Version: 1.4
3
+ Version: 1.6
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
@@ -16,6 +16,7 @@ Provides-Extra: notebook_dependencies
16
16
  Requires-Dist: autograd >=1.5 ; extra == 'notebook_dependencies'
17
17
  Requires-Dist: jupyter >1.0.0 ; extra == 'notebook_dependencies'
18
18
  Requires-Dist: notebook >6.5.2 ; extra == 'notebook_dependencies'
19
+ Requires-Dist: matplotlib >=3.6.0 ; extra == 'notebook_dependencies'
19
20
  Provides-Extra: pytest
20
21
  Requires-Dist: pytest >=6.2.5 ; extra == 'pytest'
21
22
 
@@ -40,7 +41,7 @@ You may contact me, Dion, through dh3065@columbia.edu.
40
41
 
41
42
  The **GitHub repository** is https://github.com/LDEO-CREW/Pythonic-DISORT.
42
43
 
43
- Accompanying Journal of Open Source Software paper: https://joss.theoj.org/papers/10.21105/joss.06442.
44
+ Accompanying Journal of Open Source Software paper: https://joss.theoj.org/papers/10.21105/joss.06442. **Please cite this paper if you use PythonicDISORT**.
44
45
 
45
46
  # Documentation
46
47
  https://pythonic-disort.readthedocs.io/en/latest/
@@ -70,18 +71,19 @@ These notebooks double up as examples of how to use PythonicDISORT. The tests wh
70
71
  * Test Problem 8: Absorbing / Isotropic-Scattering Medium (multiple layers)
71
72
  * Test Problem 9: General Emitting / Absorbing / Scattering Medium (multiple layers)
72
73
  * Test Problem 11: Single-Layer vs. Multiple Layers (no corresponding Jupyter Notebook)
74
+ * Test Problem I: Antiderivative / integration functionality (no corresponding Jupyter Notebook)
73
75
 
74
76
  # Installation
75
77
 
76
78
  * From PyPI: `pip install PythonicDISORT`
77
- * From Conda-forge: (TODO: need to first publish on Conda-forge)
79
+ * From Conda-Forge: `conda install -c conda-forge pythonicdisort` or `mamba install -c conda-forge pythonicdisort`
78
80
  * 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*)
79
81
 
80
82
  ## Requirements to run PythonicDISORT
81
83
  * Python 3.8+
82
84
  * `numpy >= 1.8.0`
83
85
  * `scipy >= 1.8.0`
84
- * (OPTIONAL) `autograd >= 1.5`
86
+ * (OPTIONAL) `autograd >= 1.5` (`autograd` can be used to compute gradients of the output functions)
85
87
  * (OPTIONAL) `pytest >= 6.2.5` (Required to use the command `pytest`, see *PyTest and examples of how to use PythonicDISORT*)
86
88
 
87
89
  ## (OPTIONAL) Additional requirements to run the Jupyter Notebook
@@ -89,7 +91,8 @@ These notebooks double up as examples of how to use PythonicDISORT. The tests wh
89
91
  * `notebook > 6.5.2`
90
92
  * `matplotlib >= 3.6.0`
91
93
 
92
- In addition, a F2PY-wrapped Stamnes' DISORT, or equivalent, is required to properly run the last section (section 6).
94
+ In addition, a F2PY-wrapped Stamnes' DISORT or equivalent (https://github.com/kconnour/pyRT_DISORT may work)
95
+ is required to properly run the last section (section 6).
93
96
 
94
97
  ## Compatibility
95
98
 
@@ -0,0 +1,11 @@
1
+ PythonicDISORT/__init__.py,sha256=V1obNzaFgqInxqYJ5h_r6JHbZRxx4VM5VgzCIS6EEzI,84
2
+ PythonicDISORT/_assemble_intensity_and_fluxes.py,sha256=nN2Kn7UCB6tYlVjKj1Hk_IwNO420uibsSD6o9Lx9X5o,29520
3
+ PythonicDISORT/_solve_for_coeffs.py,sha256=5hXhKi_wkEBAcRmoeauGWDygmH3aB0g9QPZN7I2gaIM,19529
4
+ PythonicDISORT/_solve_for_gen_and_part_sols.py,sha256=5p5AlosW2ALjUBn6aEcPj_1pDpfxMn4MwwGorf9WLUY,14177
5
+ PythonicDISORT/pydisort.py,sha256=GRVU2cEzM1ySuPtSEeYtmRdEPnLa3vLzfA8xd1vb95w,37860
6
+ PythonicDISORT/subroutines.py,sha256=YscXrzzT2pKexe4S-30dfpMX69oUqyvQPnaMu8C1uGc,35223
7
+ pythonicdisort-1.6.dist-info/LICENSE.md,sha256=q1i1QVh0OLzK0fj6kaNRNWsM_KZca66JwWgWwbuCPlA,1092
8
+ pythonicdisort-1.6.dist-info/METADATA,sha256=uc2na6BfMyUgBFE1GSQe3RmekJa4EqIV2t-EK8ZPOAk,8699
9
+ pythonicdisort-1.6.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
10
+ pythonicdisort-1.6.dist-info/top_level.txt,sha256=aX6kCeI1E7naZz3xfw4_mIbWh5KBh9dmF3FIvgP5m3Y,15
11
+ pythonicdisort-1.6.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- PythonicDISORT/__init__.py,sha256=V1obNzaFgqInxqYJ5h_r6JHbZRxx4VM5VgzCIS6EEzI,84
2
- PythonicDISORT/_assemble_intensity_and_fluxes.py,sha256=mNhV6KjXWiZwPFCH8nuI6hPssl4PXznOn2nkdqOmS9g,29823
3
- PythonicDISORT/_solve_for_coeffs.py,sha256=8ok2xS6emv4Qtnj_5CTQkYRH8jYzKE06ypmZErQWTPg,19384
4
- PythonicDISORT/_solve_for_gen_and_part_sols.py,sha256=OjMn1JrhyC7VqZUUMjJVloNvSRWd8l-dQ1MxGt2QZbc,13216
5
- PythonicDISORT/pydisort.py,sha256=hHN4u6eG7w7QftlgS7fy-EKwU1a_aJQgk502S5kRdbo,39682
6
- PythonicDISORT/subroutines.py,sha256=Yv81a57KBBijz5VFWKxQ99mnogJo2TllazL5IVZOTJU,34940
7
- pythonicdisort-1.4.dist-info/LICENSE.md,sha256=q1i1QVh0OLzK0fj6kaNRNWsM_KZca66JwWgWwbuCPlA,1092
8
- pythonicdisort-1.4.dist-info/METADATA,sha256=TkrQNF7KQEkfFAtkb5B2LYSqcmG6Sy29eMgVOyel-zY,8309
9
- pythonicdisort-1.4.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
10
- pythonicdisort-1.4.dist-info/top_level.txt,sha256=aX6kCeI1E7naZz3xfw4_mIbWh5KBh9dmF3FIvgP5m3Y,15
11
- pythonicdisort-1.4.dist-info/RECORD,,