Skip to content

Commit 2265a69

Browse files
committed
Vectorize estimate_shifts to remove symmetry loop
1 parent c60c03e commit 2265a69

1 file changed

Lines changed: 93 additions & 91 deletions

File tree

src/aspire/abinitio/commonline_base.py

Lines changed: 93 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,12 @@ def _get_shift_equations_approx(self):
264264
# `estimate_shifts()` requires that rotations have already been estimated.
265265
rotations = self.rotations
266266

267-
# Apply symmetry group to rotations
267+
# Apply symmetry group to rotations.
268+
# Symmetry copies add more equations for the same per-image shift unknowns.
268269
sym_rots = self.src.symmetry_group.matrices.astype(self.dtype, copy=False)
269270
n_sym = len(sym_rots)
270271

271-
# Estimate number of equations that will be used to calculate the shifts,
272-
# taking into account particle symmetry.
272+
# Estimate base image-pair equations, then expand each pair by symmetry.
273273
n_pair_equations = self._estimate_num_shift_equations(n_img)
274274
n_equations = n_pair_equations * n_sym
275275

@@ -295,94 +295,88 @@ def _get_shift_equations_approx(self):
295295

296296
d_theta = np.pi / n_theta_half
297297

298-
# Generate two index lists for [i, j] pairs of images
298+
# Generate base [i, j] image pairs before symmetry expansion.
299299
idx_i, idx_j = self._generate_index_pairs(n_pair_equations)
300300

301-
# Go through all shift equations in the size of n_equations
302-
# Iterate over the common lines pairs and for each pair find the 1D
303-
# relative shift between the two Fourier lines in the pair.
301+
# Filter, normalize, and conjugate all rays once instead of once per equation.
302+
# Conjugation uses ray from opposite side of origin.
303+
# Correpsonds to `freqs` convention in PFT,
304+
# where the legacy code used a negated frequency grid.
305+
pf = np.conj(self._apply_filter_and_norm("ijk, k -> ijk", pf, r_max, h))
306+
307+
# Iterate over image pairs; each iteration fills one block of n_sym
308+
# symmetry-induced common-line shift equations.
304309
for pair_eq_idx in range(n_pair_equations):
305310
i = idx_i[pair_eq_idx]
306311
j = idx_j[pair_eq_idx]
312+
rows = pair_eq_idx + np.arange(n_sym) * n_pair_equations
307313

308-
for sym_idx, g in enumerate(sym_rots):
309-
shift_eq_idx = pair_eq_idx + sym_idx * n_pair_equations
314+
# Common lines for Ri against all symmetry copies g @ Rj.
315+
Rjs = sym_rots @ rotations[j]
316+
c_ij, c_ji = self._get_cl_indices_from_rot_pairs(
317+
rotations[i], Rjs, n_theta_half
318+
)
310319

