PythonicDISORT 1.2__py3-none-any.whl → 1.4__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.
@@ -1,19 +1,17 @@
1
1
  import numpy as np
2
2
  import scipy as sc
3
- from math import pi
4
3
 
5
4
 
6
5
  def _solve_for_gen_and_part_sols(
7
6
  NFourier, # Number of intensity Fourier modes
8
7
  scaled_omega_arr, # Delta-scaled single-scattering albedos
9
- mu_arr_pos, mu_arr, # Quadrature nodes for 1) upper 2) both hemispheres
10
- M_inv, W, # 1) 1 / mu; 2) quadrature weights for each hemisphere
8
+ mu_arr_pos, # Upper hemisphere quadrature nodes (cosine polar angles)
9
+ M_inv, W, # 1) 1 / `mu_arr_pos`; 2) quadrature weights for each hemisphere
11
10
  N, NQuad, NLeg, # Number of 1) upper 2) both hemispheres quadrature nodes; 3) phase function Legendre coefficients
12
11
  NLayers, # Number of layers
13
12
  weighted_scaled_Leg_coeffs, # Weighted and delta-scaled Legendre coefficients
14
- mu0, I0, # Properties of the direct beam
13
+ mu0, I0_div_4pi, # Primary properties of the direct beam
15
14
  there_is_beam_source, # Is there a beam source?
16
- Nscoeffs, # Number of isotropic source polynomial coefficients
17
15
  there_is_iso_source, # Is there an isotropic source?
18
16
  ):
