1+ from __future__ import annotations
2+
13import numpy as np
24import torch
35
46from s2fft .sampling import s2_samples as samples
57from s2fft .sampling import so3_samples as wigner_samples
68
79
10+ def complex_normal (
11+ rng : np .random .Generator ,
12+ size : int | tuple [int ],
13+ var : float ,
14+ ) -> np .ndarray :
15+ """
16+ Generate array of samples from zero-mean complex normal distribution.
17+
18+ For `z ~ ComplexNormal(0, var)` we have that `imag(z) ~ Normal(0, var/2)` and
19+ `real(z) ~ Normal(0, var/2)` where `Normal(μ, σ²)` is the (real-valued) normal
20+ distribution with mean parameter `μ` and variance parameter `σ²`.
21+
22+ Args:
23+ rng: Numpy random generator object to generate samples using.
24+ size: Output shape of array to generate.
25+ var: Variance of complex normal distribution to generate samples from.
26+
27+ Returns:
28+ Complex-valued array of shape `size` contained generated samples.
29+
30+ """
31+ return (rng .standard_normal (size ) + 1j * rng .standard_normal (size )) * (
32+ var / 2
33+ ) ** 0.5
34+
35+
36+ def complex_el_and_m_indices (L : int , min_el : int ) -> tuple [np .ndarray , np .ndarray ]:
37+ """
38+ Generate pairs of el, m indices for accessing complex harmonic coefficients.
39+
40+ Equivalent to nested list-comprehension based implementation
41+
42+ ```
43+ el_indices, m_indices = np.array(
44+ [(el, m) for el in range(min_el, L) for m in range(1, el + 1)]
45+ ).T
46+ ```
47+
48+ For `L, min_el = 1024, 0`, this implementation is around 80x quicker in
49+ benchmarks compared to list-comprehension implementation.
50+
51+ Args:
52+ L: Harmonic band-limit.
53+ min_el: Inclusive lower-bound for el indices.
54+
55+ Returns:
56+ Tuple `(el_indices, m_indices)` with both entries 1D integer-valued NumPy arrays
57+ of same size, with values of corresponding entries corresponding to pairs of
58+ el and m indices.
59+
60+ """
61+ el_indices , m_indices = np .tril_indices (m = L , k = - 1 , n = L )
62+ m_indices += 1
63+ if min_el > 0 :
64+ in_range_el = el_indices >= min_el
65+ el_indices = el_indices [in_range_el ]
66+ m_indices = m_indices [in_range_el ]
67+ return el_indices , m_indices
68+
69+
870def generate_flm (
971 rng : np .random .Generator ,
1072 L : int ,
1173 L_lower : int = 0 ,
1274 spin : int = 0 ,
1375 reality : bool = False ,
1476 using_torch : bool = False ,
15- ) -> np .ndarray :
77+ ) -> np .ndarray | torch . Tensor :
1678 r"""
1779 Generate a 2D set of random harmonic coefficients.
1880
@@ -37,20 +99,24 @@ def generate_flm(
3799
38100 """
39101 flm = np .zeros (samples .flm_shape (L ), dtype = np .complex128 )
40-
41- for el in range (max (L_lower , abs (spin )), L ):
42- if reality :
43- flm [el , 0 + L - 1 ] = rng .normal ()
44- else :
45- flm [el , 0 + L - 1 ] = rng .normal () + 1j * rng .normal ()
46-
47- for m in range (1 , el + 1 ):
48- flm [el , m + L - 1 ] = rng .normal () + 1j * rng .normal ()
49- if reality :
50- flm [el , - m + L - 1 ] = (- 1 ) ** m * np .conj (flm [el , m + L - 1 ])
51- else :
52- flm [el , - m + L - 1 ] = rng .normal () + 1j * rng .normal ()
53-
102+ min_el = max (L_lower , abs (spin ))
103+ # m = 0 coefficients are always real
104+ flm [min_el :L , L - 1 ] = rng .standard_normal (L - min_el )
105+ # Construct arrays of m and el indices for entries in flm corresponding to complex-
106+ # valued coefficients (m > 0)
107+ el_indices , m_indices = complex_el_and_m_indices (L , min_el )
108+ len_indices = len (m_indices )
109+ # Generate independent complex coefficients for positive m
110+ flm [el_indices , L - 1 + m_indices ] = complex_normal (rng , len_indices , var = 2 )
111+ if reality :
112+ # Real-valued signal so set complex coefficients for negative m using conjugate
113+ # symmetry such that flm[el, L - 1 - m] = (-1)**m * flm[el, L - 1 + m].conj
114+ flm [el_indices , L - 1 - m_indices ] = (- 1 ) ** m_indices * (
115+ flm [el_indices , L - 1 + m_indices ].conj ()
116+ )
117+ else :
118+ # Non-real signal so generate independent complex coefficients for negative m
119+ flm [el_indices , L - 1 - m_indices ] = complex_normal (rng , len_indices , var = 2 )
54120 return torch .from_numpy (flm ) if using_torch else flm
55121
56122
@@ -61,7 +127,7 @@ def generate_flmn(
61127 L_lower : int = 0 ,
62128 reality : bool = False ,
63129 using_torch : bool = False ,
64- ) -> np .ndarray :
130+ ) -> np .ndarray | torch . Tensor :
65131 r"""
66132 Generate a 3D set of random Wigner coefficients.
67133
@@ -87,26 +153,49 @@ def generate_flmn(
87153
88154 """
89155 flmn = np .zeros (wigner_samples .flmn_shape (L , N ), dtype = np .complex128 )
90-
91156 for n in range (- N + 1 , N ):
92- for el in range (max (L_lower , abs (n )), L ):
93- if reality :
94- flmn [N - 1 + n , el , 0 + L - 1 ] = rng .normal ()
95- flmn [N - 1 - n , el , 0 + L - 1 ] = (- 1 ) ** n * flmn [
96- N - 1 + n ,
97- el ,
98- 0 + L - 1 ,
99- ]
100- else :
101- flmn [N - 1 + n , el , 0 + L - 1 ] = rng .normal () + 1j * rng .normal ()
102-
103- for m in range (1 , el + 1 ):
104- flmn [N - 1 + n , el , m + L - 1 ] = rng .normal () + 1j * rng .normal ()
105- if reality :
106- flmn [N - 1 - n , el , - m + L - 1 ] = (- 1 ) ** (m + n ) * np .conj (
107- flmn [N - 1 + n , el , m + L - 1 ]
108- )
109- else :
110- flmn [N - 1 + n , el , - m + L - 1 ] = rng .normal () + 1j * rng .normal ()
111-
157+ min_el = max (L_lower , abs (n ))
158+ # Separately deal with m = 0 case
159+ if reality :
160+ if n == 0 :
161+ # For m = n = 0
162+ # flmn[N - 1, el, L - 1] = flmn[N - 1, el, L - 1].conj (real-valued)
163+ # Generate independent real coefficients for n = 0
164+ flmn [N - 1 , min_el :L , L - 1 ] = rng .standard_normal (L - min_el )
165+ elif n > 0 :
166+ # Generate independent complex coefficients for positive n
167+ flmn [N - 1 + n , min_el :L , L - 1 ] = complex_normal (
168+ rng , L - min_el , var = 2
169+ )
170+ # For m = 0, n > 0
171+ # flmn[N - 1 - n, el, L - 1] = (-1)**n * flmn[N - 1 + n, el, L - 1].conj
172+ flmn [N - 1 - n , min_el :L , L - 1 ] = (- 1 ) ** n * (
173+ flmn [N - 1 + n , min_el :L , L - 1 ].conj ()
174+ )
175+ else :
176+ flmn [N - 1 + n , min_el :L , L - 1 ] = complex_normal (rng , L - min_el , var = 2 )
177+ # Construct arrays of m and el indices for entries in flmn slices for n
178+ # corresponding to complex-valued coefficients (m > 0)
179+ el_indices , m_indices = complex_el_and_m_indices (L , min_el )
180+ len_indices = len (m_indices )
181+ # Generate independent complex coefficients for positive m
182+ flmn [N - 1 + n , el_indices , L - 1 + m_indices ] = complex_normal (
183+ rng , len_indices , var = 2
184+ )
185+ if reality :
186+ # Real-valued signal so set complex coefficients for negative m using
187+ # conjugate symmetry relationship
188+ # flmn[N - 1 - n, el, L - 1 - m] =
189+ # (-1)**(m + n) * flmn[N - 1 + n, el, L - 1 + m].conj
190+ # As (m_indices + n) can be negative use floating point value (-1.0) as
191+ # base of exponentation operation to avoid Numpy
192+ # 'ValueError: Integers to negative integer powers are not allowed' error
193+ flmn [N - 1 - n , el_indices , L - 1 - m_indices ] = (- 1.0 ) ** (
194+ m_indices + n
195+ ) * flmn [N - 1 + n , el_indices , L - 1 + m_indices ].conj ()
196+ else :
197+ # Complex signal so generate independent complex coefficients for negative m
198+ flmn [N - 1 + n , el_indices , L - 1 - m_indices ] = complex_normal (
199+ rng , len_indices , var = 2
200+ )
112201 return torch .from_numpy (flmn ) if using_torch else flmn
0 commit comments