311-
# get the common line indices based on the rotations from i and j images
312-
c_ij, c_ji = self._get_cl_indices_from_rot_pair(
313-
rotations[i],
314-
g @ rotations[j],
315-
n_theta_half,
320+
# Extract the Fourier rays that correspond to the common lines
321+
pf_i = pf[i, c_ij] # shape (n_sym, n_rad)
322+
323+
# Track which symmetry-induced rays in image j use the opposite ray direction.
324+
is_pf_j_flipped = c_ji >= n_theta_half
325+
pf_j = pf[j, c_ji % n_theta_half]
326+
327+
# Apply candidate 1D shifts to all symmetry-induced rays for this image pair.
328+
pf_i_stack = pf_i[:, :, None] * shift_phases.T[None, :, :]
329+
pf_i_flipped_stack = np.conj(pf_i)[:, :, None] * shift_phases.T[None, :, :]
330+
331+
c1 = 2 * np.sum(pf_i_stack.conj() * pf_j[:, :, None], axis=1).real
332+
c2 = 2 * np.sum(pf_i_flipped_stack.conj() * pf_j[:, :, None], axis=1).real
333+
334+
# Pick the best candidate shift for each symmetry-induced ray pair.
335+
sidx1 = np.argmax(c1, axis=1)
336+
sidx2 = np.argmax(c2, axis=1)
337+
338+
score1 = c1[np.arange(n_sym), sidx1]
339+
score2 = c2[np.arange(n_sym), sidx2]
340+
sidx = np.where(score1 > score2, sidx1, sidx2)
341+
dx = -self.offsets_max_shift + sidx * self.offsets_shift_step
342+
343+
# Angle(s) of common ray(s) in image i
344+
shift_alpha = c_ij * d_theta
345+
# Angle(s) of common ray(s) in image j
346+
shift_beta = c_ji * d_theta
347+
# Row indices to construct the sparse equations
348+
shift_i[rows] = rows[:, None]
349+
# All symmetry rows for this pair use the same set of image shift unknowns.
350+
shift_j[rows] = [2 * i, 2 * i + 1, 2 * j, 2 * j + 1]
351+
# Right hand side of the current equation(s)
352+
shift_b[rows] = dx
353+
354+
# Initialize shift equation block.
355+
# One four-coefficient equation row per symmetry-induced common line.
356+
eq = np.empty((n_sym, 4), dtype=self.dtype)
357+
358+
# Compute the coefficients of the current block of equations.
359+
not_flipped = ~is_pf_j_flipped
360+
eq[not_flipped] = np.column_stack(
361+
(
362+
np.sin(shift_alpha[not_flipped]),
363+
np.cos(shift_alpha[not_flipped]),
364+
-np.sin(shift_beta[not_flipped]),
365+
-np.cos(shift_beta[not_flipped]),
316366
)
367+
)
317368

318-
# Extract the Fourier rays that correspond to the common line
319-
pf_i = pf[i, c_ij]
320-
321-
# Check whether need to flip or not Fourier ray of j image
322-
# Is the common line in image j in the positive
323-
# direction of the ray (is_pf_j_flipped=False) or in the
324-
# negative direction (is_pf_j_flipped=True).
325-
is_pf_j_flipped = c_ji >= n_theta_half
326-
if not is_pf_j_flipped:
327-
pf_j = pf[j, c_ji]
328-
else:
329-
pf_j = pf[j, c_ji - n_theta_half]
330-
331-
# Use ray from opposite side of origin.
332-
# Correpsonds to `freqs` convention in PFT,
333-
# where the legacy code used a negated frequency grid.
334-
pf_i, pf_j = np.conj(pf_i), np.conj(pf_j)
335-
336-
# perform bandpass filter, normalize each ray of each image,
337-
pf_i = self._apply_filter_and_norm("i, i -> i", pf_i, r_max, h)
338-
pf_j = self._apply_filter_and_norm("i, i -> i", pf_j, r_max, h)
339-
340-
# apply the shifts to images
341-
pf_i_flipped = np.conj(pf_i)
342-
pf_i_stack = pf_i[:, None] * shift_phases.T
343-
pf_i_flipped_stack = pf_i_flipped[:, None] * shift_phases.T
344-
345-
c1 = 2 * np.dot(pf_i_stack.T.conj(), pf_j).real
346-
c2 = 2 * np.dot(pf_i_flipped_stack.T.conj(), pf_j).real
347-
348-
# find the indices for the maximum values
349-
# and apply corresponding shifts
350-
sidx1 = np.argmax(c1)
351-
sidx2 = np.argmax(c2)
352-
sidx = sidx1 if c1[sidx1] > c2[sidx2] else sidx2
353-
dx = -self.offsets_max_shift + sidx * self.offsets_shift_step
354-
355-
# angle of common ray in image i
356-
shift_alpha = c_ij * d_theta
357-
# Angle of common ray in image j.
358-
shift_beta = c_ji * d_theta
359-
# Row index to construct the sparse equations
360-
shift_i[shift_eq_idx] = shift_eq_idx
361-
# Columns of the shift variables that correspond to the current pair [i, j]
362-
shift_j[shift_eq_idx] = [2 * i, 2 * i + 1, 2 * j, 2 * j + 1]
363-
# Right hand side of the current equation
364-
shift_b[shift_eq_idx] = dx
365-
366-
# Compute the coefficients of the current equation
367-
if not is_pf_j_flipped:
368-
shift_eq[shift_eq_idx] = np.array(
369-
[
370-
np.sin(shift_alpha),
371-
np.cos(shift_alpha),
372-
-np.sin(shift_beta),
373-
-np.cos(shift_beta),
374-
]
375-
)
376-
else:
377-
shift_beta = shift_beta - np.pi
378-
shift_eq[shift_eq_idx] = np.array(
379-
[
380-
-np.sin(shift_alpha),
381-
-np.cos(shift_alpha),
382-
-np.sin(shift_beta),
383-
-np.cos(shift_beta),
384-
]
385-
)
369+
beta_flipped = shift_beta[is_pf_j_flipped] - np.pi
370+
eq[is_pf_j_flipped] = np.column_stack(
371+
(
372+
-np.sin(shift_alpha[is_pf_j_flipped]),
373+
-np.cos(shift_alpha[is_pf_j_flipped]),
374+
-np.sin(beta_flipped),
375+
-np.cos(beta_flipped),
376+
)
377+
)
378+
379+
shift_eq[rows] = eq
386380

387381
# create sparse matrix object only containing non-zero elements
388382
shift_equations = sparse.csr_matrix(
@@ -471,18 +465,26 @@ def _get_cl_indices(self, rotations, i, j, n_theta):
471465

472466
return c_ij, c_ji
473467

474-
def _get_cl_indices_from_rot_pair(self, Ri, Rj, n_theta):
468+
def _get_cl_indices_from_rot_pairs(self, Ri, Rjs, n_theta):
475469
"""
476-
Get common-line indices for an explicit pair of rotation matrices.
470+
Get common-line indices for one rotation Ri and multiple rotations Rjs.
477471
"""
478-
rotations = Rotation(np.stack((Ri, Rj))).invert()
479-
c_ij, c_ji = rotations.common_lines(0, 1, 2 * n_theta)
472+
ell = 2 * n_theta
480473

481-
if c_ij >= n_theta:
482-
c_ij -= n_theta
483-
c_ji -= n_theta
484-
if c_ji < 0:
485-
c_ji += 2 * n_theta
474+
# Match _get_cl_indices, which calls
475+
# Rotation(np.stack((Ri, Rj))).invert().common_lines(i, j, ...).
476+
ut = np.swapaxes(Rjs, -1, -2) @ Ri
477+
478+
alpha_ij = np.arctan2(ut[:, 2, 0], -ut[:, 2, 1]) + np.pi
479+
alpha_ji = np.arctan2(-ut[:, 0, 2], ut[:, 1, 2]) + np.pi
480+
481+
c_ij = np.mod(np.round(alpha_ij * ell / (2 * np.pi)), ell).astype(int)
482+
c_ji = np.mod(np.round(alpha_ji * ell / (2 * np.pi)), ell).astype(int)
483+
484+
mask = c_ij >= n_theta
485+
c_ij[mask] -= n_theta
486+
c_ji[mask] -= n_theta
487+
c_ji[c_ji < 0] += ell
486488

487489
return c_ij, c_ji
488490

0 commit comments

Comments
 (0)