19
17
  """
@@ -31,7 +29,6 @@ def _solve_for_gen_and_part_sols(
31
29
  | `NFourier` | scalar |
32
30
  | `scaled_omega_arr` | `NLayers` |
33
31
  | `mu_arr_pos` | `NQuad/2` |
34
- | `mu_arr` | `NQuad` |
35
32
  | `M_inv` | `NQuad/2` |
36
33
  | `W` | `NQuad/2` |
37
34
  | `N` | scalar |
@@ -40,9 +37,8 @@ def _solve_for_gen_and_part_sols(
40
37
  | `NLayers` | scalar |
41
38
  | `weighted_scaled_Leg_coeffs` | `NLayers x NLeg` |
42
39
  | `mu0` | scalar |
43
- | `I0` | scalar |
40
+ | `I0_div_4pi` | scalar |
44
41
  | `there_is_beam_source` | boolean |
45
- | `Nscoeffs` | scalar |
46
42
  | `there_is_iso_source` | boolean |
47
43
 
48
44
  Notable internal variables of _solve_for_gen_and_part_sols
@@ -65,16 +61,25 @@ def _solve_for_gen_and_part_sols(
65
61
  # Initialization
66
62
  # --------------------------------------------------------------------------------------------------------------------------
67
63
  ells_all = np.arange(NLeg)
68
- ind = 0
64
+ signs_all = np.ones(NLeg)
65
+ signs_all[1::2] = -1
66
+
67
+ G_trivial = np.zeros((NQuad, NQuad))
68
+ stride = NQuad + 1
69
+ G_trivial.ravel()[N : N + N * stride : stride] = 1
70
+ G_trivial.ravel()[NQuad * N : NQuad * N + N * stride : stride] = 1
69
71
 
70
72
  G_collect = np.empty((NFourier * NLayers, NQuad, NQuad))
71
73
  K_collect = np.empty((NFourier * NLayers, NQuad))
72
- alpha_arr = np.empty((NFourier * NLayers, N, N))
73
- beta_arr = np.empty((NFourier * NLayers, N, N))
74
+ A_arr = np.empty((NFourier * NLayers, NQuad, NQuad))
74
75
  no_shortcut_indices = []
75
76
  no_shortcut_indices_0 = []
77
+ ind = 0
76
78
 
77
79
  if there_is_beam_source:
80
+ mu_arr_pos_mu0 = np.append(mu_arr_pos, -mu0)
81
+
82
+ X_arr = np.empty((NFourier * NLayers, NQuad))
78
83
  B_collect = np.zeros((NFourier * NLayers, NQuad))
79
84
  if there_is_iso_source:
80
85
  G_inv_collect_0 = np.empty((NLayers, NQuad, NQuad))
@@ -88,20 +93,26 @@ def _solve_for_gen_and_part_sols(
88
93
  # --------------------------------------------------------------------------------------------------------------------------
89
94
  m_equals_0 = (m == 0)
90
95
  ells = ells_all[m:]
91
- degree_tile = np.tile(ells, (N, 1)).T
92
- fac = sc.special.poch(ells + m + 1, -2 * m)
93
- signs = np.ones(NLeg - m)
94
- signs[1::2] = -1
95
-
96
- asso_leg_term_pos = sc.special.lpmv(m, degree_tile, mu_arr_pos)
96
+ poch = sc.special.poch(ells + m + 1, -2 * m)
97
+ signs = signs_all[:NLeg - m]
98
+
99
+ if there_is_beam_source:
100
+ degree_tile = np.broadcast_to(ells[:, None], (NLeg - m, N + 1))
101
+ lpmv_all = sc.special.lpmv(m, degree_tile, mu_arr_pos_mu0)
102
+ asso_leg_term_pos = lpmv_all[:, :-1]
103
+ scalar_fac_asso_leg_term_mu0 = I0_div_4pi * (2 - m_equals_0) * poch * lpmv_all[:, -1]
104
+ else:
105
+ degree_tile = np.broadcast_to(ells[:, None], (NLeg - m, N))
106
+ asso_leg_term_pos = sc.special.lpmv(m, degree_tile, mu_arr_pos)
107
+
97
108
  asso_leg_term_neg = asso_leg_term_pos * signs[:, None]
98
- asso_leg_term_mu0 = sc.special.lpmv(m, ells, -mu0)
109
+ fac_asso_leg_term_posT = poch[None, :] * asso_leg_term_pos.T
99
110
  # --------------------------------------------------------------------------------------------------------------------------
100
111
 
101
112
  # Loop over NLayers atmospheric layers
102
113
  # --------------------------------------------------------------------------------------------------------------------------
103
114
  for l in range(NLayers):
104
- weighted_scaled_Leg_coeffs_l = weighted_scaled_Leg_coeffs[l, :][ells]
115
+ weighted_scaled_Leg_coeffs_l = weighted_scaled_Leg_coeffs[l, m:]
105
116
  scaled_omega_l = scaled_omega_arr[l]
106
117
  omega_times_Leg_coeffs = (scaled_omega_l / 2) * weighted_scaled_Leg_coeffs_l
107
118
 
@@ -109,7 +120,7 @@ def _solve_for_gen_and_part_sols(
109
120
 
110
121
  # Generate D
111
122
  # --------------------------------------------------------------------------------------------------------------------------
112
- D_temp = omega_times_Leg_coeffs[None, :] * (fac[None, :] * asso_leg_term_pos.T)
123
+ D_temp = omega_times_Leg_coeffs[None, :] * fac_asso_leg_term_posT
113
124
  D_pos = D_temp @ asso_leg_term_pos
114
125
  D_neg = D_temp @ asso_leg_term_neg
115
126
 
@@ -119,89 +130,73 @@ def _solve_for_gen_and_part_sols(
119
130
  # Assemble the coefficient matrix and additional terms
120
131
  # --------------------------------------------------------------------------------------------------------------------------
121
132
  DW = D_pos * W[None, :]
122
- np.fill_diagonal(DW, np.diag(DW) - 1)
133
+ DW.ravel()[::N + 1] -= 1
123
134
  alpha = M_inv[:, None] * DW
124
135
  beta = M_inv[:, None] * D_neg * W[None, :]
125
136
 
126
137
  # --------------------------------------------------------------------------------------------------------------------------
127
138
 
128
- # Particular solution for the direct beam source (refer to section 3.6.1 of the Comprehensive Documentation)
139
+ # Setup to solve for particular solution w.r.t. direct beam source (see section 3.6.1 of the Comprehensive Documentation)
129
140
  # --------------------------------------------------------------------------------------------------------------------------
130
141
  if there_is_beam_source:
131
142
  # Generate X
132
143
  X_temp = (
133
- (scaled_omega_l * I0 * (2 - (m == 0)) / (4 * pi))
144
+ scalar_fac_asso_leg_term_mu0
145
+ * scaled_omega_l
134
146
  * weighted_scaled_Leg_coeffs_l
135
- * (fac * asso_leg_term_mu0)
136
147
  )
137
148
  X_pos = X_temp @ asso_leg_term_pos
138
149
  X_neg = X_temp @ asso_leg_term_neg
139
- X_tilde = np.concatenate([-M_inv * X_pos, M_inv * X_neg])
140
-
141
- A = np.concatenate(
142
- [
143
- np.concatenate([-alpha, -beta], axis=1),
144
- np.concatenate([beta, alpha], axis=1),
145
- ],
146
- axis=0,
147
- )
148
- np.fill_diagonal(A, np.diag(A) + 1 / mu0)
149
- B_collect[ind, :] = -np.linalg.solve(A, X_tilde) # We moved the minus sign out
150
+ # Signs flipped due to the extra minus sign
151
+ X_arr[ind, :N] = M_inv * X_pos
152
+ X_arr[ind, N:] = -M_inv * X_neg
150
153
 
151
154
  # --------------------------------------------------------------------------------------------------------------------------
152
155
 
153
- alpha_arr[ind, :, :] = alpha
154
- beta_arr[ind, :, :] = beta
156
+ A_arr[ind, N:, :N] = beta
157
+ A_arr[ind, N:, N:] = alpha
155
158
  no_shortcut_indices.append(ind)
156
159
  if m_equals_0 and there_is_iso_source: # Keep the list empty if there is no isotropic source
157
160
  no_shortcut_indices_0.append(l)
158
161
 
159
162
  else:
160
163
  # This is a shortcut to the diagonalization results
161
- G = np.zeros((NQuad, NQuad))
162
- np.fill_diagonal(G[N:, :N], 1)
163
- np.fill_diagonal(G[:N, N:], 1)
164
-
165
- G_collect[ind, :, :] = G
166
- K_collect[ind, :] = -1 / mu_arr
164
+ G_collect[ind, :, :] = G_trivial
165
+ K_collect[ind, :N] = -M_inv
166
+ K_collect[ind, N:] = M_inv
167
167
  if m_equals_0 and there_is_iso_source:
168
- G_inv_collect_0[l, :, :] = G
168
+ G_inv_collect_0[l, :, :] = G_trivial
169
169
 
170
170
  ind += 1
171
171
 
172
172
  if len(no_shortcut_indices) > 0:
173
-
173
+ A_arr = A_arr[no_shortcut_indices, :, :]
174
+ alpha_arr = A_arr[:, N:, N:]
175
+ beta_arr = A_arr[:, N:, :N]
176
+
174
177
  # Diagonalization of coefficient matrix (refer to section 3.4.2 of the Comprehensive Documentation)
175
178
  # --------------------------------------------------------------------------------------------------------------------------
176
- alpha_arr = alpha_arr[no_shortcut_indices, :, :]
177
- beta_arr = beta_arr[no_shortcut_indices, :, :]
178
-
179
+ apb = alpha_arr + beta_arr
180
+ amb = alpha_arr - beta_arr
179
181
  K_squared_arr, eigenvecs_GpG_arr = np.linalg.eig(
180
- np.einsum(
181
- "lij, ljk -> lik", alpha_arr - beta_arr, alpha_arr + beta_arr, optimize=True
182
- ),
182
+ np.einsum("lij, ljk -> lik", amb, apb, optimize=True)
183
183
  )
184
184
 
185
185
  # Eigenvalues arranged negative then positive, from largest to smallest magnitude
186
- K_arr = np.concatenate((-np.sqrt(K_squared_arr), np.sqrt(K_squared_arr)), axis=1)
187
- eigenvecs_GpG_arr = np.concatenate((eigenvecs_GpG_arr, eigenvecs_GpG_arr), axis=2)
188
- eigenvecs_GmG_arr = (
189
- np.einsum(
190
- "lij, ljk -> lik", alpha_arr + beta_arr, eigenvecs_GpG_arr, optimize=True
191
- )
192
- / K_arr[:, None, :]
193
- )
194
-
195
- # Eigenvector matrices
196
- G_arr = np.concatenate(
197
- (
198
- (eigenvecs_GpG_arr - eigenvecs_GmG_arr) / 2,
199
- (eigenvecs_GpG_arr + eigenvecs_GmG_arr) / 2,
200
- ),
201
- axis=1,
202
- )
186
+ K_arr_pos = np.sqrt(K_squared_arr)
187
+ K_arr = np.concatenate((-K_arr_pos, K_arr_pos), axis=1)
188
+
189
+ # Build eigenvector matrix from four blocks
190
+ eigenvecs_GpG_arr /= 2
191
+ eigenvecs_GmG_arr = np.einsum("lij, ljk -> lik", apb, eigenvecs_GpG_arr / K_arr_pos[:, None, :], optimize=True)
192
+ GpG_p_GmG_div_2 = eigenvecs_GpG_arr + eigenvecs_GmG_arr
193
+ GpG_m_GmG_div_2 = eigenvecs_GpG_arr - eigenvecs_GmG_arr
194
+
195
+ G_collect[no_shortcut_indices, :N, :N] = GpG_p_GmG_div_2
196
+ G_collect[no_shortcut_indices, N:, N:] = GpG_p_GmG_div_2
197
+ G_collect[no_shortcut_indices, :N, N:] = GpG_m_GmG_div_2
198
+ G_collect[no_shortcut_indices, N:, :N] = GpG_m_GmG_div_2
203
199
 
204
- G_collect[no_shortcut_indices, :, :] = G_arr
205
200
  K_collect[no_shortcut_indices, :] = K_arr
206
201
  if len(no_shortcut_indices_0) > 0: # If there is no isotropic source this list will be empty
207
202
  G_inv_collect_0[no_shortcut_indices_0, :, :] = np.linalg.inv(
@@ -209,6 +204,17 @@ def _solve_for_gen_and_part_sols(
209
204
  )
210
205
 
211
206
  # --------------------------------------------------------------------------------------------------------------------------
207
+
208
+ if there_is_beam_source:
209
+
210
+ # Solve for particular solution w.r.t. direct beam source (refer to section 3.6.1 of the Comprehensive Documentation)
211
+ # --------------------------------------------------------------------------------------------------------------------------
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)
216
+
217
+ # --------------------------------------------------------------------------------------------------------------------------
212
218
 
213
219
  outputs = (
214
220
  G_collect.reshape((NFourier, NLayers, NQuad, NQuad)),