Skip to content

Commit e2fbc22

Browse files
committed
Initial CLMatrix class implementation.
1 parent 945ec1d commit e2fbc22

8 files changed

Lines changed: 327 additions & 301 deletions

File tree

src/aspire/abinitio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
g_sync,
66
)
77
from .commonline_base import CLOrient3D
8+
from .commonline_matrix import CLMatrix
89
from .commonline_sdp import CommonlineSDP
910
from .commonline_lud import CommonlineLUD
1011
from .commonline_irls import CommonlineIRLS

src/aspire/abinitio/commonline_base.py

Lines changed: 1 addition & 291 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import logging
22
import math
3-
import os
43

54
import numpy as np
65
import scipy.sparse as sparse
76

87
from aspire.image import Image
98
from aspire.operators import PolarFT
10-
from aspire.utils import Rotation, complex_type, fuzzy_mask, tqdm
9+
from aspire.utils import Rotation, fuzzy_mask
1110
from aspire.utils.random import choice
1211

1312
from .commonline_utils import _generate_shift_phase_and_filter
@@ -84,31 +83,7 @@ def __init__(
8483
self.mask = mask
8584
self._pf = None
8685

87-
# Sanity limit to match potential clmatrix dtype of int16.
88-
if self.n_img > (2**15 - 1):
89-
raise NotImplementedError(
90-
"Commonlines implementation limited to <2**15 images."
91-
)
92-
93-
# Auto configure GPU
94-
self.__gpu_module = None
95-
try:
96-
import cupy as cp
97-
98-
if cp.cuda.runtime.getDeviceCount() >= 1:
99-
gpu_id = cp.cuda.runtime.getDevice()
100-
logger.info(
101-
f"cupy and GPU {gpu_id} found by cuda runtime; enabling cupy."
102-
)
103-
self.__gpu_module = self.__init_cupy_module()
104-
else:
105-
logger.info("GPU not found, defaulting to numpy.")
106-
107-
except ModuleNotFoundError:
108-
logger.info("cupy not found, defaulting to numpy.")
109-
11086
# Outputs
111-
self.clmatrix = None
11287
self.rotations = None
11388
self.shifts = None
11489

@@ -172,25 +147,6 @@ def estimate_rotations(self):
172147
"""
173148
raise NotImplementedError("subclasses should implement this")
174149

175-
@property
176-
def clmatrix(self):
177-
"""
178-
Returns Common Lines Matrix.
179-
180-
Computes if `clmatrix` is None.
181-
182-
:return: Common Lines Matrix
183-
"""
184-
if self._clmatrix is None:
185-
self.build_clmatrix()
186-
else:
187-
logger.info("Using existing estimated `clmatrix`.")
188-
return self._clmatrix
189-
190-
@clmatrix.setter
191-
def clmatrix(self, value):
192-
self._clmatrix = value
193-
194150
@property
195151
def rotations(self):
196152
"""
@@ -229,228 +185,6 @@ def shifts(self):
229185
def shifts(self, value):
230186
self._shifts = value
231187

232-
def build_clmatrix(self):
233-
"""
234-
Build common-lines matrix from Fourier stack of 2D images
235-
236-
Wrapper for cpu/gpu dispatch.
237-
"""
238-
239-
logger.info("Begin building Common Lines Matrix")
240-
241-
# host/gpu dispatch
242-
if self.__gpu_module:
243-
res = self.build_clmatrix_cu()
244-
else:
245-
res = self.build_clmatrix_host()
246-
247-
# Unpack result
248-
self._shifts_1d, self.clmatrix = res
249-
250-
return self.clmatrix
251-
252-
def build_clmatrix_host(self):
253-
"""
254-
Build common-lines matrix from Fourier stack of 2D images
255-
"""
256-
257-
n_img = self.n_img
258-
n_check = self.n_check
259-
260-
if self.n_theta % 2 == 1:
261-
msg = "n_theta must be even"
262-
logger.error(msg)
263-
raise NotImplementedError(msg)
264-
265-
n_theta_half = self.n_theta // 2
266-
267-
# need to do a copy to prevent modifying self.pf for other functions
268-
pf = self.pf.copy()
269-
270-
# Allocate local variables for return
271-
# clmatrix represents the common lines matrix.
272-
# Namely, clmatrix[i,j] contains the index in image i of
273-
# the common line with image j. Note the common line index
274-
# starts from 0 instead of 1 as Matlab version. -1 means
275-
# there is no common line such as clmatrix[i,i].
276-
clmatrix = -np.ones((n_img, n_img), dtype=self.dtype)
277-
# When cl_dist[i, j] is not -1, it stores the maximum value
278-
# of correlation between image i and j for all possible 1D shifts.
279-
# We will use cl_dist[i, j] = -1 (including j<=i) to
280-
# represent that there is no need to check common line
281-
# between i and j. Since it is symmetric,
282-
# only above the diagonal entries are necessary.
283-
cl_dist = -np.ones((n_img, n_img), dtype=self.dtype)
284-
285-
# Allocate variables used for shift estimation
286-
287-
# set maximum value of 1D shift (in pixels) to search
288-
# between common-lines.
289-
max_shift = self.max_shift
290-
# Set resolution of shift estimation in pixels. Note that
291-
# shift_step can be any positive real number.
292-
shift_step = self.shift_step
293-
# 1D shift between common-lines
294-
shifts_1d = np.zeros((n_img, n_img))
295-
296-
# Prepare the shift phases to try and generate filter for common-line detection
297-
r_max = pf.shape[2]
298-
shifts, shift_phases, h = _generate_shift_phase_and_filter(
299-
r_max, max_shift, shift_step, self.dtype
300-
)
301-
302-
# Apply bandpass filter, normalize each ray of each image
303-
# Note that only use half of each ray
304-
pf = self._apply_filter_and_norm("ijk, k -> ijk", pf, r_max, h)
305-
306-
# Setup a progress bar
307-
_total_pairs_to_test = self.n_img * (self.n_check - 1) // 2
308-
pbar = tqdm(desc="Searching over common line pairs", total=_total_pairs_to_test)
309-
310-
# Search for common lines between [i, j] pairs of images.
311-
# Creating pf and building common lines are different to the Matlab version.
312-
# The random selection is implemented.
313-
for i in range(n_img - 1):
314-
p1 = pf[i]
315-
p1_real = np.real(p1)
316-
p1_imag = np.imag(p1)
317-
318-
# build the subset of j images if n_check < n_img
319-
n_remaining = n_img - i - 1
320-
n_j = min(n_remaining, n_check)
321-
subset_j = np.sort(choice(n_remaining, n_j, replace=False) + i + 1)
322-
323-
for j in subset_j:
324-
p2_flipped = np.conj(pf[j])
325-
326-
for shift in range(len(shifts)):
327-
shift_phase = shift_phases[shift]
328-
p2_shifted_flipped = (shift_phase * p2_flipped).T
329-
# Compute correlations in the positive r direction
330-
part1 = p1_real.dot(np.real(p2_shifted_flipped))
331-
# Compute correlations in the negative r direction
332-
part2 = p1_imag.dot(np.imag(p2_shifted_flipped))
333-
334-
c1 = part1 - part2
335-
sidx = c1.argmax()
336-
cl1, cl2 = np.unravel_index(sidx, c1.shape)
337-
sval = c1[cl1, cl2]
338-
339-
c2 = part1 + part2
340-
sidx = c2.argmax()
341-
cl1_2, cl2_2 = np.unravel_index(sidx, c2.shape)
342-
sval2 = c2[cl1_2, cl2_2]
343-
344-
if sval2 > sval:
345-
cl1 = cl1_2
346-
cl2 = cl2_2 + n_theta_half
347-
sval = sval2
348-
sval = 2 * sval
349-
if sval > cl_dist[i, j]:
350-
clmatrix[i, j] = cl1
351-
clmatrix[j, i] = cl2
352-
cl_dist[i, j] = sval
353-
shifts_1d[i, j] = shifts[shift]
354-
pbar.update()
355-
pbar.close()
356-
357-
return shifts_1d, clmatrix
358-
359-
def build_clmatrix_cu(self):
360-
"""
361-
Build common-lines matrix from Fourier stack of 2D images
362-
"""
363-
364-
import cupy as cp
365-
366-
n_img = self.n_img
367-
r = self.pf.shape[2]
368-
369-
if self.n_theta % 2 == 1:
370-
msg = "n_theta must be even"
371-
logger.error(msg)
372-
raise NotImplementedError(msg)
373-
374-
# Copy to prevent modifying self.pf for other functions
375-
# Simultaneously place on GPU
376-
pf = cp.array(self.pf)
377-
378-
# Allocate local variables for return
379-
# clmatrix represents the common lines matrix.
380-
# Namely, clmatrix[i,j] contains the index in image i of
381-
# the common line with image j. Note the common line index
382-
# starts from 0 instead of 1 as Matlab version. -1 means
383-
# there is no common line such as clmatrix[i,i].
384-
clmatrix = -cp.ones((n_img, n_img), dtype=np.int16)
385-
386-
# Allocate variables used for shift estimation
387-
#
388-
# Set maximum value of 1D shift (in pixels) to search
389-
# between common-lines.
390-
# Set resolution of shift estimation in pixels. Note that
391-
# shift_step can be any positive real number.
392-
#
393-
# Prepare the shift phases to try and generate filter for common-line detection
394-
#
395-
# Note the CUDA implementation has been optimized to not
396-
# compute or return diagnostic 1d shifts.
397-
_, shift_phases, h = _generate_shift_phase_and_filter(
398-
r, self.max_shift, self.shift_step, self.dtype
399-
)
400-
# Transfer to device, dtypes must match kernel header.
401-
shift_phases = cp.asarray(shift_phases, dtype=complex_type(self.dtype))
402-
403-
# Apply bandpass filter, normalize each ray of each image
404-
# Note that this only uses half of each ray
405-
pf = self._apply_filter_and_norm("ijk, k -> ijk", pf, r, h)
406-
407-
# Tranpose `pf` for better (CUDA) memory access pattern, and cast as needed.
408-
pf = cp.ascontiguousarray(pf.T, dtype=complex_type(self.dtype))
409-
410-
# Get kernel
411-
if self.dtype == np.float64:
412-
build_clmatrix_kernel = self.__gpu_module.get_function(
413-
"build_clmatrix_kernel"
414-
)
415-
elif self.dtype == np.float32:
416-
build_clmatrix_kernel = self.__gpu_module.get_function(
417-
"fbuild_clmatrix_kernel"
418-
)
419-
else:
420-
raise NotImplementedError(
421-
"build_clmatrix_kernel only implemented for float32 and float64."
422-
)
423-
424-
# Configure grid of blocks
425-
blkszx = 32
426-
# Enough blocks to cover n_img-1
427-
nblkx = (self.n_img + blkszx - 2) // blkszx
428-
blkszy = 32
429-
# Enough blocks to cover n_img
430-
nblky = (self.n_img + blkszy - 1) // blkszy
431-
432-
# Launch
433-
logger.info("Launching `build_clmatrix_kernel`.")
434-
build_clmatrix_kernel(
435-
(nblkx, nblky),
436-
(blkszx, blkszy),
437-
(
438-
n_img,
439-
pf.shape[1],
440-
r,
441-
pf,
442-
clmatrix,
443-
len(shift_phases),
444-
shift_phases,
445-
),
446-
)
447-
448-
# Copy result device arrays to host
449-
clmatrix = clmatrix.get().astype(self.dtype, copy=False)
450-
451-
# Note diagnostic 1d shifts are not computed in the CUDA implementation.
452-
return None, clmatrix
453-
454188
def estimate_shifts(self, equations_factor=1, max_memory=4000):
455189
"""
456190
Estimate 2D shifts in images
@@ -757,27 +491,3 @@ def _apply_filter_and_norm(self, subscripts, pf, r_max, h):
757491
pf /= np.linalg.norm(pf, axis=-1)[..., np.newaxis]
758492

759493
return pf
760-
761-
@staticmethod
762-
def __init_cupy_module():
763-
"""
764-
Private utility method to read in CUDA source and return as
765-
compiled CuPy module.
766-
"""
767-
768-
import cupy as cp
769-
770-
# Read in contents of file
771-
fp = os.path.join(os.path.dirname(__file__), "commonline_base.cu")
772-
with open(fp, "r") as fh:
773-
module_code = fh.read()
774-
775-
# CuPy compile the CUDA code
776-
# Note these optimizations are to steer aggresive optimization
777-
# for single precision code. Fast math will potentionally
778-
# reduce accuracy in single precision.
779-
return cp.RawModule(
780-
code=module_code,
781-
backend="nvcc",
782-
options=("-O3", "--use_fast_math", "--extra-device-vectorization"),
783-
)

src/aspire/abinitio/commonline_c2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
from scipy.linalg import eigh
55

6-
from aspire.abinitio import CLOrient3D, JSync, SyncVotingMixin
6+
from aspire.abinitio import CLMatrix, JSync, SyncVotingMixin
77
from aspire.utils import J_conjugate, Rotation, all_pairs
88

99
from .commonline_utils import (
@@ -15,7 +15,7 @@
1515
logger = logging.getLogger(__name__)
1616

1717

18-
class CLSymmetryC2(CLOrient3D, SyncVotingMixin):
18+
class CLSymmetryC2(CLMatrix, SyncVotingMixin):
1919
"""
2020
Define a class to estimate 3D orientations using common lines methods for molecules with C2 cyclic symmetry.
2121

src/aspire/abinitio/commonline_c3_c4.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
from numpy.linalg import norm, svd
55

6-
from aspire.abinitio import CLOrient3D, JSync, SyncVotingMixin
6+
from aspire.abinitio import CLMatrix, JSync, SyncVotingMixin
77
from aspire.operators import PolarFT
88
from aspire.utils import J_conjugate, Rotation, all_pairs, anorm, trange
99

@@ -16,7 +16,7 @@
1616
logger = logging.getLogger(__name__)
1717

1818

19-
class CLSymmetryC3C4(CLOrient3D, SyncVotingMixin):
19+
class CLSymmetryC3C4(CLMatrix, SyncVotingMixin):
2020
"""
2121
Define a class to estimate 3D orientations using common lines methods for molecules with
2222
C3 and C4 cyclic symmetry.

0 commit comments

Comments
 (0)