Skip to content

Commit bb1363a

Browse files
authored
Merge pull request #151 from ComputationalCryoEM/orientation
Include the Dirac and Polar Fourier basis Classes
2 parents a732bf8 + 756e93e commit bb1363a

File tree

8 files changed

+518
-120
lines changed

8 files changed

+518
-120
lines changed

src/aspire/basis/__init__.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,46 +152,46 @@ def mat_evaluate_t(self, X):
152152
"""
153153
return mdim_mat_fun_conj(X, len(self.sz), 1, self.evaluate_t)
154154

155-
def expand(self, v):
155+
def expand(self, x):
156156
"""
157-
Expand array in basis
157+
Obtain coefficients in the basis from those in standard coordinate basis
158158
159-
This is a similar function to `evaluate_t` but with more accuracy by
160-
using the cg optimizing of linear equation, Ax=b.
159+
This is a similar function to evaluate_t but with more accuracy by using
160+
the cg optimizing of linear equation, Ax=b.
161161
162-
If `v` is a matrix of size `basis.ct`-by-..., `B` is the change-of-basis
163-
matrix of this basis, and `x` is a matrix of size `self.sz`-by-...,
164-
the function calculates v = (B' * B)^(-1) * B' * x, where the rows
165-
of `B` and columns of `x` are read as vectorized arrays.
166-
167-
:param v: An array whose first few dimensions are to be expanded in this basis.
168-
These dimensions must equal `self.sz`.
169-
:return: The coefficients of `v` expanded in this basis. If more than
170-
one array of size `self.sz` is found in `v`, the second and higher
171-
dimensions of the return value correspond to those higher dimensions of `v`.
162+
:param x: An array whose first two or three dimensions are to be expanded
163+
the desired basis. These dimensions must equal `self.sz`.
164+
:return : The coefficients of `v` expanded in the desired basis.
165+
The first dimension of `v` is with size of `count` and the
166+
second and higher dimensions of the return value correspond to
167+
those higher dimensions of `x`.
172168
173169
"""
174-
ensure(v.shape[:self.ndim] == self.sz, f'First {self.ndim} dimensions of v must match {self.sz}.')
170+
# ensure the first dimensions with size of self.sz
171+
x, sz_roll = unroll_dim(x, self.ndim + 1)
172+
ensure(x.shape[:self.ndim] == self.sz,
173+
f'First {self.ndim} dimensions of x must match {self.sz}.')
175174

176-
v, sz_roll = unroll_dim(v, self.ndim + 1)
177-
b = self.evaluate_t(v)
178-
operator = LinearOperator(
179-
shape=(self.count, self.count),
180-
matvec=lambda x: self.evaluate_t(self.evaluate(x))
181-
)
175+
operator = LinearOperator(shape=(self.count, self.count),
176+
matvec=lambda v: self.evaluate_t(self.evaluate(v)))
182177

183178
# TODO: (from MATLAB implementation) - Check that this tolerance make sense for multiple columns in v
184-
tol = 10 * np.finfo(v.dtype).eps
179+
tol = 10*np.finfo(x.dtype).eps
185180
logger.info('Expanding array in basis')
186-
v, info = cg(operator, b, tol=tol)
187181

188-
v = v[..., np.newaxis]
182+
# number of image samples
183+
n_data = np.size(x, self.ndim)
184+
v = np.zeros((self.count, n_data), dtype=x.dtype)
189185

190-
if info != 0:
191-
raise RuntimeError('Unable to converge!')
186+
for isample in range(0, n_data):
187+
b = self.evaluate_t(x[..., isample])
188+
# TODO: need check the initial condition x0 can improve the results or not.
189+
v[..., isample], info = cg(operator, b, tol=tol)
190+
if info != 0:
191+
raise RuntimeError('Unable to converge!')
192192

193+
# return v coefficients with the first dimension of self.count
193194
v = roll_dim(v, sz_roll)
194-
195195
return v
196196

197197
def expand_t(self, v):

src/aspire/basis/dirac.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
1+
import logging
2+
import numpy as np
3+
14
from aspire.basis import Basis
5+
from aspire.utils.matrix import roll_dim, unroll_dim
6+
from aspire.utils.matlab_compat import m_flatten, m_reshape
7+
8+
logger = logging.getLogger(__name__)
29

310

411
class DiracBasis(Basis):
5-
pass
12+
"""
13+
Define a derived class for Dirac basis
14+
"""
15+
def __init__(self, sz, mask=None):
16+
"""
17+
Initialize an object for Dirac basis
18+
:param sz: The shape of the vectors for which to define the basis.
19+
:param mask: A boolean _mask of size sz indicating which coordinates
20+
to include in the basis (default np.full(sz, True)).
21+
"""
22+
if mask is None:
23+
mask = np.full(sz, True)
24+
self._mask = m_flatten(mask)
25+
26+
super().__init__(sz)
27+
28+
def _build(self):
29+
"""
30+
Build the internal data structure to Dirac basis
31+
"""
32+
logger.info('Expanding object in a Dirac basis.')
33+
self.count = np.sum(self._mask)
34+
self._sz_prod = self.nres ** self.ndim
35+
36+
def evaluate(self, v):
37+
"""
38+
Evaluate coefficients in standard coordinate basis from those in Dirac basis
39+
40+
:param v: A coefficient vector (or an array of coefficient vectors) to
41+
be evaluated. The first dimension must equal `self.count`.
42+
:return: The evaluation of the coefficient vector(s) `v` for this basis.
43+
This is an array whose first dimensions equal `self.sz` and the remaining
44+
dimensions correspond to dimensions two and higher of `v`.
45+
"""
46+
v, sz_roll = unroll_dim(v, 2)
47+
x = np.zeros(shape=(self._sz_prod,) + v.shape[1:])
48+
x[self._mask, ...] = v
49+
x = m_reshape(x, self.sz + x.shape[1:])
50+
x = roll_dim(x, sz_roll)
51+
52+
return x
53+
54+
def evaluate_t(self, x):
55+
"""
56+
Evaluate coefficient in Dirac basis from those in standard coordinate basis
57+
58+
:param x: The coefficient array to be evaluated. The first dimensions
59+
must equal `self.sz`.
60+
:return: The evaluation of the coefficient array `v` in the dual basis
61+
of `basis`. This is an array of vectors whose first dimension equals
62+
`self.count` and whose remaining dimensions correspond to
63+
higher dimensions of `v`.
64+
"""
65+
x, sz_roll = unroll_dim(x, self.ndim + 1)
66+
x = m_reshape(x, new_shape=(self._sz_prod,) + x.shape[self.ndim:])
67+
v = np.zeros(shape=(self.count, ) + x.shape[1:])
68+
v = x[self._mask, ...]
69+
v = roll_dim(v, sz_roll)
70+
71+
return v
72+
73+
def expand(self, x):
74+
return self.evaluate_t(x)

src/aspire/basis/ffb_2d.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
from numpy import pi
44
from scipy.special import jv
55
from scipy.fftpack import ifft, fft
6-
from scipy.sparse.linalg import LinearOperator, cg
76

87
from aspire.nfft import anufft3, nufft3
9-
10-
from aspire.utils import ensure
118
from aspire.utils.matrix import roll_dim, unroll_dim
129
from aspire.utils.matlab_compat import m_reshape
1310
from aspire.basis.basis_utils import lgwt
@@ -246,45 +243,3 @@ def evaluate_t(self, x):
246243
# return v coefficients with the first dimension of self.count
247244
v = roll_dim(v, sz_roll)
248245
return v
249-
250-
def expand(self, x):
251-
"""
252-
Obtain coefficients in FB basis from those in standard 2D coordinate basis
253-
254-
This is a similar function to evaluate_t but with more accuracy by using
255-
the cg optimizing of linear equation, Ax=b.
256-
257-
:param x: An array whose first two dimensions are to be expanded in FB basis.
258-
These dimensions must equal `self.sz`.
259-
:return : The coefficients of `v` expanded in FB basis. The first dimension
260-
of `v` is with size of `count` and the second and higher dimensions
261-
of the return value correspond to those higher dimensions of `x`.
262-
263-
"""
264-
# ensure the first two dimensions with size of self.sz
265-
x, sz_roll = unroll_dim(x, self.ndim + 1)
266-
x = m_reshape(x, (self.sz[0], self.sz[1], -1))
267-
ensure(x.shape[:self.ndim] == self.sz,
268-
f'First {self.ndim} dimensions of x must match {self.sz}.')
269-
270-
operator = LinearOperator(shape=(self.count, self.count),
271-
matvec=lambda v: self.evaluate_t(self.evaluate(v)))
272-
273-
# TODO: (from MATLAB implementation) - Check that this tolerance make sense for multiple columns in v
274-
tol = 10*np.finfo(x.dtype).eps
275-
logger.info('Expanding array in basis')
276-
277-
# number of image samples
278-
n_data = np.size(x, self.ndim)
279-
v = np.zeros((self.count, n_data), dtype=x.dtype)
280-
281-
for isample in range(0, n_data):
282-
b = self.evaluate_t(x[..., isample])
283-
# TODO: need check the initial condition x0 can improve the results or not.
284-
v[..., isample], info = cg(operator, b, tol=tol)
285-
if info != 0:
286-
raise RuntimeError('Unable to converge!')
287-
288-
# return v coefficients with the first dimension of self.count
289-
v = roll_dim(v, sz_roll)
290-
return v

src/aspire/basis/ffb_3d.py

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import logging
22
import numpy as np
33
from numpy import pi
4-
from scipy.sparse.linalg import LinearOperator, cg
54

65
from aspire.nfft import anufft3, nufft3
7-
from aspire.utils import ensure
86
from aspire.utils.matrix import roll_dim, unroll_dim
97
from aspire.utils.matlab_compat import m_flatten, m_reshape
108
from aspire.basis.basis_utils import sph_bessel, norm_assoc_legendre, lgwt
@@ -339,48 +337,3 @@ def evaluate_t(self, x):
339337
v[ind, :] = v_ell
340338
v = roll_dim(v, sz_roll)
341339
return v
342-
343-
def expand(self, x):
344-
345-
"""
346-
Obtain expansion coefficients in FB basis from those in standard 3D coordinate basis
347-
348-
This is a similar function to evaluate_t but with more accuracy by using
349-
the cg optimizing of linear equation, Ax=b.
350-
351-
:param x: An array whose first three dimensions are to be expanded in FB basis.
352-
These dimensions must equal `self.sz`.
353-
:return : The coefficients of `v` expanded in FB basis. The first dimension
354-
of `v` is with size of `count` and the second and higher dimensions
355-
of the return value correspond to those higher dimensions of `x`.
356-
357-
"""
358-
# TODO: this is function could be move to base class if all standard and fast versions of 2d and 3d are using
359-
# the same data structures of x and v.
360-
# ensure the first three dimensions with size of self.sz
361-
x, sz_roll = unroll_dim(x, self.ndim + 1)
362-
x = m_reshape(x, (self.sz[0], self.sz[1], self.sz[2], -1))
363-
364-
ensure(x.shape[:self.ndim] == self.sz, f'First {self.ndim} dimensions of x must match {self.sz}.')
365-
366-
operator = LinearOperator(shape=(self.count, self.count),
367-
matvec=lambda v: self.evaluate_t(self.evaluate(v)))
368-
369-
# TODO: (from MATLAB implementation) - Check that this tolerance make sense for multiple columns in v
370-
tol = 10*np.finfo(x.dtype).eps
371-
logger.info('Expanding array in basis')
372-
373-
# number of image samples
374-
n_data = np.size(x, self.ndim)
375-
v = np.zeros((self.count, n_data), dtype=x.dtype)
376-
377-
for isample in range(0, n_data):
378-
b = self.evaluate_t(x[..., isample])
379-
# TODO: need check the initial condition x0 can improve the results or not.
380-
v[..., isample], info = cg(operator, b, tol=tol)
381-
if info != 0:
382-
raise RuntimeError('Unable to converge!')
383-
384-
# return v coefficients with the first dimension of self.count
385-
v = roll_dim(v, sz_roll)
386-
return v

src/aspire/basis/polar_2d.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import logging
2+
import numpy as np
3+
import finufftpy
4+
5+
from aspire.utils import ensure
6+
from aspire.utils.matrix import roll_dim, unroll_dim
7+
from aspire.utils.matlab_compat import m_reshape
8+
from aspire.basis import Basis
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class PolarBasis2D(Basis):
14+
"""
15+
Define a derived class for polar Fourier representation for 2D images
16+
"""
17+
18+
def __init__(self, size, nrad=None, ntheta=None):
19+
"""
20+
Initialize an object for the 2D polar Fourier grid class
21+
22+
:param size: The shape of the vectors for which to define the grid.
23+
Currently only square images are supported.
24+
:param nrad: The number of points in the radial dimension.
25+
:param ntheta: The number of points in the angular dimension.
26+
"""
27+
28+
ndim = len(size)
29+
ensure(ndim == 2, 'Only two-dimensional grids are supported.')
30+
ensure(len(set(size)) == 1, 'Only square domains are supported.')
31+
32+
self.nrad = nrad
33+
if nrad is None:
34+
self.nrad = self.nres // 2
35+
36+
self.ntheta = ntheta
37+
if ntheta is None:
38+
# try to use the same number as Fast FB basis
39+
self.ntheta = 8 * self.nrad
40+
41+
super().__init__(size)
42+
43+
def _build(self):
44+
"""
45+
Build the internal data structure to 2D polar Fourier grid
46+
"""
47+
logger.info('Represent 2D image in a polar Fourier grid')
48+
49+
self.count = self.nrad * self.ntheta
50+
self._sz_prod = self.sz[0] * self.sz[1]
51+
52+
# precompute the basis functions in 2D grids
53+
self.freqs = self._precomp()
54+
55+
def _precomp(self):
56+
"""
57+
Precomute the polar Fourier grid
58+
"""
59+
omega0 = 2 * np.pi / (2 * self.nrad - 1)
60+
dtheta = 2 * np.pi / self.ntheta
61+
62+
freqs = np.zeros((2, self.nrad * self.ntheta))
63+
for i in range(self.ntheta):
64+
freqs[0, i * self.nrad: (i + 1) * self.nrad] = np.arange(self.nrad) * np.sin(i * dtheta)
65+
freqs[1, i * self.nrad: (i + 1) * self.nrad] = np.arange(self.nrad) * np.cos(i * dtheta)
66+
67+
freqs *= omega0
68+
return freqs
69+
70+
def evaluate(self, v):
71+
"""
72+
Evaluate coefficients in standard 2D coordinate basis from those in polar Fourier basis
73+
74+
:param v: A coefficient vector (or an array of coefficient vectors)
75+
in polar Fourier basis to be evaluated. The first dimension must equal to
76+
`self.count`.
77+
:return x: The evaluation of the coefficient vector(s) `x` in standard 2D
78+
coordinate basis. This is an array whose first two dimensions equal `self.sz`
79+
and the remaining dimensions correspond to dimensions two and higher of `v`.
80+
"""
81+
v, sz_roll = unroll_dim(v, 2)
82+
nimgs = v.shape[1]
83+
84+
half_size = self.nrad * self.ntheta // 2
85+
86+
v = m_reshape(v, (self.nrad, self.ntheta, nimgs))
87+
88+
v = (v[:, :self.ntheta // 2, :]
89+
+ v[:, self.ntheta // 2:, :].conj())
90+
91+
v = m_reshape(v, (half_size, nimgs))
92+
93+
# finufftpy require it to be aligned in fortran order
94+
x = np.empty((self._sz_prod, nimgs), dtype='complex128', order='F')
95+
finufftpy.nufft2d1many(self.freqs[0, :half_size],
96+
self.freqs[1, :half_size],
97+
v, 1, 1e-15, self.sz[0], self.sz[1], x)
98+
x = m_reshape(x, (self.sz[0], self.sz[1], nimgs))
99+
x = x.real
100+
# return coefficients whose first two dimensions equal to self.sz
101+
x = roll_dim(x, sz_roll)
102+
103+
return x
104+
105+
def evaluate_t(self, x):
106+
"""
107+
Evaluate coefficient in polar Fourier grid from those in standard 2D coordinate basis
108+
109+
:param x: The coefficient array in the standard 2D coordinate basis to be
110+
evaluated. The first two dimensions must equal `self.sz`.
111+
:return v: The evaluation of the coefficient array `v` in the polar Fourier grid.
112+
This is an array of vectors whose first dimension is `self.count` and
113+
whose remaining dimensions correspond to higher dimensions of `x`.
114+
"""
115+
# ensure the first two dimensions with size of self.sz
116+
x, sz_roll = unroll_dim(x, self.ndim + 1)
117+
nimgs = x.shape[2]
118+
119+
# finufftpy require it to be aligned in fortran order
120+
half_size = self.nrad * self.ntheta // 2
121+
pf = np.empty((half_size, nimgs), dtype='complex128', order='F')
122+
finufftpy.nufft2d2many(self.freqs[0, :half_size], self.freqs[1, :half_size], pf, 1, 1e-15, x)
123+
pf = m_reshape(pf, (self.nrad, self.ntheta // 2, nimgs))
124+
v = np.concatenate((pf, pf.conj()), axis=1)
125+
126+
# return v coefficients with the first dimension size of self.count
127+
v = m_reshape(v, (self.nrad * self.ntheta, nimgs))
128+
v = roll_dim(v, sz_roll)
129+
130+
return v
2.13 KB
Binary file not shown.

0 commit comments

Comments
 (0)