From 28d11833d512388a64e1df3ab0648df0420573c0 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Sat, 2 Apr 2022 20:30:03 -0700 Subject: [PATCH 01/52] I have no idea if this works I can't install torch to test as its ssh certificate is dead --- .../iterative_refinement/expectation_maximization.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ebaa780..7703bfa 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -1,6 +1,7 @@ """Iterative refinement with Bayesian expectation maximization.""" import numpy as np +import torch from simSPI.transfer import eval_ctf @@ -471,7 +472,7 @@ def insert_slice(slice_real, xyz, n_pix): slice_real : float64 arr Shape (n_pix, n_pix) the slice of interest. xyz : arr - Shape (n_pix**2, 3) plane corresponding to slice rotation. + Shape (3, n_pix**2) plane corresponding to slice rotation. n_pix : int Number of pixels. @@ -485,10 +486,10 @@ def insert_slice(slice_real, xyz, n_pix): otherwise 0. Shape (n_pix, n_pix, n_pix) """ - shape = xyz.shape[0] - count_3d = np.ones((n_pix, n_pix, n_pix)) - count_3d[0, 0, 0] *= shape - inserted_slice_3d = np.ones((n_pix, n_pix, n_pix)) + inserted_slice_tensor = torch.sparse_coo_tensor(xyz, slice_real.reshape((n_pix**2,), (3, n_pix**2, 1))) + count_tensor = torch.sparse_coo_tensor(xyz, np.ones((n_pix**2,)), (3, n_pix**2, 1)) + inserted_slice_3d = inserted_slice_tensor.coalesce().numpy() + count_3d = count_tensor.coalesce().numpy() return inserted_slice_3d, count_3d @staticmethod From 2f36c647137c8d5af18bb449dc2f272e4eddd8ba Mon Sep 17 00:00:00 2001 From: thisTyler Date: Sun, 3 Apr 2022 20:56:57 -0700 Subject: [PATCH 02/52] This one actually works --- .../iterative_refinement/expectation_maximization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 7703bfa..e7c220b 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -486,10 +486,10 @@ def insert_slice(slice_real, xyz, n_pix): otherwise 0. Shape (n_pix, n_pix, n_pix) """ - inserted_slice_tensor = torch.sparse_coo_tensor(xyz, slice_real.reshape((n_pix**2,), (3, n_pix**2, 1))) - count_tensor = torch.sparse_coo_tensor(xyz, np.ones((n_pix**2,)), (3, n_pix**2, 1)) - inserted_slice_3d = inserted_slice_tensor.coalesce().numpy() - count_3d = count_tensor.coalesce().numpy() + inserted_slice_tensor = torch.sparse_coo_tensor(xyz + n_pix // 2, slice_real.reshape((n_pix**2,)), (n_pix, n_pix, n_pix)) + count_tensor = torch.sparse_coo_tensor(xyz + n_pix // 2, np.ones((n_pix**2,)), (n_pix, n_pix, n_pix)) + inserted_slice_3d = inserted_slice_tensor.to_dense().numpy() + count_3d = count_tensor.to_dense().numpy() return inserted_slice_3d, count_3d @staticmethod From 684958b6c81b10406f5b8169d6337d29f2e72304 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Mon, 4 Apr 2022 00:43:37 -0700 Subject: [PATCH 03/52] Added generate/insert test --- tests/test_expectation_maximization.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 598179f..b4d6f59 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -180,13 +180,27 @@ def test_apply_wiener_filter(test_ir, n_pix): def test_insert_slice(test_ir, n_pix): - """Test insertion of particle slice.""" - particle_slice = np.ones((n_pix, n_pix)) - xyz = test_ir.generate_xy_plane(n_pix) + """Test insertion of particle slice. + + Pull a slice out, put it back in. See if it's the same. + """ + xy_plane = test_ir.generate_xy_plane(n_pix) + map_plane_ones = np.zeros((n_pix, n_pix, n_pix)) + map_plane_ones[2] = np.ones((n_pix, n_pix)) + + rot_90deg_about_y = np.array( + [ + [[0, 0, 1], [0, 1, 0], [-1, 0, 0]], + ] + ) + + slices, xyz_rotated_planes = test_ir.generate_slices( + map_plane_ones, xy_plane, n_pix, rot_90deg_about_y + ) - inserted, count = test_ir.insert_slice(particle_slice, xyz, n_pix) - assert inserted.shape == (n_pix, n_pix, n_pix) - assert count.shape == (n_pix, n_pix, n_pix) + inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], n_pix) + assert np.allclose(inserted, map_plane_ones) + assert np.allclose(count, map_plane_ones) def test_compute_fsc(test_ir, n_pix): From c1012c030536df8997a82645e7ca48efe95dc16d Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 07:46:34 +0000 Subject: [PATCH 04/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 8 ++++++-- tests/test_expectation_maximization.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index e7c220b..071aacc 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -486,8 +486,12 @@ def insert_slice(slice_real, xyz, n_pix): otherwise 0. Shape (n_pix, n_pix, n_pix) """ - inserted_slice_tensor = torch.sparse_coo_tensor(xyz + n_pix // 2, slice_real.reshape((n_pix**2,)), (n_pix, n_pix, n_pix)) - count_tensor = torch.sparse_coo_tensor(xyz + n_pix // 2, np.ones((n_pix**2,)), (n_pix, n_pix, n_pix)) + inserted_slice_tensor = torch.sparse_coo_tensor( + xyz + n_pix // 2, slice_real.reshape((n_pix**2,)), (n_pix, n_pix, n_pix) + ) + count_tensor = torch.sparse_coo_tensor( + xyz + n_pix // 2, np.ones((n_pix**2,)), (n_pix, n_pix, n_pix) + ) inserted_slice_3d = inserted_slice_tensor.to_dense().numpy() count_3d = count_tensor.to_dense().numpy() return inserted_slice_3d, count_3d diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index b4d6f59..329d21e 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -181,7 +181,7 @@ def test_apply_wiener_filter(test_ir, n_pix): def test_insert_slice(test_ir, n_pix): """Test insertion of particle slice. - + Pull a slice out, put it back in. See if it's the same. """ xy_plane = test_ir.generate_xy_plane(n_pix) From 2d8dc4414a26a4f7585e58a56d59548f506b5939 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Mon, 4 Apr 2022 00:55:58 -0700 Subject: [PATCH 05/52] Fixed test --- tests/test_expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 541ca86..cb6e274 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -233,7 +233,7 @@ def test_insert_slice(test_ir, n_pix): """ xy_plane = test_ir.generate_xy_plane(n_pix) map_plane_ones = np.zeros((n_pix, n_pix, n_pix)) - map_plane_ones[2] = np.ones((n_pix, n_pix)) + map_plane_ones[n_pix // 2] = np.ones((n_pix, n_pix)) rot_90deg_about_y = np.array( [ From c7d5931c40db42dcb38a7ba28c5afd3740086a65 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 16:08:11 -0700 Subject: [PATCH 06/52] Change to scipy interpolation. Altered generate_slices to give a volume to rotated xy_plane, added generate_xyz_voxels as a 3D counterpart to generate_xy_plane. --- .../expectation_maximization.py | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ca759dc..8168588 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -4,6 +4,7 @@ import torch from geomstats.geometry import special_orthogonal from scipy.ndimage import map_coordinates +from scipy.interpolate import griddata from simSPI.transfer import eval_ctf @@ -351,7 +352,35 @@ def generate_xy_plane(n_pix): return xy_plane @staticmethod - def generate_slices(map_3d_f, xy_plane, n_pix, rots): + def generate_xyz_voxels(n_pix): + """Generate (x,y,z) cube. + + x, y, z axis values range [-n // 2, ..., n // 2 - 1] + + Parameters + ---------- + n_pix : int + Number of pixels along one edge of the cube. + + Returns + ------- + xyz : arr + Array describing xyz cube in space. + Shape (3, n_pix**3) + """ + axis_pts = np.arange(-n_pix // 2, n_pix // 2) + grid = np.meshgrid(axis_pts, axis_pts, axis_pts) + + xyz = np.zeros((3, n_pix**2)) + + for d in range(3): + xyz[d, :] = grid[d].flatten() + xyz[:, [0, 1]] = xyz[:, [1, 0]] + + return xyz + + @staticmethod + def generate_slices(map_3d_f, xy_plane, n_pix, rots, z_offset = 0.05): """Generate slice coordinates by rotating xy plane. Interpolate values from map_3d_f onto 3D coordinates. @@ -390,8 +419,8 @@ def generate_slices(map_3d_f, xy_plane, n_pix, rots): of projection of rotated map_3d_f. Shape (n_rotations, n_pix, n_pix) xyz_rotated : arr - Rotated xy planes. - Shape (n_rotations, 3, n_pix**2) + Rotated xy planes, with 3D depth added according to z_offset. + Shape (n_rotations, 3, 3 * n_pix**2) Notes @@ -424,11 +453,13 @@ def generate_slices(map_3d_f, xy_plane, n_pix, rots): slices = np.empty((n_rotations, map_3d_f.shape[0], map_3d_f.shape[1])) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, n_pix**2)) + xyz_rotated = np.empty((n_rotations, 3, 3*n_pix**2)) + offset = np.array([[0,],[0,],[z_offset,]]) + xy_plane = np.concatenate((xy_plane + offset, xy_plane, xy_plane - offset), axis=1) for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = map_coordinates(map_3d_f, xyz_rotated[i] + n_pix // 2).reshape( + slices[i] = map_coordinates(map_3d_f, xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2).reshape( (n_pix, n_pix) ) @@ -521,15 +552,20 @@ def apply_wiener_filter(projection, ctf, small_number): return projection_wfilter_f @staticmethod - def insert_slice(slice_real, xyz, n_pix): + def insert_slice(slice_real, xy_rotated, n_pix): """Rotate slice and interpolate onto a 3D grid. + Rotated xy-planes are expected to be of nonzero depth (i.e. a rotated + 2D plane with some small added z-depth to give "volume" to the slice in + order for interpolation to be feasible). The slice values are constant + along the depth axis of the slice. + Parameters ---------- slice_real : float64 arr Shape (n_pix, n_pix) the slice of interest. - xyz : arr - Shape (3, n_pix**2) plane corresponding to slice rotation. + xy_rotated : arr + Shape (3, 3 * n_pix**2) nonzero-depth "plane" of rotated slice coords. n_pix : int Number of pixels. @@ -543,14 +579,10 @@ def insert_slice(slice_real, xyz, n_pix): otherwise 0. Shape (n_pix, n_pix, n_pix) """ - inserted_slice_tensor = torch.sparse_coo_tensor( - xyz + n_pix // 2, slice_real.reshape((n_pix**2,)), (n_pix, n_pix, n_pix) - ) - count_tensor = torch.sparse_coo_tensor( - xyz + n_pix // 2, np.ones((n_pix**2,)), (n_pix, n_pix, n_pix) - ) - inserted_slice_3d = inserted_slice_tensor.to_dense().numpy() - count_3d = count_tensor.to_dense().numpy() + xyz = IterativeRefinement.generate_xyz_voxels(n_pix) + slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) + inserted_slice_3d = griddata(xy_rotated, slice_values, xyz, fill_value=0).reshape((n_pix, n_pix, n_pix)) + count_3d = griddata(xy_rotated, np.ones_like(slice_values), xyz, fill_value=0).reshape((n_pix, n_pix, n_pix)) return inserted_slice_3d, count_3d @staticmethod From 6d13c43a07b424e40d39ff0270b3708a0fd8fae7 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:08:28 +0000 Subject: [PATCH 07/52] Format code with black --- .../expectation_maximization.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 8168588..0f6f984 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -380,7 +380,7 @@ def generate_xyz_voxels(n_pix): return xyz @staticmethod - def generate_slices(map_3d_f, xy_plane, n_pix, rots, z_offset = 0.05): + def generate_slices(map_3d_f, xy_plane, n_pix, rots, z_offset=0.05): """Generate slice coordinates by rotating xy plane. Interpolate values from map_3d_f onto 3D coordinates. @@ -453,15 +453,29 @@ def generate_slices(map_3d_f, xy_plane, n_pix, rots, z_offset = 0.05): slices = np.empty((n_rotations, map_3d_f.shape[0], map_3d_f.shape[1])) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, 3*n_pix**2)) - offset = np.array([[0,],[0,],[z_offset,]]) - xy_plane = np.concatenate((xy_plane + offset, xy_plane, xy_plane - offset), axis=1) + xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) + offset = np.array( + [ + [ + 0, + ], + [ + 0, + ], + [ + z_offset, + ], + ] + ) + xy_plane = np.concatenate( + (xy_plane + offset, xy_plane, xy_plane - offset), axis=1 + ) for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = map_coordinates(map_3d_f, xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2).reshape( - (n_pix, n_pix) - ) + slices[i] = map_coordinates( + map_3d_f, xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2 + ).reshape((n_pix, n_pix)) return slices, xyz_rotated @@ -555,10 +569,10 @@ def apply_wiener_filter(projection, ctf, small_number): def insert_slice(slice_real, xy_rotated, n_pix): """Rotate slice and interpolate onto a 3D grid. - Rotated xy-planes are expected to be of nonzero depth (i.e. a rotated - 2D plane with some small added z-depth to give "volume" to the slice in - order for interpolation to be feasible). The slice values are constant - along the depth axis of the slice. + Rotated xy-planes are expected to be of nonzero depth (i.e. a rotated + 2D plane with some small added z-depth to give "volume" to the slice in + order for interpolation to be feasible). The slice values are constant + along the depth axis of the slice. Parameters ---------- @@ -581,8 +595,12 @@ def insert_slice(slice_real, xy_rotated, n_pix): """ xyz = IterativeRefinement.generate_xyz_voxels(n_pix) slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) - inserted_slice_3d = griddata(xy_rotated, slice_values, xyz, fill_value=0).reshape((n_pix, n_pix, n_pix)) - count_3d = griddata(xy_rotated, np.ones_like(slice_values), xyz, fill_value=0).reshape((n_pix, n_pix, n_pix)) + inserted_slice_3d = griddata( + xy_rotated, slice_values, xyz, fill_value=0 + ).reshape((n_pix, n_pix, n_pix)) + count_3d = griddata( + xy_rotated, np.ones_like(slice_values), xyz, fill_value=0 + ).reshape((n_pix, n_pix, n_pix)) return inserted_slice_3d, count_3d @staticmethod From 83f2aba7fb7450a4a84a370c78a70a50dcc782d0 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 16:10:57 -0700 Subject: [PATCH 08/52] Removed torch import --- .../iterative_refinement/expectation_maximization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 8168588..2d79c1f 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -1,7 +1,6 @@ """Iterative refinement with Bayesian expectation maximization.""" import numpy as np -import torch from geomstats.geometry import special_orthogonal from scipy.ndimage import map_coordinates from scipy.interpolate import griddata @@ -565,7 +564,7 @@ def insert_slice(slice_real, xy_rotated, n_pix): slice_real : float64 arr Shape (n_pix, n_pix) the slice of interest. xy_rotated : arr - Shape (3, 3 * n_pix**2) nonzero-depth "plane" of rotated slice coords. + Shape (3, 3*n_pix**2) nonzero-depth "plane" of rotated slice coords. n_pix : int Number of pixels. From fa14c57827c5fd416f0c55c61afde5c0ac825045 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 16:17:56 -0700 Subject: [PATCH 09/52] Pre-commit and adding complex functionality to generate_slices --- .../expectation_maximization.py | 34 ++++++++++++------- tests/test_expectation_maximization.py | 4 +-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 9c50c71..90daa5e 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -8,8 +8,8 @@ primal_to_fourier_3D, ) from geomstats.geometry import special_orthogonal -from scipy.ndimage import map_coordinates from scipy.interpolate import griddata +from scipy.ndimage import map_coordinates from simSPI.transfer import eval_ctf @@ -286,7 +286,7 @@ def normalize_map(map_3d, counts, norm_const): Shape (n_pix, n_pix, n_pix) map normalized by counts. """ - return map_3d * counts / (norm_const + counts**2) + return map_3d * counts / (norm_const + counts ** 2) @staticmethod def apply_noise_model(map_3d_f_norm_1, map_3d_f_norm_2): @@ -403,7 +403,7 @@ def generate_xy_plane(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts) - xy_plane = np.zeros((3, n_pix**2)) + xy_plane = np.zeros((3, n_pix ** 2)) for d in range(2): xy_plane[d, :] = grid[d].flatten() @@ -430,7 +430,7 @@ def generate_xyz_voxels(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix**2)) + xyz = np.zeros((3, n_pix ** 2)) for d in range(3): xyz[d, :] = grid[d].flatten() @@ -511,10 +511,10 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): """ n_rotations = len(rots) n_pix = len(map_3d_f) - slices = np.empty((n_rotations, n_pix, n_pix)) + slices = np.empty((n_rotations, n_pix, n_pix), dtype=np.complex_) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) + xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix ** 2)) offset = np.array( [ [ @@ -534,9 +534,17 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = map_coordinates( - map_3d_f, xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2 - ).reshape((n_pix, n_pix)) + slices[i] = ( + map_coordinates( + map_3d_f.real, + xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, + ).reshape((n_pix, n_pix)) + + 1j + * map_coordinates( + map_3d_f.imag, + xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, + ).reshape((n_pix, n_pix)) + ) return slices, xyz_rotated @@ -596,7 +604,7 @@ def compute_bayesian_weights(particle, slices, sigma): ) slices_norm = np.linalg.norm(slices, axis=(1, 2)) ** 2 particle_norm = np.linalg.norm(particle) ** 2 - scale = -((2 * sigma**2) ** -1) + scale = -((2 * sigma ** 2) ** -1) log_bayesian_weights = scale * (slices_norm - 2 * corr_slices_particle) offset_safe = log_bayesian_weights.max() bayesian_weights = np.exp(log_bayesian_weights - offset_safe) @@ -655,7 +663,7 @@ def insert_slice(slice_real, xy_rotated, n_pix): Shape (n_pix, n_pix, n_pix) """ xyz = IterativeRefinement.generate_xyz_voxels(n_pix) - slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) + slice_values = np.repeat(slice_real.reshape((n_pix ** 2,)), 3, axis=0) inserted_slice_3d = griddata( xy_rotated, slice_values, xyz, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) @@ -723,8 +731,8 @@ def binary_mask_3d(center, radius, shape, fill=True, shell_thickness=1): a, b, c = center nx0, nx1, nx2 = shape x0, x1, x2 = np.ogrid[-a : nx0 - a, -b : nx1 - b, -c : nx2 - c] - r2 = x0**2 + x1**2 + x2**2 - mask = r2 <= radius**2 + r2 = x0 ** 2 + x1 ** 2 + x2 ** 2 + mask = r2 <= radius ** 2 if not fill and radius - shell_thickness > 0: mask_outer = mask mask_inner = r2 <= (radius - shell_thickness) ** 2 diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index cf66b63..2ef085e 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -78,7 +78,7 @@ def test_grid_SO3_uniform(test_ir, n_particles): def test_generate_xy_plane(test_ir, n_pix): """Test generation of xy plane.""" xy_plane = test_ir.generate_xy_plane(n_pix) - assert xy_plane.shape == (3, n_pix**2) + assert xy_plane.shape == (3, n_pix ** 2) n_pix_2 = 2 plane_2 = np.array([[-1, 0, -1, 0], [-1, -1, 0, 0], [0, 0, 0, 0]]) @@ -116,7 +116,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xy_plane = test_ir.generate_xy_plane(n_pix) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, n_pix**2) + assert xyz_rotated_planes.shape == (n_particles, 3, n_pix ** 2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) From e6d38b13b81123bbb95eec99893e71ba63bab111 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:18:12 +0000 Subject: [PATCH 10/52] Format code with black --- .../expectation_maximization.py | 34 +++++++++---------- tests/test_expectation_maximization.py | 4 +-- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 90daa5e..08ddf48 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -286,7 +286,7 @@ def normalize_map(map_3d, counts, norm_const): Shape (n_pix, n_pix, n_pix) map normalized by counts. """ - return map_3d * counts / (norm_const + counts ** 2) + return map_3d * counts / (norm_const + counts**2) @staticmethod def apply_noise_model(map_3d_f_norm_1, map_3d_f_norm_2): @@ -403,7 +403,7 @@ def generate_xy_plane(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts) - xy_plane = np.zeros((3, n_pix ** 2)) + xy_plane = np.zeros((3, n_pix**2)) for d in range(2): xy_plane[d, :] = grid[d].flatten() @@ -430,7 +430,7 @@ def generate_xyz_voxels(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix ** 2)) + xyz = np.zeros((3, n_pix**2)) for d in range(3): xyz[d, :] = grid[d].flatten() @@ -514,7 +514,7 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): slices = np.empty((n_rotations, n_pix, n_pix), dtype=np.complex_) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix ** 2)) + xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) offset = np.array( [ [ @@ -534,16 +534,14 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = ( - map_coordinates( - map_3d_f.real, - xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, - ).reshape((n_pix, n_pix)) - + 1j - * map_coordinates( - map_3d_f.imag, - xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, - ).reshape((n_pix, n_pix)) + slices[i] = map_coordinates( + map_3d_f.real, + xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + ).reshape((n_pix, n_pix)) + 1j * map_coordinates( + map_3d_f.imag, + xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + ).reshape( + (n_pix, n_pix) ) return slices, xyz_rotated @@ -604,7 +602,7 @@ def compute_bayesian_weights(particle, slices, sigma): ) slices_norm = np.linalg.norm(slices, axis=(1, 2)) ** 2 particle_norm = np.linalg.norm(particle) ** 2 - scale = -((2 * sigma ** 2) ** -1) + scale = -((2 * sigma**2) ** -1) log_bayesian_weights = scale * (slices_norm - 2 * corr_slices_particle) offset_safe = log_bayesian_weights.max() bayesian_weights = np.exp(log_bayesian_weights - offset_safe) @@ -663,7 +661,7 @@ def insert_slice(slice_real, xy_rotated, n_pix): Shape (n_pix, n_pix, n_pix) """ xyz = IterativeRefinement.generate_xyz_voxels(n_pix) - slice_values = np.repeat(slice_real.reshape((n_pix ** 2,)), 3, axis=0) + slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) inserted_slice_3d = griddata( xy_rotated, slice_values, xyz, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) @@ -731,8 +729,8 @@ def binary_mask_3d(center, radius, shape, fill=True, shell_thickness=1): a, b, c = center nx0, nx1, nx2 = shape x0, x1, x2 = np.ogrid[-a : nx0 - a, -b : nx1 - b, -c : nx2 - c] - r2 = x0 ** 2 + x1 ** 2 + x2 ** 2 - mask = r2 <= radius ** 2 + r2 = x0**2 + x1**2 + x2**2 + mask = r2 <= radius**2 if not fill and radius - shell_thickness > 0: mask_outer = mask mask_inner = r2 <= (radius - shell_thickness) ** 2 diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 2ef085e..cf66b63 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -78,7 +78,7 @@ def test_grid_SO3_uniform(test_ir, n_particles): def test_generate_xy_plane(test_ir, n_pix): """Test generation of xy plane.""" xy_plane = test_ir.generate_xy_plane(n_pix) - assert xy_plane.shape == (3, n_pix ** 2) + assert xy_plane.shape == (3, n_pix**2) n_pix_2 = 2 plane_2 = np.array([[-1, 0, -1, 0], [-1, -1, 0, 0], [0, 0, 0, 0]]) @@ -116,7 +116,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xy_plane = test_ir.generate_xy_plane(n_pix) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, n_pix ** 2) + assert xyz_rotated_planes.shape == (n_particles, 3, n_pix**2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) From 8b8b8ce8a73c81b549ddd473bb7d04bf62a10049 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 16:31:56 -0700 Subject: [PATCH 11/52] Fixed tests, moved xyz generation out of insert_slice method, pdated some parameters, fixed generate_xyz_voxels --- .../expectation_maximization.py | 26 ++++++++++--------- tests/test_expectation_maximization.py | 8 +++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 08ddf48..fe7020c 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -131,6 +131,8 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): .reshape(map_shape) ) + xyz_voxels = IterativeRefinement.generate_xyz_voxels(n_pix) + for _ in range(self.max_itr): half_map_3d_f_1 = ( @@ -213,10 +215,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_1)): xyz = xyz_rotated[one_slice_idx] inserted_slice_3d_r, count_3d_r = IterativeRefinement.insert_slice( - particle_f_deconv_1.real, xyz, n_pix + particle_f_deconv_1.real, xyz, xyz_voxels ) inserted_slice_3d_i, count_3d_i = IterativeRefinement.insert_slice( - particle_f_deconv_1.imag, xyz, n_pix + particle_f_deconv_1.imag, xyz, xyz_voxels ) map_3d_f_updated_1 += inserted_slice_3d_r + 1j * inserted_slice_3d_i counts_3d_updated_1 += count_3d_r + count_3d_i @@ -224,10 +226,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] inserted_slice_3d_r, count_3d_r = IterativeRefinement.insert_slice( - particle_f_deconv_2.real, xyz, n_pix + particle_f_deconv_2.real, xyz, xyz_voxels ) inserted_slice_3d_i, count_3d_i = IterativeRefinement.insert_slice( - particle_f_deconv_2.imag, xyz, n_pix + particle_f_deconv_2.imag, xyz, xyz_voxels ) map_3d_f_updated_2 += inserted_slice_3d_r + 1j * inserted_slice_3d_i counts_3d_updated_2 += count_3d_r + count_3d_i @@ -430,11 +432,11 @@ def generate_xyz_voxels(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix**2)) + xyz = np.zeros((3, n_pix**3)) - for d in range(3): - xyz[d, :] = grid[d].flatten() - xyz[:, [0, 1]] = xyz[:, [1, 0]] + for di in range(3): + xyz[di] = grid[di].flatten() + xyz[[0, 1]] = xyz[[1, 0]] return xyz @@ -633,7 +635,7 @@ def apply_wiener_filter(projection, ctf, small_number): return projection_wfilter_f @staticmethod - def insert_slice(slice_real, xy_rotated, n_pix): + def insert_slice(slice_real, xy_rotated, xyz): """Rotate slice and interpolate onto a 3D grid. Rotated xy-planes are expected to be of nonzero depth (i.e. a rotated @@ -647,8 +649,8 @@ def insert_slice(slice_real, xy_rotated, n_pix): Shape (n_pix, n_pix) the slice of interest. xy_rotated : arr Shape (3, 3*n_pix**2) nonzero-depth "plane" of rotated slice coords. - n_pix : int - Number of pixels. + xyz : arr + Shape (3, n_pix**3) voxels of 3D map. Returns ------- @@ -660,7 +662,7 @@ def insert_slice(slice_real, xy_rotated, n_pix): otherwise 0. Shape (n_pix, n_pix, n_pix) """ - xyz = IterativeRefinement.generate_xyz_voxels(n_pix) + n_pix = slice_real.shape[0] slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) inserted_slice_3d = griddata( xy_rotated, slice_values, xyz, fill_value=0 diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index cf66b63..0bd052f 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -116,7 +116,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xy_plane = test_ir.generate_xy_plane(n_pix) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, n_pix**2) + assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix**2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) @@ -254,10 +254,12 @@ def test_insert_slice(test_ir, n_pix): ) slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones, xy_plane, n_pix, rot_90deg_about_y + map_plane_ones, xy_plane, rot_90deg_about_y ) - inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], n_pix) + xyz_voxels = test_ir.generate_xyz_voxels(n_pix) + + inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], xyz_voxels) assert np.allclose(inserted, map_plane_ones) assert np.allclose(count, map_plane_ones) From f0f15dffaff03cddfece07f0e2f0c5cdc9022da3 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:33:15 +0000 Subject: [PATCH 12/52] Format code with black --- reconstructSPI/iterative_refinement/expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index fe7020c..eb0d9a6 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -650,7 +650,7 @@ def insert_slice(slice_real, xy_rotated, xyz): xy_rotated : arr Shape (3, 3*n_pix**2) nonzero-depth "plane" of rotated slice coords. xyz : arr - Shape (3, n_pix**3) voxels of 3D map. + Shape (3, n_pix**3) voxels of 3D map. Returns ------- From f4eb642504db64f50c8a83259ca8258e0da2e32c Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 16:33:52 -0700 Subject: [PATCH 13/52] Pre-commit --- .../expectation_maximization.py | 34 ++++++++++--------- tests/test_expectation_maximization.py | 4 +-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index eb0d9a6..8983ef6 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -288,7 +288,7 @@ def normalize_map(map_3d, counts, norm_const): Shape (n_pix, n_pix, n_pix) map normalized by counts. """ - return map_3d * counts / (norm_const + counts**2) + return map_3d * counts / (norm_const + counts ** 2) @staticmethod def apply_noise_model(map_3d_f_norm_1, map_3d_f_norm_2): @@ -405,7 +405,7 @@ def generate_xy_plane(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts) - xy_plane = np.zeros((3, n_pix**2)) + xy_plane = np.zeros((3, n_pix ** 2)) for d in range(2): xy_plane[d, :] = grid[d].flatten() @@ -432,7 +432,7 @@ def generate_xyz_voxels(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix**3)) + xyz = np.zeros((3, n_pix ** 3)) for di in range(3): xyz[di] = grid[di].flatten() @@ -516,7 +516,7 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): slices = np.empty((n_rotations, n_pix, n_pix), dtype=np.complex_) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) + xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix ** 2)) offset = np.array( [ [ @@ -536,14 +536,16 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = map_coordinates( - map_3d_f.real, - xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, - ).reshape((n_pix, n_pix)) + 1j * map_coordinates( - map_3d_f.imag, - xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, - ).reshape( - (n_pix, n_pix) + slices[i] = ( + map_coordinates( + map_3d_f.real, + xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, + ).reshape((n_pix, n_pix)) + + 1j + * map_coordinates( + map_3d_f.imag, + xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, + ).reshape((n_pix, n_pix)) ) return slices, xyz_rotated @@ -604,7 +606,7 @@ def compute_bayesian_weights(particle, slices, sigma): ) slices_norm = np.linalg.norm(slices, axis=(1, 2)) ** 2 particle_norm = np.linalg.norm(particle) ** 2 - scale = -((2 * sigma**2) ** -1) + scale = -((2 * sigma ** 2) ** -1) log_bayesian_weights = scale * (slices_norm - 2 * corr_slices_particle) offset_safe = log_bayesian_weights.max() bayesian_weights = np.exp(log_bayesian_weights - offset_safe) @@ -663,7 +665,7 @@ def insert_slice(slice_real, xy_rotated, xyz): Shape (n_pix, n_pix, n_pix) """ n_pix = slice_real.shape[0] - slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) + slice_values = np.repeat(slice_real.reshape((n_pix ** 2,)), 3, axis=0) inserted_slice_3d = griddata( xy_rotated, slice_values, xyz, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) @@ -731,8 +733,8 @@ def binary_mask_3d(center, radius, shape, fill=True, shell_thickness=1): a, b, c = center nx0, nx1, nx2 = shape x0, x1, x2 = np.ogrid[-a : nx0 - a, -b : nx1 - b, -c : nx2 - c] - r2 = x0**2 + x1**2 + x2**2 - mask = r2 <= radius**2 + r2 = x0 ** 2 + x1 ** 2 + x2 ** 2 + mask = r2 <= radius ** 2 if not fill and radius - shell_thickness > 0: mask_outer = mask mask_inner = r2 <= (radius - shell_thickness) ** 2 diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 0bd052f..3b332a2 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -78,7 +78,7 @@ def test_grid_SO3_uniform(test_ir, n_particles): def test_generate_xy_plane(test_ir, n_pix): """Test generation of xy plane.""" xy_plane = test_ir.generate_xy_plane(n_pix) - assert xy_plane.shape == (3, n_pix**2) + assert xy_plane.shape == (3, n_pix ** 2) n_pix_2 = 2 plane_2 = np.array([[-1, 0, -1, 0], [-1, -1, 0, 0], [0, 0, 0, 0]]) @@ -116,7 +116,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xy_plane = test_ir.generate_xy_plane(n_pix) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix**2) + assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix ** 2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) From 327d8de1088e6277789a5a9fc3e97b9aafc2b45a Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:34:11 +0000 Subject: [PATCH 14/52] Format code with black --- .../expectation_maximization.py | 34 +++++++++---------- tests/test_expectation_maximization.py | 4 +-- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 8983ef6..eb0d9a6 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -288,7 +288,7 @@ def normalize_map(map_3d, counts, norm_const): Shape (n_pix, n_pix, n_pix) map normalized by counts. """ - return map_3d * counts / (norm_const + counts ** 2) + return map_3d * counts / (norm_const + counts**2) @staticmethod def apply_noise_model(map_3d_f_norm_1, map_3d_f_norm_2): @@ -405,7 +405,7 @@ def generate_xy_plane(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts) - xy_plane = np.zeros((3, n_pix ** 2)) + xy_plane = np.zeros((3, n_pix**2)) for d in range(2): xy_plane[d, :] = grid[d].flatten() @@ -432,7 +432,7 @@ def generate_xyz_voxels(n_pix): axis_pts = np.arange(-n_pix // 2, n_pix // 2) grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix ** 3)) + xyz = np.zeros((3, n_pix**3)) for di in range(3): xyz[di] = grid[di].flatten() @@ -516,7 +516,7 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): slices = np.empty((n_rotations, n_pix, n_pix), dtype=np.complex_) overwrite_empty_with_zero = 0 slices[:, :, 0] = overwrite_empty_with_zero - xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix ** 2)) + xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) offset = np.array( [ [ @@ -536,16 +536,14 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): for i in range(n_rotations): xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = ( - map_coordinates( - map_3d_f.real, - xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, - ).reshape((n_pix, n_pix)) - + 1j - * map_coordinates( - map_3d_f.imag, - xyz_rotated[i, :, n_pix ** 2 : 2 * n_pix ** 2] + n_pix // 2, - ).reshape((n_pix, n_pix)) + slices[i] = map_coordinates( + map_3d_f.real, + xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + ).reshape((n_pix, n_pix)) + 1j * map_coordinates( + map_3d_f.imag, + xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + ).reshape( + (n_pix, n_pix) ) return slices, xyz_rotated @@ -606,7 +604,7 @@ def compute_bayesian_weights(particle, slices, sigma): ) slices_norm = np.linalg.norm(slices, axis=(1, 2)) ** 2 particle_norm = np.linalg.norm(particle) ** 2 - scale = -((2 * sigma ** 2) ** -1) + scale = -((2 * sigma**2) ** -1) log_bayesian_weights = scale * (slices_norm - 2 * corr_slices_particle) offset_safe = log_bayesian_weights.max() bayesian_weights = np.exp(log_bayesian_weights - offset_safe) @@ -665,7 +663,7 @@ def insert_slice(slice_real, xy_rotated, xyz): Shape (n_pix, n_pix, n_pix) """ n_pix = slice_real.shape[0] - slice_values = np.repeat(slice_real.reshape((n_pix ** 2,)), 3, axis=0) + slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) inserted_slice_3d = griddata( xy_rotated, slice_values, xyz, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) @@ -733,8 +731,8 @@ def binary_mask_3d(center, radius, shape, fill=True, shell_thickness=1): a, b, c = center nx0, nx1, nx2 = shape x0, x1, x2 = np.ogrid[-a : nx0 - a, -b : nx1 - b, -c : nx2 - c] - r2 = x0 ** 2 + x1 ** 2 + x2 ** 2 - mask = r2 <= radius ** 2 + r2 = x0**2 + x1**2 + x2**2 + mask = r2 <= radius**2 if not fill and radius - shell_thickness > 0: mask_outer = mask mask_inner = r2 <= (radius - shell_thickness) ** 2 diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 3b332a2..0bd052f 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -78,7 +78,7 @@ def test_grid_SO3_uniform(test_ir, n_particles): def test_generate_xy_plane(test_ir, n_pix): """Test generation of xy plane.""" xy_plane = test_ir.generate_xy_plane(n_pix) - assert xy_plane.shape == (3, n_pix ** 2) + assert xy_plane.shape == (3, n_pix**2) n_pix_2 = 2 plane_2 = np.array([[-1, 0, -1, 0], [-1, -1, 0, 0], [0, 0, 0, 0]]) @@ -116,7 +116,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xy_plane = test_ir.generate_xy_plane(n_pix) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix ** 2) + assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix**2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) From b681f6c3cfc8d6c8493d20b235c27354e25c1e42 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 19:10:23 -0700 Subject: [PATCH 15/52] Fixed shapes. --- .../iterative_refinement/expectation_maximization.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index eb0d9a6..aab2dce 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -664,12 +664,15 @@ def insert_slice(slice_real, xy_rotated, xyz): """ n_pix = slice_real.shape[0] slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) + inserted_slice_3d = griddata( - xy_rotated, slice_values, xyz, fill_value=0 + xy_rotated.T, slice_values, xyz.T, fill_value = 0 ).reshape((n_pix, n_pix, n_pix)) + count_3d = griddata( - xy_rotated, np.ones_like(slice_values), xyz, fill_value=0 + xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value = 0 ).reshape((n_pix, n_pix, n_pix)) + return inserted_slice_3d, count_3d @staticmethod From ae7e4f429a41b851e764ed4fe72d6b608204e14f Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 02:10:38 +0000 Subject: [PATCH 16/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index aab2dce..c625c36 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -664,13 +664,13 @@ def insert_slice(slice_real, xy_rotated, xyz): """ n_pix = slice_real.shape[0] slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) - + inserted_slice_3d = griddata( - xy_rotated.T, slice_values, xyz.T, fill_value = 0 + xy_rotated.T, slice_values, xyz.T, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) - + count_3d = griddata( - xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value = 0 + xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) return inserted_slice_3d, count_3d From 371f5e21955c28c6886245fc6cdfda71f2e4020a Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 19:27:00 -0700 Subject: [PATCH 17/52] Fixed np.repeat vs np.tile bug --- .../expectation_maximization.py | 4 ++-- tests/test_expectation_maximization.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index aab2dce..30e21b1 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -663,12 +663,12 @@ def insert_slice(slice_real, xy_rotated, xyz): Shape (n_pix, n_pix, n_pix) """ n_pix = slice_real.shape[0] - slice_values = np.repeat(slice_real.reshape((n_pix**2,)), 3, axis=0) + slice_values = np.tile(slice_real.reshape((n_pix**2,)), (3,)) inserted_slice_3d = griddata( xy_rotated.T, slice_values, xyz.T, fill_value = 0 ).reshape((n_pix, n_pix, n_pix)) - + count_3d = griddata( xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value = 0 ).reshape((n_pix, n_pix, n_pix)) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 0bd052f..f51687a 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -260,8 +260,17 @@ def test_insert_slice(test_ir, n_pix): xyz_voxels = test_ir.generate_xyz_voxels(n_pix) inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], xyz_voxels) - assert np.allclose(inserted, map_plane_ones) - assert np.allclose(count, map_plane_ones) + + omit_idx_artefact = 1 + + assert np.allclose( + inserted[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], + map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:] + ) + assert np.allclose( + count[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], + map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:] + ) def test_compute_fsc(test_ir, n_pix): From 315d5bcd0a742e08a0c430d989c5b7b0878f016d Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 02:27:50 +0000 Subject: [PATCH 18/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 2 +- tests/test_expectation_maximization.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index cb708d5..701dea9 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -664,7 +664,7 @@ def insert_slice(slice_real, xy_rotated, xyz): """ n_pix = slice_real.shape[0] slice_values = np.tile(slice_real.reshape((n_pix**2,)), (3,)) - + inserted_slice_3d = griddata( xy_rotated.T, slice_values, xyz.T, fill_value=0 ).reshape((n_pix, n_pix, n_pix)) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index f51687a..8df8b5e 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -260,16 +260,16 @@ def test_insert_slice(test_ir, n_pix): xyz_voxels = test_ir.generate_xyz_voxels(n_pix) inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], xyz_voxels) - + omit_idx_artefact = 1 assert np.allclose( inserted[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], - map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:] + map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], ) assert np.allclose( count[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], - map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:] + map_plane_ones[omit_idx_artefact:, omit_idx_artefact:, omit_idx_artefact:], ) From 498b5483550bf96a61bf4fa1beb1c9c2d4063336 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 20:03:33 -0700 Subject: [PATCH 19/52] Added vectorized insert_slice to iterative refinement method --- .../expectation_maximization.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index cb708d5..fda4965 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -133,6 +133,12 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): xyz_voxels = IterativeRefinement.generate_xyz_voxels(n_pix) + insert_slice_v = np.vectorize( + IterativeRefinement.insert_slice, + excluded=["xyz",], + signature="(n,n),(3,m),(3,k)->(n,n,n),(n,n,n)" + ) + for _ in range(self.max_itr): half_map_3d_f_1 = ( @@ -214,10 +220,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_1)): xyz = xyz_rotated[one_slice_idx] - inserted_slice_3d_r, count_3d_r = IterativeRefinement.insert_slice( + inserted_slice_3d_r, count_3d_r = insert_slice_v( particle_f_deconv_1.real, xyz, xyz_voxels ) - inserted_slice_3d_i, count_3d_i = IterativeRefinement.insert_slice( + inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_1.imag, xyz, xyz_voxels ) map_3d_f_updated_1 += inserted_slice_3d_r + 1j * inserted_slice_3d_i @@ -225,10 +231,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] - inserted_slice_3d_r, count_3d_r = IterativeRefinement.insert_slice( + inserted_slice_3d_r, count_3d_r = insert_slice_v( particle_f_deconv_2.real, xyz, xyz_voxels ) - inserted_slice_3d_i, count_3d_i = IterativeRefinement.insert_slice( + inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_2.imag, xyz, xyz_voxels ) map_3d_f_updated_2 += inserted_slice_3d_r + 1j * inserted_slice_3d_i From f8c05432b952ba22942cd307f88fa230cb9f490d Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:03:58 +0000 Subject: [PATCH 20/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index c1a23a1..ef05d50 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -135,8 +135,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): insert_slice_v = np.vectorize( IterativeRefinement.insert_slice, - excluded=["xyz",], - signature="(n,n),(3,m),(3,k)->(n,n,n),(n,n,n)" + excluded=[ + "xyz", + ], + signature="(n,n),(3,m),(3,k)->(n,n,n),(n,n,n)", ) for _ in range(self.max_itr): From 54f5b5aeccc3a01ee2b9d90ba10c2edb101d134b Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 20:25:10 -0700 Subject: [PATCH 21/52] Shape fix --- .../iterative_refinement/expectation_maximization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index c1a23a1..f3fa2cd 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -226,8 +226,8 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_1.imag, xyz, xyz_voxels ) - map_3d_f_updated_1 += inserted_slice_3d_r + 1j * inserted_slice_3d_i - counts_3d_updated_1 += count_3d_r + count_3d_i + map_3d_f_updated_1 += inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + counts_3d_updated_1 += count_3d_r[0] + count_3d_i[0] for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] @@ -237,8 +237,8 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_2.imag, xyz, xyz_voxels ) - map_3d_f_updated_2 += inserted_slice_3d_r + 1j * inserted_slice_3d_i - counts_3d_updated_2 += count_3d_r + count_3d_i + map_3d_f_updated_2 += inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + counts_3d_updated_2 += count_3d_r[0] + count_3d_i[0] map_3d_f_norm_1 = IterativeRefinement.normalize_map( map_3d_f_updated_1, counts_3d_updated_1, count_norm_const From 30f50d94bc30eb6951013c839eb1910f20267d8d Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:25:41 +0000 Subject: [PATCH 22/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ac8ac90..f54be4c 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -228,7 +228,9 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_1.imag, xyz, xyz_voxels ) - map_3d_f_updated_1 += inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + map_3d_f_updated_1 += ( + inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + ) counts_3d_updated_1 += count_3d_r[0] + count_3d_i[0] for one_slice_idx in range(len(bayes_factors_2)): @@ -239,7 +241,9 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_2.imag, xyz, xyz_voxels ) - map_3d_f_updated_2 += inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + map_3d_f_updated_2 += ( + inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + ) counts_3d_updated_2 += count_3d_r[0] + count_3d_i[0] map_3d_f_norm_1 = IterativeRefinement.normalize_map( From eab58f19f0eae401474d3bf6a98443233ccd5e5f Mon Sep 17 00:00:00 2001 From: thisTyler Date: Tue, 5 Apr 2022 20:36:36 -0700 Subject: [PATCH 23/52] Changed index 0s to sums in addition to make it more rigorous in case vectors of slices are inserted --- .../expectation_maximization.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index f54be4c..ae70cee 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -228,10 +228,12 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_1.imag, xyz, xyz_voxels ) - map_3d_f_updated_1 += ( - inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + map_3d_f_updated_1 += np.sum( + inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 + ) + counts_3d_updated_1 += np.sum( + count_3d_r + count_3d_i, axis=0 ) - counts_3d_updated_1 += count_3d_r[0] + count_3d_i[0] for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] @@ -241,10 +243,12 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): inserted_slice_3d_i, count_3d_i = insert_slice_v( particle_f_deconv_2.imag, xyz, xyz_voxels ) - map_3d_f_updated_2 += ( - inserted_slice_3d_r[0] + 1j * inserted_slice_3d_i[0] + map_3d_f_updated_2 += np.sum( + inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 + ) + counts_3d_updated_2 += np.sum( + count_3d_r + count_3d_i, axis=0 ) - counts_3d_updated_2 += count_3d_r[0] + count_3d_i[0] map_3d_f_norm_1 = IterativeRefinement.normalize_map( map_3d_f_updated_1, counts_3d_updated_1, count_norm_const From f04d99f9a92b2a0a77d07a1fe8e984ffb4c49a84 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:36:54 +0000 Subject: [PATCH 24/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ae70cee..ae5103c 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -231,9 +231,7 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): map_3d_f_updated_1 += np.sum( inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 ) - counts_3d_updated_1 += np.sum( - count_3d_r + count_3d_i, axis=0 - ) + counts_3d_updated_1 += np.sum(count_3d_r + count_3d_i, axis=0) for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] @@ -246,9 +244,7 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): map_3d_f_updated_2 += np.sum( inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 ) - counts_3d_updated_2 += np.sum( - count_3d_r + count_3d_i, axis=0 - ) + counts_3d_updated_2 += np.sum(count_3d_r + count_3d_i, axis=0) map_3d_f_norm_1 = IterativeRefinement.normalize_map( map_3d_f_updated_1, counts_3d_updated_1, count_norm_const From afd1502c0d4a368e1f9bcdc276af59e463c82bc2 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 11:11:06 -0700 Subject: [PATCH 25/52] Added demo notebook for the tolerance value stuff --- Insert Slice Tolerance Demo.ipynb | 439 ++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 Insert Slice Tolerance Demo.ipynb diff --git a/Insert Slice Tolerance Demo.ipynb b/Insert Slice Tolerance Demo.ipynb new file mode 100644 index 0000000..901dc24 --- /dev/null +++ b/Insert Slice Tolerance Demo.ipynb @@ -0,0 +1,439 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 44, + "id": "ca419b10", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "from geomstats.geometry import special_orthogonal\n", + "from scipy.spatial import QhullError\n", + "from scipy.interpolate import griddata\n", + "\n", + "\n", + "def plot_vectors(ax, vectors, color='k.'):\n", + " for vec in range(vectors.shape[1]):\n", + " ax.plot3D(vectors[0,vec], vectors[1,vec], vectors[2,vec], color)\n", + "\n", + "def plot_3d_matrix(ax, mat):\n", + " ii, jj, kk = mat.shape\n", + " for i in range(ii):\n", + " for j in range(jj):\n", + " for k in range(kk):\n", + " if mat[i,j,k].real == 1:\n", + " ax.plot3D(i, j, k, 'r.')\n", + " if mat[i,j,k].real == 0:\n", + " ax.plot3D(i, j, k, 'b.')" + ] + }, + { + "cell_type": "markdown", + "id": "4f6184f6", + "metadata": {}, + "source": [ + "We begin by defining an xy plane and a test slice. \n", + "\n", + "These are both simple in this $n=2$ case - what we have is `xy_plane` consisting of vectors to each of the four points in the x,y,z=0 grid of width and height $n=2$. \n", + "\n", + "Then, we also have `test_slice`, which assigns a value of $1$ to each of those points." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1fc7b02f", + "metadata": {}, + "outputs": [], + "source": [ + "xy_plane = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]])\n", + "test_slice = np.array([[1, 1], [1, 1]])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ee193a7a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAPwAAADyCAYAAABpoagXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABeZ0lEQVR4nO29d3hc1bU2/p5pGnWNepcly92SJdtyodiU0AzYDs3AdyEEEi65tIQkF/iSJ9c3BfhIu7kh5fklzoWb0O0QGzBOgAQIzQUX9d7rzGhG0sxI0/fvD3lvzoymnDNzZiRb530engdLZ+bs0Zy119prvetdHCEEMmTIWBxQzPcCZMiQET/IBi9DxiKCbPAyZCwiyAYvQ8YigmzwMmQsIsgGL0PGIoIqzO/lmp0MGbEHF68byR5ehoxFBNngZchYRJANXoaMRQTZ4GXIWESQDV6GjEUE2eBlyFhEkA1ehoxFBNngZchYRJANXoaMRQTZ4GXIWESQDV6GjEUE2eBlyFhEkA1ehoxFBNngZchYRJANXoaMRQTZ4OcBhBA4nU643W7IMuEy4olwAhgyJIbX64XT6YTdbmc/UyqVUKvVUKlUUCqV4Li46SHIWGTgwngY2f1IBEII3G433G43OI6Dy+ViPyeEwOv1MkN3OBxITU2FRqORN4DFgbh9wbKHjwNoCM83agqO48BxHBQKBbu2q6sLS5YsQVJSEgA5ApAhHWSDjzHcbjcGBwfh8XhQVFQEjuOYVw9kuHQDUCqVUCqVzPvPzMyw61UqFftP3gBkiIFs8DECP4T3er0slBeLQBGAx+OB2+1m16hUKhYBKBQKeQOQERSywccAXq8XLpeLhfDUqwtFqOvp+1H4bwAcx/lEAPIGIIMP2eAlBDU+mpCjXjmYAQcL68Ug0AbgdrvZGmh0oFKpoNFo5A1gkUM2eIlACIHL5YLH45ljhP4GH87ri40I/F/rvwEMDg4CAAoKCuQIYJFDNngJQGvr1GMHysTPF8GGvx6aBHS5XD4RAM0BKJVKeQM4zyEbfBTwr63TEN4fUp7howWtAFAE2gBoAlClUgXcwGScu5ANPkL419ZDGYW/AY+OjqK9vR1qtRoZGRnQ6XRIT0+fY4hSItj6Am0ATqcTDocDwGweQq1WswhA3gDObcgGHwFoYi5YCO8PavAejwetra1wOp3YsGEDCCGYnJyE0WhEV1cXlEoldDody/DPB0JtAPwEIP8IIOPcgUytFQEawtfX16OyshJarVbQ68bHxzE8PAyr1YqioiKUlJT4bBgUDocDExMT6OnpASEESUlJ0Ol00Ol0SElJidizDg4OQqFQoLCwMKLXU9Bnxev1orm5GStXrmShv7wBRAWZWrvQwK+t0wSdUIyPj8NgMGDjxo1IS0sLel1CQgLy8vIwMTGB/Px8aDQaTExMYGBgABaLxWcDSEpKintozU/+2e12Fgk4nU44nU4AkCOABQ7Z4MPAv7ZOw1ohBu92u9HS0gK73Y7c3NyQxs4HPQIkJiYiMTERBQUFIIRgZmYGZrMZPT09sNlsSElJYRtAYmJiVJ8zEvBpwMDnjUD+GwC/D0DeAOYXssGHQLDaOsdxYc/YFosFDQ0NKC0tRXJyMoaHh6NaC8dxSEpKQlJSEoqKikAIgc1mg9lsRkdHB+x2O1JTU9kGkJCQENX9hK7J/9/+HABCCBwOx5wkoLwBzA9kgw+CULX1UGUzQgiGhobQ39+PqqoqpKamYnJyUvKyHMdxSElJQUpKCkpKSuD1emGxWGA2m9Hc3AyXy4X09HTodDp4PJ55MaxwGwAhxCf8p2VAGbGDbPB+4IfwwWrrCoUioId3u91oamqCQqHApk2boFLN/nnjQbxRKBRIT09Heno6lixZAq/Xi8nJSZjNZoyNjbGIQKfTISMjg60tngi0AXi9XiYGMjw8jOLiYmg0GrkTMEaQDZ4HobX1QAY8NTWFxsZGLFmyZE42PND1saLWUigUChbeazQaAEBiYiLMZjN6e3vBcVxQDkC84P83Hh0dRWFhoawGFEPIBn8W4eixfPCTdoQQDAwMYGhoCOvWrUNycvKc6+eTWkuhUCiQlZWFrKwsAIDL5cLExMQcDoBOp0NaWtq8na39OQD+WgDyBhAdFr3BC6XH8kGTdi6XC42NjUhISMCmTZuCesmFRK2lUKvVyMnJQU5ODoDZ0prZbGYswISEBEk4ANEgkBaAvAFEh0Vt8F6vF3q9HoQQ6HQ6wQ8Lx3GwWCxobW3F0qVLkZ+fH/b6+fbw4T6bRqNBXl4e8vLyAAB2ux1mszkoB2A+IGQDkNWAQmNRGjw/MWexWEAIQWZmpuDXTk1NwW63Y8OGDYIe/vn28JG8l1arRUFBQVAOgMPhwPDwMHQ6HbRa7YKJAPgJVwqNRoOEhAS5ExCL0OD9Q3ilUslINeHgdDrR0NAAt9uNyspKwZ5uIXj4aBCIA3D06FF4PJ554wAEW6f/BtDf3w+1Wo3c3FyfVuDFqgWwqAw+kPRUsBKbP2h9e9myZbBaraIelPn28FKDbpQlJSWMA2C1WmEymeZwADIyMliVYD7WSWv9KpVKlgPDIjH4ULX1cDRZQgi6u7thNBqxfv16JCYmwmaziepmO98M3h8KhQJpaWlIS0ubwwGgir20BBhvDoDX6/WRGgsnB3a+bwDnvcGHkp4CQtNkHQ4HGhoakJqairq6urAadcFwrhlwtOBzAIDZduKJiYl54QDwDd4fgTaA810N6Lw2eCG19WAh/fj4OFpbW7F8+XJWugr3mmDwN3ibzYb6+npwHAedTofMzEykpqZGvKEsdCiVSkEcACrpLSUHQMz7iVEDOlc3gPPS4MXU1v2NlxCCzs5OTExMYMOGDQF73qPx8KOjo+ju7sbKlSuhVqsxMTGB4eFhWCwWaLVa6HQ6OJ3OeUt8xQOBOAAmkwlutxsnTpyARqNhEUJqampURhXNBhJKDGR4eBh5eXlISko6p+TAzjuDFyM9BfgavN1uR319PTIzM7Fx48aQ1FqPxyN4TfTYQFtl6fHA7Xaz2je/9GUymaDX62EymVgEcD5vABqNBjk5ORgaGsKGDRsYB2BwcBBWq5VthDqdDsnJyaKMSsqIgb8BmEwm5OXl+agB0QhgIWsBnFcGT8Mv+scX8mBQgzcYDGhvb8fKlStZ6BnqNWI8vN1uh81mQ2FhIVauXAmO43wmxwC+pS+PxwO1Wo2UlBSW+Xa73SzzrdPp5qX5JZbgG2YwDkBvby9sNhuSk5N9dABCfc9SHxEoPB4PGwcGfM51oFoADzzwAL773e9i5cqVkt87GpwXTw0N4VtbW5GRkYHc3FxRr5+YmIDD4cDGjRsFeVIxIT3dSBISErBkyRJBr6EPcGpqKlJTU1FWVgaPx8My3319fez8TxNf4R7qhR5qhpq1F0wHoLOzE3a7nQmBBIqEYmXwXq/XJ9znU32B2QhgPkRJwuGcN3h+bV1sMm1mZgZNTU3gOA4bNmwQbBRC7kNzAZOTk6irq8OJEycErysQlEolMjMzGSPQ5XLBbDZDr9ejo6MDGo0GmZmZceG+xyKhSAgR3MfgrwNgtVphNpvR0tICp9PpwwGIlcHTtQQDVSRaaDhnDT7QWCelUinY4KmhLF26FMPDw5ISaRwOB+rr66HT6UJuJKG8WjijouwxGs3Y7XaYTCb09/fDarUiOTmZbQBSG6gUI7L8EWiUthDwOQBlZWXwer2YmppiOQCLxYKenh5kZWVJygEI9zednp6WDV4qBKutC/G8Xq8XbW1tmJmZQV1dnc8oJqEIVbunjLwVK1YgOztb1PtGA61Wi8LCQhQWFrKw12Qyob29HRaLBcnJyaw+rlar47YuoZBqE1EoFMjIyEBGRgbKy8tx/PhxZGVlYXJyEn19fQAQFw6Ay+WaN4ZhKJxzBh+qtq5UKkNmz6enp1FfX4/8/HyWPItEAz5Q0o4Qgt7eXuj1+qDlPKGItg7PD3tLS0vR19cHt9sNq9WKgYEBEEKQkZGBzMzMiB76WHn4WIXe2dnZbPN1u92YmJjA+Ph4VDoACz0nEgznjMELqa0rFIqgjTCjo6Po6urC2rVrkZ6e7vMasQbvb5AulwsNDQ1ISkryYeQtFHAch+TkZNbGSx96SnxRqVQ+BKBwD3MsDD4W7xkIKpXKZwNwOp2YmJjA2NgY2tvbBXEAwm3G8foskeCcMHihtfVAxsuf9rJp06Y54WwkBs9/zeTkJJqamlBRURG2L14oQh0ZpID/Q+9wOHzOvElJSez8H6jsNZ9JO6mh0Wjm5ELCcQBoSS4UFqrRL3iDFzPWyT9pZ7Va0dDQwKa9BEuQiQU1yIGBAQwODgaVtooU8X5QEhISkJ+fj/z8fBBCMD097VP2oq2vmZmZ7Fy6UJJ2UkMIByA1NRVAcKN2u93zohEoBAvW4CORnlIoFOwMPzw8jN7eXqxdu1bwAAgxazOZTOA4LqS0VSgICZvnAzT8T05ORnFxsY/8dWNjIzweD1JTU+FyueB2uyXNei8Eg+cjGAdgbGwM09PTOH78uM8wEJq3oRuDEBw5cgTXXHNNGwAlgN8TQp7yW0MCgP8FsAHAOIA9hJBe3u9LATQD2EsI+Um4+y1Ig/d4PLBardBoNKL4yZSu2tDQAK/X6yMVLRWsVisaGxuh0Wiwdu1aSd+bYiE1z/jLX3s8HhgMBphMJpw+fZpl/v0bgMQilkk7qUCTocDsMWjVqlVsM6THRrVajQ8//FCQwXs8Htx///0AcA2AQQDHOY47RAhp5l12DwAzIaSS47hbAfw/AHt4v/8ZgLeEfoYF9RemXt1ut+PUqVOimxHsdjvGxsag0+lQXV0tubGPjIygvr4ey5cvX5AsqniAZrVTUlKwceNGrF27FklJSRgeHsaJEydQX1+PgYEB2Gw2UZtWrBKBsQA9w3Mcx+r/NTU12LhxI3Q6HTo6OnDmzBnU1dXhmWeeCfo+x44dQ2VlJQgh3YQQJ4CXAOzyu2wXgOfO/v9+AJdzZ/9QHMftBtADoEno2heMh+fX1mnZS+gDQGvpfX19SE9PR3FxsaRr83q9Pom/WI9zjoWHl9KY+Gvji18G0r7jn/9D0ZbPpVJfsKSdQqHAkiVLcNdddwEAfvWrX2F0dDTo+wwNDaGkpIT/o0EAm/0uKwIwAACEEDfHcZMAsjiOswN4FMAVAL4ldO0LwuDFaML7g057USqVWLduHTo7OyVdG792v2rVKtb4Eq1BejyeuI2AihfTLtCZly99FaoBKBbGGUuDD/W+lOmYlJSEiooKye9/FnsB/JwQYhVjL/Nq8KGkp4TAf9qL3W6X1PNS+u2aNWuQkZHBfh5JKY8Pi8WC+vp6ALMlMsqRpxz4hXSGDwSh3pjjOMENQJQ1KSXi7eEphNJqi4qKMDAwwP9RMYAhv8uGAJQAGOQ4TgUgHbPJu80AbuI47mkAGQC8HMfZCSHBzxCYR4MX27fu/9r+/n4MDw/7lMTCMe3CvSddg9frRWdnJ6amplBXVzeHIhmpQRJCMDIywqoHCQkJcLlcPhz4lJQUpqiyUBHpZhSqAchoNLLPLFUDULw65fwhNEtfV1eHjo4OcBxXjlnDvhXA7X6XHQLwJQCfALgJwN/J7BdwMb2A47i9AKzhjB2YJ4OPJoQPNe0lUs9LDZjjONb4kpmZGbTxJRKDJ4SgpaUFDocDmzZtYrTehIQEn7ovpb+Oj4/DbDYzCmxGRsaC2gSk8Mb8BqCUlBS4XC6oVKqADUCRJElj6eFDJYSFdsqpVCo888wzuPbaa/+K2bLcHwghTRzHfR/ACULIIQD7APyR47hOACbMbgoRI64GL7a27h86TkxMoKmpKei0l0gNnr5uYmICLS0tYRtfxN7H4XBgenraJw8QKBKhIXBOTg4SExNRWlqKiYkJmEwmdHd3Bwz/5wOxyqhrNJqgDUAOh8Pn/C+kASiWBh8qAWmz2QQPNtmxYwcIIcv5PyOEfI/3/3YAN4d6D0LIXkE3QxwNPpAmfCjQ8Jzqiff29mJsbAy1tbVBB0BE+hByHIeenh6YzWZBjS9iPDztntNqtViyZEnANR49ehQffvghLrroImzevJm9v7/4o8PhmBP+0w0gnhJYscqo872mfwMQbXs1mUyCG4DChd6RIlzSzmaz+WffFwxibvD+fetCQ3hKk6XTXpKTk7Fp0ybJd2yXy8XCx40bNwoWYQgHmmcYGRnB+vXrUV9fH3CTOHr0KHbu3Amn0wmNRoNDhw5h6dKlAd8zUPjPz4Av1PBfCMJtIvy2V0BYA1CsqiBCzvALsRceiLHB+4fwYryCUqmEyWRCV1cXli1bJlq2SggmJyfR2NiIxMRElJeXS/ZweDweNDU1QaFQoK6ujpE0+AZP/xYffvghnE4nPB4PnE4nPvzwQ0rGCHmPQBnwYOG/1B55IdTMhTQA0WSr1OsNl6VflAZPvfOxY8dYkkooaANHd3c3m/YiJQj5fKZ7bW0t2tvbJSvnTU9P48yZMygpKfEhAAU7Blx00UXQaDTMw1900UURPZyhwn+z2YzExER4PB5Jwv+F2B4bqAGov78fk5OTOH78eMAGoEghVVluPiC5wfvX1sXWV+m0FwBYvXq15MbOJ+rQLL9YFdpgoIKV/j33QHCD37x5Mw4dOuRzhjeZTFGvhx/+9/T0sKGZLS0tcLlcUYX/C9Hg+aANQLSltbi4eE4DEP/8L5aCLZR4sxAhqcEHk54SCv60F6PRGLHXDfbwWK1W1NfXo6ysDEVFRezn0RJpCCHo6uqC2WwOWLcHQif6Nm/ejM2bNwu6NhJwHAetVovc3FyUlpYyAgw//NfpdMjKyhKU/Y8FKSiWTLtADUB09FVPT4/oBiAhZ3jaQrvQIJnBE0LgcDgiqq2TANNeTCZT1DV1Pmi7bFVV1ZwvIxqDd7lcqK+vR2pqatjhFQuFPedPgKHn34GBAaZ/R38frGKxkD08RbBNxP/443Q6YTabMTIygra2NiZ6kZmZiaSkpDnrEhLSn/cGT4080JcW6ssMNu0lUtYcze7TL5pOfHG5XEHbZSM1eIvFgoaGBixduhR5eXkhrxVj8PHeHPzPv7T+3draykY/UwKMUqlcEEk7oe8pJFznNwABs/LlJpMpaANQuLWK6YePNyQN6QMZDv1ZoB0x1LSXSA2eimCoVCrW+FJQUIDS0lJR0ljhQHXsqqurBSVoFpKHDwX/+jc//O/t7YVCoWBeX0rDX0ibSGJiIoqKioI2ANntdhiNxqATgMIx8eYTMV8VNVy+wXu9XnR0dMBisQSd9hKNwXu93qCNL8FeI9QYaausy+XChRdeKPiLXcgePhT8w3+n04m+vj6YTCYcO3ZMUPgvBAu1Wy5Q+fPYsWOYmppCf38/APhMABKzaR05cgQPP/ww2tvbOyFC7YbjuCsAPAVAA8AJ4NuEkL8LuWfMDV6lUsHtdrNE1szMDOrr65GTkxNySAPNKouFQqFAV1cXHA5H0ASaPzhOmGik3W7HmTNnkJubi8TERFG7+EIy4mhAVV2VSiXKy8uDhv9ihz4sJA8fCkqlEiqVipGj6OhrvV6P9vZ2PPnkk/B4PKivr0dVVVXQ+1O1m7fffhtLly5dDXFqN0YA1xNChjmOWwvgr5jtmw8LSQ0+0BfG99RjY2Po7OzE6tWrodPpQr6XUqmEw+EQdX+qOJqXl4f169eLksYKZ/CUIkuPH6Ojo6Ie0kAGfy4k+AKBn5gNF/5T7x9O/vpcMXjAt0rhP/r6xz/+MW677TY8/fTTsFqtOHjwYMD3oGo3FRUVIIQ4OY6jajd8g9+F2b53YFbt5hmO4zhCyCneNU0AEjmOSyCEhDWYuIT0tP5Lp70I8bp8QUohoCW9tLQ0FBYWinp4Qhk8IQR9fX0YGxvz4dnTqEBoDdvfiE0mEwwGA6sFx1oIIx5Mu0Dhv8lkYuy3UOH/Qg3pA71nqL9laWkp0tLS8Pzzz4d8n2jUbjDr4SluBHBSiLEDceLSNzc3o7i4mE17EQKhZ3hCCLq7uzE+Po4NGzagp6dH9Nk/mMFTko5KpZozYEKsF6bXU4796OgoCgsLWa5Bq9UiMzMTWVlZC97DC4VGoxGU/c/IyIiZh5e6p0CqXngpwHHcGsyG+VcKfU1MQ/qRkREYDAYsWbJE8KhkCiEGTxtrqKCi2IGSFIEm1thsNtTX18+hyPJfI+Y+lHXY2NgIjpudVuvxeFgpaHp6mrWCzszMAADLBEf70MZL4ioUwoX/09PTGBwcRHZ2tqDpN0IQi+YZqXj0UardgOO4YgCvAbiTENIldP0x8fB02ovL5UJJSUlE3O1whksbX/wba6KdJAN8Lm0ViCJLIdYLezwetLS0oLS0FCUlJayxiD7YVAuuuLgYU1NT6OrqwsTEBHp6eqBSqZCVlYXMzEw2/WQ+IYU39g//jx8/Dq1WKyj8F4pYhPRCDF6M2k1PTw8qKio0EKF2w3FcBoA3ATxGCPlIzPolN3j/aS8DAwNR1dP9wW87DdQbH4mH54fb/JnuoXINYkp5VMZp6dKlKC0tZZ8jFC8gISEBlZWVAD5vhKHTT9LS0pgxzMck2FiE3xzHIT8/f474RVtbGxwOhw/3X2j2P1YGLwXphqrdXHXVVQDQAnFqNw8AqATwPY7jqFjGlYQQfdj7hl2ZCJhMJjQ2NvpMe1GpVHA6naLfK1BI73a70djYCLVazdpO/SE22Udf43K5cPLkSaSmpoYsF1IILeXRrrz8/HyfUC/U+/tHD/598FQIgo65pjz4tLS0uHj/WOQX+JtIIPEL2vorJvsfq6SdVK2xO3bswI4dOwCACSAIUbshhPwQwA+Fr/pzSGrw6enpc+irSqUSbrdb9Hv5GzylsVKF2mCIJKS32+0YHh7GmjVrwlJkKcKF9JTS6/F4UFdXh66uLlHEm1C/o40g5eXlTARzeHgYra2tLBTOysqKqQpOLDaWUBGPkOy/v/bdfIT0C7lTDpDY4KkcFR/RcOLp64aGhtDX1yeIxhpqZHQg0PfOzs4WbOz0PsE2FofDwQg6ZWVlrGYtxjMKvVatVvsMgqChMKWB6nQ6OJ3OoLJgkSAWIb0Y+Gf/+QlPfvgfi3Wey+IXQJyYdpEYPA2Zaf+y0DlxSqUSdrs97HX8aTKrV68OOSEk2PoCGSVNJvr3B8SDWhsoE04nn05MTGB0dJR5/0BjoIWCkPkZ7RwItPc9OTkZJSUl8Hq9LPs/PT2NkydPsiOPFNl/2eB5CEbGiCSkn5mZwfT0NEpKSoKOeg4EISE9pcjm5eWxgYCRZPb9jXJoaAj9/f0Bk4nzUVtXKpXIzs6GxWJhG8H4+DgbA83vgluozR5iQXvbdTodTCYT1q5dC7PZjKGhIUxNTSEpKYkdDyIRVxGStIuFHJtUiFvzjBhQCq5Wq2VZbaEIZ/AmkwktLS1YtWoVOxNGonjDT9p5vV6WTa6rqwtoPAuheSYxMRHFxcVsDDT1hH19feycLEQEY75DejHwn33nH/6L3fTCtdwu5NZYYIGF9F6vF+3t7bDZbNi0aROOHz8u+n7BNphgFFlAeMadD2qUTqcTZ86cQVZWVkgm4Xyz5/zXxfeEwFwJ7NTUVOYJ/cuT55LB8xEq/KebHu17D1bxEKJJv+hDeiEGT4UwsrOzsWLFiogfqEAenpbzNBrNHIpssNcIuY/VakVLSwuWL1/OmieCYSF4+FDwL/1ZLBaMj4+jsbERXq+XGX9aWtp5QfsF5m56VPlmeHgYbW1tSExMnBP+n8sClkAcPLyQujhtfOGH2RRivYm/8VKKbGlpqY+OXajXCIHNZmPRgpAQbj49fCTHlbS0NKSlpbHSn9lsxujoKNra2gDMlmAzMjKi6oGPJSJRMBIS/tPcRzAsupDe/8EOdxbs7u6GyWQKKITBnz4jFHymHaXIVlVVMSJQIIgxeEIIO3YsW7ZM8Je70D18KPBnwBFC0NHRAbfbzejT0Q7AiBWRJ5pKQrDwv729HV1dXejv7w8ofCk0pKfiFx6PB11dXY8JFb84+7vHMdsr7wHwECHkr0I/17ylZp1OJ+rr65GWloYNGzYEFRsUa/AKhQJutxsdHR2CKLL0NUIM3uVy4cyZM8jIyEBBQYGoyGOhGXGk4DgOarUaOp0OOTk5cwZgqNVqxvsPJAAZCOdCLzwN/1NSUrBkyRKo1WoW/lssFiQmJqKrq0uQgCVf/KK4uBgJCQm3CRW/4DhuNWYptmsAFAJ4h+O45YQQQYmyeTF4OhQy3ESZSDL8tAsrPT1dEEUWEGbw/oKVPT09ottj+ffwer0wmUwBddEX+ubAN1B/BVgqANnd3Y2ZmRmkpaUhKysrZBZ8IbfG+s/9o2f4QOH/P//5T7S1teGaa67BxRdfjJ/85CcBS3988YuzECx+cfbnL53tf+85y7PfhNkGm7CIeUhPQX/W19eH0dHRkEMhKcQ2wkxNTaG+vh4ajQbLli0TteZQGBsbQ1dXlw/TT2xmn/93cTgcOH36NLRaLRsSQUtiC/n8JwR8AUg6AHJ8fNyn9OfPgY8VIy5aDx9o7p9Go5mzkdDw/9/+7d/w8ssv48MPP8TJkyeD5jeiFL8oAvCp32sFyVsBcfLwCoUCTqcTLS0t0Gg0godCimmEoaSXmpoa1NfXR7tkAJ/r5U9NTaGurs6nMy1SAYypqSk0NDRgxYoV7KF3OBwYHx9nfeEpKSlwOBxwu90LkhAj1ED9B0D6c+BTUlJY3X8hqt0Emvu3ffv2kO/r9XqRmJiIiy66KKp7xwpxe5pOnDiBiooKFBQUCH6NkJCeNqm43e6gpJdI4Ha7UV9fj5SUlID6eJEIYFgsFoyMjKCmpgZJSUmsizAhIYHNRfd6vTCbzTCbzTh9+rRPyCz0TBxrROqR/TnwVqsV4+PjGBwcxPT0NLq6uiST/JLC4APN/Qv1vkIdQJTiF/TnoV4bFDEJ6fkYGhqC1WpFTU0Nm/QpFOEMnk+RpU0qUsBms+HMmTMoLy8PukGJCekJIRgbG8PU1BS2bt0KtVod9OGgXlGr1WLDhg3M+9Mz8UKgw0qRX+B48s95eXlob29HWlqaj+QXTf5FQoGVwuADzf07fvx42G7GcM8hX/zibKlYjPjFIQAvcBz3M8wm7ZYBOCb0M8XsiaEKL16vF9nZ2ZKr3oSq3UcDvV6Pzs7OgCOp+KDVgHCgpB+v14vCwkJBghX8B8bf+09OTrLwnyrhzIf3l/JehBAolUqm/koIwczMDMbHx3064LKysgSX/qTK0vvP/QsFKqISDnzxi7MO7RWh4hdnr3sFswk+N4D7hWbogRgZPCW7UN427QsXi0BneEIIent7odfr51BkowE5OxCScgLClfKEePiZmRmcPn0apaWlUKvVmJycFLUef/gzw+x2u09GPD09nWXEAyWWpILUSTZ/JViO45jkV0lJCau80EhHiORXPCSq/eF0OgU7Np74BQD86Ox7hxW/OPu7H9HXiIXkBm8wGNDa2oo1a9YwRpIUPfFAeIpspHC73ZiZmYHL5QrKCfBHuKQd1bGnGvwGg0HweoQmBLVabUDv39PTA7VazTL/0UzGDQSpDT4cScZf/45udHzJL7rR0QgqVgYfCgudZQfEwOCTk5PnZLSjUb2hYhZ01HM4xRuxmJ6expkzZ6BSqURx+EMl7WjFYP369XMUWGKFQN5/fHwcXV1dmJychM1mAyFEEhVcYH4jBv+NjvL+aSKM3wUpJcJtIgtd7QaIkcH7G3c0Ht5ut7N22XAUWQoabof7wo1GI9ra2rB27Vo0NzdHPUmG0m6np6fnVAzEPHxSGJNWq2X18M7OTqjVaqaCS9lwkQphSE0KisYb82e/A2CSX4ODg5iZmYHFYmHhf7SSX+e6+AUQhyw9EN1gSIPBgImJCWzatEmwQiv1vqHKJ729vTAYDIzDH+41ge7Bf/DdbjfOnDmDtLQ01NTUBD1X8kE3RvoQxSoEpUo4lA1HvT8VwqBceKHePxYhvVTvRyW/XC4XawLyl/yivH+xf++FNIQiUsSlrqNSqUTPiXM6nejs7ATHcaLmxAGhw206DEKtVrPhFeFeEwj8pN309DROnz6NiooK5OfnB10TBdWkV6lU8Hq98Hg87KFXKBQxz7bzvT9VhOWf/fmZ/0CIRdIuFuG3RqPxmfzqdrsxMTEBg8HABFZobkCI5p8QtZtF5+EDQayHp7pwhYWFmJ6eFv1wBbvfzMwMzpw5wzTz+YjE4AkhrDwYamgFBTVumpWmIb/X6wUhBF6vF16vF3a7HV6vF263GwqFIqbJJ39FWMqF53t//3KY1CF9vMQmVSoVsrOzGR+Etr92dHSw9lf6WYPNfT+XFWuBBRjSDw4OYmBgALW1tfB4POjp6RG9hkDGS6Wtgk2uFWvwCoUCU1NTIWfc+4MatD85gxq0UqmE1WpFc3MzG0fs8XjY3456/1huAP5ceOr9u7u7odFokJWV5TMxRwrMV7ccf9oPX/ueTvvh9zdwHCef4QXfRIDMldfrRXNzM1OoVSqVmJ6ejiizzTdeOqlmdHQ0ZN1eDHPO6/Wip6cHDocDF110UdhzLyEEWq0WVqsVJ0+eZF7Gv4Y8Pj7ORlzRB4fv/enf0OPxMMOPt/cfHx9nnYPUICLtg6dYCJNj/T+r/7Sf1NTUsNyMha52A8QxpA9VlpuZmUF9fT3y8/NRWlrKjCCSKTL0fjR0bmpqAoCwdXuhQpZUw442vggxdtrTX1dXB6fTycpl09PTyMjIQHZ2Nqanp6HX67F+/XqfB4vv/dVqtc+Z3+Px+CT+4uH9i4uLYTabUVFR4UP7pd6fZv7FYCH2wwea9jMwMIDJyUlYLBYf6Wt6H6vVKqpkbDKZsGfPHrzzzjsdAHoB3EIIMftfx3HclwB89+w/f0gIeY7juCQAr2J2ao0HwOuEkMfC3XPez/D0DBwo1I4mu2+329HW1oaCggJBMtdCQnrKBaisrERycjI6OjpCXk+Tc/T9gcCNMh0dHYwpNzY2huzs7KBGw/fq1PvTzQ0I7v2lNqhg3p9SYfnZ8HCb4kLw8KHAcbPTfmh/f0FBARO/mJqaQnJyMmw2G8bHx0W1ZT/11FO4/PLL8fbbby/jOO4xAI8BeNTv3pkA/gPARgAEwGdn+fQOAD8hhPyD4zgNgHc5jruGEPJWqHvG5QwfKKTnl8aChdqRGrzT6URbWxuqqqoE8+zDGbzBYGByWampqZieng4aEfATcKGaKbxeL/r7+5GTk4OKigrMzMzAaDSipaUFLpcLmZmZyM7ODto9xvf+9P2o8fPP/nQtUiGQR+ZLYFMVHBrJJCQkhPT+C9HDBwI9w/tLftlsNrz++ut466238Prrr+PYsWP4+te/HlRDkeLgwYN477336D+fA/Ae/AwewFUA3iaEmACA47i3AVxNCHkRwD8AgBDi5DjuJGY750IiJh7en5TiH9K73W40NDRAq9X6lMb8EYle/MDAAEwmE5YtWyaqqSaYwdONyWg0+nDsQ10vxNipSm9JSQnryEtKSkJpaSmbGjM+Po6RkRE2M46e/YOdJQN5f4fDgcnJSWRnZ8PlckmS+AtnoP4qOHwxSKfT6ZP5p9/xQvbwFB6PZw4XhHIcbrvtNpw4cQI33XQT3G532PM+MCuswuvGHAUQaNYZE8I4izmCF9zs+OjrAfwi3D3jJoBBjSNWFFk6OorOpBebRApkwHTUlVKpnMOxD8a0E2Lsk5OTaG5uxqpVq5g4hD+USqWPF7FarTAajThz5gwAICsrC9nZ2UHHJykUCszMzKChoQHl5eXIzMxk3p+fAIyE9CPWI/Oz4dT7G41GVgunIbOUiJXBh2rWstlsyM7Oxvr169nPvvCFLwQcY/ajH/n2vpxtfRVd7zzbK/8igP8mhHSHuz4uBk8fjtHRUXR3d4dtPRULOrwxJycHq1atQl9fX8SDJfjvefr0aRQUFAScfuMffdCzNDWGYAYxNjaG3t5e1NTUCE5u8XvHy8vLWeKvr68PVqsV6enpyM7ORmZmJqsfT01NoampCatXr2bGxPf+/oZPSSVCvH80dfhA3r+jowOjo6MYGxub4/0jRazyAuHKcv7P9TvvvBP0+ry8PIyMjFBB1AIAgea7DwG4hPfvYsyG/hT/H4AOQsh/hVk+gDiF9JRMMjQ0NKexJlpQks6KFSsYoSKS7D7fw1MZKv+BkHzwy3iBknP+oEcDs9mM9evXR/U30Gg0LINMdeMMBgNjymm1WkxMTDBlnUCf1T/0998EwpX9pDpzJyUlIS0tDSkpKcjMzITZbPbx/nRzENsGHcszfDBYrVZRZbmdO3fiueeew2OPPQbMil0cDHDZXwE8wXEczWhfCeBxAOA47oeYVcL5itB7xtzD0zJWKIrsp59+ig8++ADbtm3Dli1bBL93sOGN/C47oaAGT6OQmpqakKwp6uH5zLlQybmWlhYoFArU1NRILp/M143r6enB8PAwEhMTUV9fD51Oh+zsbOh0uqgSf3zvHysuPR1+mZ2d7SOCQY9qtBQmRAJrPgxeiEQ1H4899hhuueUWPP744x0A+gDcAgAcx20EcB8h5CuEEBPHcT8AQOeuff/sz4oBfAdAK4CTZ7+PZwghvw91z5gaPPW+y5cvD1rC+vTTT3HNNdcw3bC33nrLx+gDdb55vbPDG+12e0AdO7GsOXqf0dFREEIERyFut9snFA4Ep9OJhoYG5OTkiJqCKxZUwMNms2HLli2swmE2m2EwGNDe3o6kpCRmUMGYgULKfvTnUq7d/+8SSATDbDbPkcAK5f1jQdcNtYnMzMyI4iBkZWXh3XffBWZlqhgIISfA89qEkD8A+IPfNYMARH/AmBn8wMAABgcHmfft7u4OeAb64IMPfJRBP/jgAx+D9+9ioxFDZmZm0OGNYuWt3W43hoeHWUNNuAeFShmlp6fj6NGjSE9PR05Ojs8ZGpg901Et+3Cz56IBZSmq1WpUV1ez9ft7TJvNBqPRiMbGRng8HmRmZiInJyfo4MRA3n9kZIT9DVwuF5OniibzL8Qb+38WmvmPxPtHinBn+FhUG6RGTAy+s7MTVquVUWSBz2vq/n+wbdu2+SiDbtu2zef3/Okz9GwdbnijmDM8laGi50ghxk6Tc6tWrQIwG8kYjUZ2hs7OzoZarUZvby/Wrl0raYLSHy6XC/X19cjJyQk5WpuWj+jkFH7f+NTUFFJTU5GdnY2srKyg0c3g4CDjTfgfaQD4NPvEMuvPcb5joPy9f2JiIpxOJxwOR9Q98HyECuljwSWIBWJi8GVlZXPCXFqL969PbtmyBW+99VbQMzw1+JGREfT09IQ9WwPCQ3q+DJXdbofdbg95fbBMPD1DV1ZWYmZmBp2dnTAajdBqtRgdHYXH40F6errkDwRV7S0vLw85wScQaN84nZwyNTUFo9GI/v5+KBQKH74/AEYFrq2t9TFmMWf/YIj2vB3I+58+fdqnB14K7x/uDH8uGH1MDF6j0czxsKFYc1u2bAmarFMoFOjq6oLL5cKmTZsESTMLMXh/GarR0dGQr+En50Jl4gcHB+H1erFt2zYQQmAymTA0NISWlhakpqay0D/aSoXFYkFTUxNWrlwZtJYvFLQOnp6ejqVLl8LhcMBoNDIj93q9SEpKQlVVVUiSVLizfzDjl9JQqPfXaDSora2F2+2e4/3p2V+s9w+1TqlbhmOFmJXl5txIQMecP+iY4uzsbNTW1gp+KEJtLsFkqKJlzlFhjeTkZJ9zNJ88Q71oX18f80o5OTmCxBf4oF111dXVol8rBAkJCSgqKkJ+fj4b3aVSqXDixAkkJCQwbxqK7w/45lKCkX5ilfWnUKlUPvLX09PTGB8fZ96fdvylpaUJ8v6hmJOR6OfHG3GbZCBWyJIy8lJTU1FYWCiJ4k0oGapArxFLky0uLg7KHvT3ona7nWnqORwOlkALF3YODQ1heHh4Tled1KCTcgsKCnw44dPT0xHx/f1JPzTspx1/9G8tRdIr2AbCP/uXlpYy7z86OsqqGHQDEOv9zwV5KyDOBi/Uw9PhjVVVVRgZGYmKREMRTobK/zVCmXMWiwWNjY1YuXJlQGGNYNBqtT7NJiaTifHmU1JSmBeloT8hBN3d3bBarVi/fr0kyrPBECo3IBXfn8p7DQ8Ps9wO3QCi7fUXunH4e3/a8UZ1GejZX4j3PxfUboA4hvRCDJ6cHd5I57qr1Wro9XrRBu9/LyEyVP6iGUKM3WAwsKmy0XzZ/lNXLBYLjEYjTp06BYVCgaysLExNTSEhIcHnuBAL0CEiQjawaPn+g4ODGB8fZxuYf68//X+xZb9IIgV+FYPq3/l7f5fLFTTzL3t4/xupVCFDejq8MTk52Weuu9iaOuBrvP39/RgZGQkrQ0VfIzQ519/fD6PRiA0bNkhKFebOKq2mpaWhoqKCzbkDZkuI7e3tyMnJiZprHgi0qaeqqkq0cosYvr9SqURPTw+sVivWrVvnE/ZH0uvvDymOBv7ef2pqCo2NjWhqaoLX62UMRsphECtvRcUvent70dnZ+TZEiF/4/f4QgApCyFpBn0vwCqOEUqkMqlwbanhjpLx4j8fDEjMbN24MGwJTRp8QmmxraysIIXNKVFLDbrejqakJS5cuRV5enk+9ua2tTVAILRSUvy6mqScU/Pn+fK4C5VysXbs2ZNYfmFv2E+L9pabV8ll/NPNvMpkwPDyM1tZWTE1N4f333xd1zKLiF4899hg4jnsXIsQv6MbAcdwNAKxiPsu8h/SU9hlsyEQkvHi32w2bzYaCggIsWbJEEJlGrVazRGFubm5AI3K5XEzLTcpptYFAcwP8Flr/erN/CE2z/sHmrQXD8PAwhoaGYpYIVChmp+JkZGSgpaUFhBCkpaX5qOOE4vvT9wjl/fmkn1jz6FUqlc9RpqenBwcOHMBHH32ErVu34j/+4z9w9dVXh3y/aMQvALzIcVwKgEcA3AvgFaGfI64hPd/g6R9qfHwcdXV1QR80On1GKKjRajQalJeXh72ePjhKpRKbN2/G9PQ0DAYDMyIa1gFAY2NjRCQXseCX3YKdCwOF0PzauU6nQ05OTkgjAuDTwRfLRCDVFkhJSUF5eTk4jvNhyYnl+wPBST/0eeEfAaJFMNINx3GoqKjA9u3bUV1djfvuu0/QDAYJxC9+AOCnAKaFf4p5KsvRoZAJCQlhhzeKye7zZagaGhrCXh8oOUfLNkuWLGFG1NLSgsnJSeTm5rLscqxC+Ui9rUajmaOVZzQafYwoJyeHvSchBB0dHXA6nT7n6FjA4/Ggvr4eWVlZc+i/wfj+DQ0N8Hq9Yfn+gK/3n5mZQU9PD5YuXcoiAWr00Ri/0CEU/KRwrMQvOI6rAbCUEPINjuOWCH0dMA9lOTq8sbS0NKzmF/91oRBMhirca8L1sGs0Gna237p1K6anpzE2Noa2tjbGmsvKyhLE/hOynp6eHkxNTUXtbWlmPysry8eI6uvr4fV6WdY/KSkJa9asienRhHIfCgoKwiocBeP7DwwMwGKxhOX703IiZR8G6vWnmvqU9CMU4Rpnpqen50iqxVD8YiuAjRzH9WLWhnM5jnuPEHIJwiCuZ/jp6WmcOnVK0JQW/utCZelDyVAFglAyDW03pXVvlUrFPCUtnRkMBvT29kKtViMnJyck+ywUaL+8UqnEunXrJDVAfyOy2+04deoUgFkDaW1t9cmeSwmn04nTp0+jrKwMeXmBItbQEMP3p8a+atWqOQo//NDfn/RDrwvn/YUMoRBTlotG/OLsmf43AHDWw78hxNiBGHp4vuoNIQTDw8OwWq24+OKLRbGYQmXpw8lQ+UMMTbapqQlarTagAfJLZ0uXLp2jNkvD52D1Zz5oOTIeiUDam79kyRKWPaf6cl1dXdBqtcyIxCrM+IMa4NKlS5kSUTQIxfe3Wq1wuVyoqKgIWRrzJ/3Q50HIgA8hajeRiF/s27cPAL4AEeIXgm8SADEP6anxKBQKJCcni6YsBgvphchQ8SmWQo3d4XCgvr4eBQUFKC4Oq/oLYFaiuaSkBCUlJXC73RgfH0d/fz8sFgsyMjJY8sz/gaGU3NLS0qBDKKUCnatXWVnpIwXG15anoX9TUxM8Hg+ysrIEb1x8TE9PM/JOtI09wUD5/hkZGWxWgM1mw/Hjx0Xz/QMN+PAv+wlRuxFTh+eJXwCzBg9AmPgFH4SQXgCCavBAjA2eP7yxuLgYn3zyiej3CBTSC5Gh4vfRC2XOWa1WNDY2YtmyZUE3kXBQqVQsDOV70M7OTiQmJrLQ3+l0Mi0+MZTcSGC1WtHQ0OAjaBkINGFZVlYGl8vls3FR4kxWVlZYT9fQ0IA1a9YELLNKCXovf6KQWL4/EL7s53Q6Q5b8Fj3Tzmw2M9XUaB5ovoen52o+9TYY6JcjJDkHfE48qaqqkuyL43tQ2qllMBjw2WefYWZmBsXFxTFtgAFmv4e2tjbR9F+1Wo38/Hzk5+f7EGe6u7uZB83JyfEJ/SlTL1qqsRBQnkKge/H5/pQkI5bvD3x+9p+amsLo6ChWr14d9OwfSLF2ISJmBj8+Ph5yeKNQ0DM8LeVptVpB8+IVCgVcLhfz6KGuHxgYwNjYWEw70GjJb2pqis2Zs1gs6OzsxMzMjOBuOTHQ6/VMNCSa74ESZ3Q6HZYtW8Y8aHNzM1wuF+suGxwclIypFwpTU1Nobm7GunXrwrYH+5NkxPL9bTYbmpqaUF1djZSUlKCkH5PJtOAHSQIxNPhly5aJ5sAHAjX4EydOoKSkRFApj2qL6fV6FBQUBI0EvF4v2tvb4Xa7sX79+pjWomnZbXJykpXdaOuvf7ecFCW/oaEhjIyMRC2JHQj+HrSnpwddXV3QaDTo7u5mob8U5Up/TExMoLW1FevWrRO9sYjh+6tUKtZIxD8yBMr8f/zxx+jq6lrwenYAwIVR6ohYxoMquvLx8ccfY+vWraISQGazGcePH0ddXZ2gowFNztlsNoyMjGB8fJyVzfghKB13lZ6ezphfsQLl33MchxUrVoR8MGj5yWAwBF17KFBOwuTkJKqqqmLKngOAkZER5tlVKhUL/enaaegvhdenx5NoI5ZA4B9bTCYTFAoFpqensXr16pD6iSdOnMBDDz2EgwcPoqysLNLbx00XK64Gf/ToUUGNLBRUhsrj8eCiiy4Ke32w5NzMzAwMBgMMBgM8Hg8yMjJgNBoDNutIDbqx6HS6iMpudO1GoxFutztk5pwQgra2Nng8HqxatSrmHmdwcBBjY2NYt25dQG9Oy5UGg4Elz+ixRezfwWQyoaOjA+vWrZPc2P1B+SJ5eXmwWCxB+f6nT5/G1772Nfz5z3/G0qVLo7nl+WnwJ06cQFVVVdjSHH1wZ2ZmUFVVhWPHjuGCCy4I+xqhybnm5mYkJSUxiaPc3NyYiEzSEVj8gZHRgJb8DAaDT8mPltUaGxuRlJSEpUuXxlxMsbe3FxMTE4KjCCqWYTQaMTk5KUrfb3x8nHXySalCGwi0pLhmzRqWhKN8f6PRCLPZjFOnTkGv1+P111/HwYMHsXz58mhve34a/KlTp7BixYqQiRa+DFVlZSU4jsPHH38c0uCFTH8BZst5fX19qK6uRmJiIjs76/V6TE1NIS0tDbm5uZKwzmiJb/ny5aKm2AoFLfkZDAaYTCY4nU5kZmZixYoVMc3800qJ3W7H6tWrI4oi+Ky58fHxkPp+dIRWTU1NzCsatIy8evXqoCVFQgiOHDmCH/3oR4x6/fzzz6OioiKaW5/7Bk8z63zU19ejvLw8aPkimAzVJ598gs2bNwdUOxVKk6UJs6qqqoDhJyEEk5OT0Ov1MJlMPjVzsQ8aHY28du3amGdunU4nCz8JITAajQA+7/KTsjxGIy9CSNAhIJGA6vsZDAYffT+n04n+/n7U1NRInnj0BzV2PjU3ENrb23HnnXfi+eefR1VVFSYnJ5GUlBTt+s5Pg29ubkZBQUHA5FsoGapjx46htrbW548q1NjpVBaVSoXly5cL8ki04YSenTmOQ25urqDk08jICAYGBrBu3bq4hZ/+RCGn08lyFna7XZKSH/07arXamB4ZaNTV39+PiYkJZGdnM32CWBm93W7H6dOnwxp7T08PbrvtNjz33HOora2VcglxM/i4dcsBwWmy4WSo/Nl2QplzTqeTCVoI4dpT8BtOysvLYbfbYTAYQnLlaXZ8YmKCNdvEEpR4EojRptFoUFRUhKKiIklKfh6PBw0NDcjIyMCSJUti8Gk+B22jJoRg27Zt7G9P9f34ob8Umw419pUrV4Y09v7+ftx+++3Yt2+f1MYeV8TMw3u93jlKNZ2dnUhNTWWdU7Rc5Xa7sWbNmqDnZv7Zn2/s4fqTGxoafLjjUsDtdrPw02q1suytXj/b3bhy5cqYZ8fpkUGsLn0kJT/a3JObmyu4tyAaDA8PY2RkBDU1NXOeB9owYzQaMTMzw0Q+ItX34xt7KM7/0NAQbr75Zvz6178OmzyOEOd+SB/I4Ht7e5lQAx0KmZ2dHVaGip79k5KSBCXnqGJMrM/QXq+XHUWoWENubm7MSCfArFJKX1+fJEcG/3Klf8nP5XLh9OnTKC4ujnn5Epgt8+n1eqxbty5s0tQ/cy5W38/hcDBHEorfMTo6iptuugk///nPsX37dtGfSSDOT4MfGBgAIQSZmZmsw0mIXFRTUxMKCgrYgxjK2AcHBzEyMoLq6uqYn6H5Zbf8/HxYLBbo9XqMj49Do9Ew7ynVOgYGBqDX61FdXS35eZY2y9DIJTU1FZOTk6isrIyol10s+vv7MT4+jurqatEVEv+cCxBa30+osev1etx44414+umncfnll4v/UMJx7hs8IQROp9PnZ8PDwxgfH4fFYmHcZCHvQ0dDFRcXQ6fTBTR4Ktlkt9tDHg+kAj0yBCu70UYZg8EAQkhUWXP+EIq1a9fG5bOdOnUKqampbOa5f8Xi6NGjbADo5s2bo7ofnxkoxXGISpMZjUbYbDYffT+3241Tp05h2bJlIculRqMRN954I37wgx+EFaSUAOefwRNC0NjYCJPJhK1btwqWoaLdSRMTE9Dr9ZicnJxTL6dJpZSUlLiQTijFU+iRgT6Aer0edrudhc5CyD6EELS0tIDjOElLYcFANzKasQ5UsRgcHMQ999wDl8sFjUaDN998M2Kj7+npgcViCSlZHQ34+n4mkwl2ux1FRUVYsmRJ0GfQbDbjhhtuwHe/+11cf/31kq8pAM6vLD2VoXK5XMjNzRVl7FSEgC90SOvlnZ2d0Gq1sNlsKCsrQ0lJScw/y+joKKsNC6V48gUmKeOMTpRNT09njDN/z00HVKakpKCioiLmxk4z//xmEf+KhcPhwMGDB+F0OuH1euF0OvG3v/0NmzZtErU+GrXMzMzEzNiBz/X9UlNTYTabsXz5crhcLqbvR0P/lJQUcByHyclJ3HzzzXj00UfjZexxRcw8PDB7VuLLUKWlpWFoaAhr1qwJ+TqhzLnJyUlWLrLZbOzcLHRTEQNCCPr6+mAymVBdXS1JUo42bFCyT1JSEqs5A7PJyry8vLhkx2kXmpDM/9GjR3HttdfC6XRCrVbjV7/6FcrKygSX/OhIMafTidWrV8d8I6PaehUVFT4VG5fLxUL/3t5evPzyy+jv78fDDz+MO+64I6Zr8sO5H9IDs7RIvgyVxWJBT08PqqurA99MIJkGmE2odHd3+zyg09PT0Ov1MBgM4DiOGX+0nVrxaEqhoTNd//T0NHJzc7F06dKYN4tQrrqYxhT/M7zQkh/NyXi93rgcUVwuF06dOjXH2P1hNptx1113Qa1WY3R0FP/yL/+CRx55JKZr4+HcN3iv14tPP/0UK1euZImqmZkZtLa2BiQuiKHJUk9bVVUVNFvtcDhgMBig1+vhdrsZY0vsVBaaH0hLS4t5Gy3w+RmayjQbDAbWJZebm8tCT6lAy3xSc9UDlfyys7MxPDzM2oTjZezl5eUhW1xnZmawZ88e3HbbbbjnnnsAzNboY73R8nDuGzwwG0rx35/W3uvq6nxvIpA5R+WcaQJLqKeloZter8fMzIzgpBldb1FRUVhNdSlAJaLWrl3r029A128wGGCz2RhVNtqBksPDwxgeHsa6detiylWn6+/q6oLb7UZeXh7LW8Tq7E45BEuWLAlp7Ha7Hbfffjt2796Nf/3Xf416E7r77rvxxhtvIDc3F42NjXN+TwjBww8/jMOHDyMpKQnPPvss1q9ff34avMfjwfHjx7Fly5bPbyDQ2GmiJTs7G6WlpRF/MTRpZjAYMDU1hYyMDOTm5s4ZyUQ9bTSClmJAyULhlFy8Xi9MJhMMBgMmJiaQmprKyD5iynXR1L3FghCC5uZmJCQkoLy8HJOTk6zLLykpKeImpWCgpbeysrKQPA+n04k77rgDV1xxBR588EFJIo4PPvgAKSkpuPPOOwMa/OHDh/HLX/4Shw8fxtGjR/Hwww/j6NGj50eWnq9ND/iOcQY+nwlGfxcMtEmkoqIi6rlu/JnmtMVUr9ejvb2dGY9SqWTdbvEQJqQNN0I09fgDGOi5meYzEhISWJNPsPehnYP+o5pjBa/Xi6amJiQnJ7MWUr6wJy35nTlzBhzH+RBmIgE19tLS0pDPisvlwt13343t27dLZuwAsG3bNvT29gb9/cGDB3HnnXeC4zhs2bIFExMT4DiugBAyIskCwiCuzTNiNeKB2WRKa2trTGSP/VVlp6am2IBLnU4Hi8UCrVYb03CXzpmPpOGGP5xh2bJlPsYDgCUtaVKTkpPcbjeqqqpifoam5ViqIRdo/f4lP6PRyAhUYvgKwKyxnz59GqWlpSHZgW63G1/96lexYcMGfPOb34z534GPoaEhn/JxcXEx2tvbiwCcfwZPIdTYh4eHMTg4iNra2rgkUCYmJuD1erF9+3bY7Xbo9XqcOnXKJyqQiiZLS1N2ux01NTWSeFr+IExqPG1tbXA4HMjKyoLNZoNWq8WqVaviYuz19fVM2ksI6HAJfpff8PAwWlpakJaWxs79gTZGauzFxcUhjd3j8eDf/u3fsGrVKvzf//t/42rsCwExD+n5IISAEIKRkRHk5OQE9WhUVcVms2HDhg1xOWPSshs1Pup5KioqWMa5oaGB0WT5nlMsaPJRpVJh7dq1MXno+MZD69Aej4dVSmKZNKPTYrOzsyMmQymVSlbW45f8enp6GN+CjsTyeDwsuRpqgo/H48FDDz2E4uJi7N27d16MvaioCAMDn0+AHhwcBGaHRsYFcfPwNDm3Zs0aJjWVmJg4R9yAjqZKTExEdXV1zL8UPpstWKkoMTGRyTJTcYm2tjY4nU5kZWUhLy9PcLmMGgPtLY/H52tqakJ+fj5KS0t9pLE6OjqQnJzMkn5SHF2o8UnZTss/ulRWVjJd/KamJrjdbrhcLhQWFoY0dq/Xi29+85vQ6XR44okn5s2z79y5E8888wxuvfVWHD16lNKX4xLOAzHO0lNdu0ACkzRhMzY2BqPRCI1Gg8zMTIyOjqK4uFiQ/ny0oGW3wsLCiO5He+P1ej0rl+Xm5iIjIyPgA+Vyudjo5Hh8PhrmUlqvP+hgBtrhp1KpREliB7qf0NHQUsDj8eDUqVNITk6G2+1m+gS0UYY/OurslFb893//d0wTlbfddhvee+89GI1G5OXl4T//8z9Z1+h9990HQggeeOABHDlyBElJSfif//kfbNy48fwoy7ndbsa5DndeNxgMaG5uhlqtZtlmKc/M/pC67EbLZbTBJz09nTX4KBQKNk21oqIiZF1YKkQyqtmfLCOGrEQ3l6Kiorj0ztNIIi8vj22e/sKeSqUSJ06cQF9fHxwOB377298u1GER54fBP/fcc6ioqAioXsKHwWBAV1cXm+tGHzyqIiMVRZZiYmICLS0tMSu7EUJYuc9kMiEhIQFWqxVr1qyJS02fbi6VlZUR38+frMQn+/gbPyW5hMuOS4VAxu4POqL8kUcewWeffYbly5fjvvvuw6233hrz9UWA88PgX3vtNbzwwgtoa2vDZZddhl27dqGurs4nrB8YGIDBYEBVVVXA2rHD4YBer4der4fH40FOTg7y8vIiTpjReWvxGGgAfD5UMzMzExaLBRqNJmytPBrEYlQzzZgbDAbWnkybZDweD2O0RcuREAKv14szZ84gJycnZI6AEIKnn34anZ2deO6552A0GjE6OoqampqYrzECnB8GTzEzM4MjR45g//79OHPmDLZv345rr70Wb7zxBm699VbBc91owkyv18PpdCI7Oxt5eXmC+fH9/f0wGAwxUYwJBIPBgO7ubp/Nxb/BR6garhDQ8cmxJAzR9mTaH2+321FYWIjy8vKY68ZTYw+X/SeE4Be/+AVOnTqFF154QZLv+siRI3j44Yfh8Xjwla98heUEKPr7+/GlL30JExMT8Hg8eOqpp7Bjxw6hb39+GTwfDocDf/nLX/Ctb30Lubm5qK2txQ033IALL7xQ1BcTiB+fl5cXdARTe3s7XC5XxMMTxGJ4eBhDQ0NYt25dUEOgiqy0QYaemSPR4ZucnERLS4uk465DgbY9FxcXM4EPKTsU/UHr+llZWWGN/Te/+Q0+/PBDvPLKK5JsQh6PB8uXL8fbb7+N4uJi1NXV4cUXX8Tq1avZNffeey9qa2vxta99Dc3NzdixY0dIxp0fzg9qbSAkJCSgqakJ//Vf/4Xrr78e//jHP3DgwAF8+9vfxqZNm7B7925s37497BelVqtRUFCAgoICeDweGI1GNgU0MzMTeXl5SE9PZ2yv5ORkLF++PC7lmN7eXpjNZjYlNhi0Wi1KSkpQUlLCOuO6urrYBpabm4u0tLSwa6YqtpFMVI0EVO2VL+9FmXIGgwGtra2sZJmbmxt0FLNQeL1eNDQ0IDMzM6yx79u3D++99x4OHDggWcRx7NgxVFZWMmrwrbfeioMHD/oYPMdxmJqaAjC7+cajShEJ4u7hg8HtduOf//wnXn31Vbz//vuora3F7t27cdlll4k6a/PHR01OTrLurGXLlsXcs0sVSdAGH71eD4vFAp1Ox8p9/u9JySjxGHwBfD6hJVyOgM7B0+v1QctlQkCNPSMjIyxj79lnn8XBgwdx8OBBSfMz+/fvx5EjR/D73/8eAPDHP/4RR48exTPPPMOuGRkZwZVXXgmz2QybzYZ33nkHGzZsEHqL89fDB4NKpcKll16KSy+9FB6PBx9//DH279+P//zP/8Tq1auxe/duXHHFFWGTdZShlZycjDNnzqC4uBh2u52RHPilMilBm0S0Wi3WrFkTlUfzb/Axm80YGxtDW1ubT3ecXq9n1ON45CRoQjDchBZg9vvMy8tDXl4e+wwGgwHt7e1ISUkRJOdNo7P09PSwxv7888/jwIEDeP311+PZx87w4osv4q677sI3v/lNfPLJJ7jjjjvQ2Ni44MqAC8bg+VAqlbj44otx8cUXw+v14vjx43j11Vfx1FNPobKyEjt37sTVV18dNDFF+8r5DTe0VDY2NoaOjo6I20oDgQ5ryMrKimZGeEBQTbasrCyfhFlraysIIdGOKRYMm83GpqqKbWLy/wwWiwUGgwG9vb1Qq9UsccmPUPiNN+Gm3bz66qv405/+hDfffDPi6k0oBKLD+pcD9+3bhyNHjgAAtm7dyublxaNyIQYLJqQXAq/Xi9OnT2P//v146623UFJSgp07d2LHjh0svBwbG0Nvby+bEBsI/LbS8fFxHy05sR1rlK0Xr2ENwOc5goqKCjaCWaVSBTQcKRDL7P/MzAyrWhBCWHtsT08P66ILhb/85S/4zW9+gzfeeCNs1BEp3G43li9fjnfffRdFRUWoq6vDCy+84KPNeM0112DPnj2466670NLSgssvvxxDQ0NCI73zN0svFajs9f79+/Hmm2+yB0Wj0eBnP/uZ4BCX0kspxVer1TLDCfce9Dwr9TirUGvt7OyEw+GYkyPwN5xoG3woqLHzlWxjBVp27erqAiEEhYWFIdtj33zzTfz85z/Hm2++GXKghBQ4fPgwvv71r8Pj8eDuu+/Gd77zHXzve9/Dxo0bsXPnTjQ3N+OrX/0qrFYrOI7D008/jSuvvFLo28sGLwYejwf33nsvTp48iYSEBKSmpmLnzp24/vrrkZOTI+o8zef3U68ZSAWXSjqvXr06Zp6FD0IIWltbBenBOZ1OZvyUrxCJHt7U1BSamppQXV0dl1IfVcbRarVYsmQJS75OTU3NkfP+29/+hieffBKHDx+OC3sxxpANXgwsFgv27duHhx56CBzHoaurCwcOHMDBgweh0Wiwc+dO7Nq1C/n5+aIeeD5JRqFQMK85MzODtra2uNW8oxnVzG/wmZ6eZg0+4UQlaF1f7MDKSMGXwfL/jDT/YjAY8PLLL+P999/H6Ogo3nrrLaxYsSLma4sDZIOXAoQQ9Pf348CBA/jLX/4Cr9eL66+/Hrt370ZxcbEow6GCGENDQ5iZmUFpaSmKiopiXveWclQzv2RJvWagqgXVqI9XXZ9O11Gr1aisrAz5vbz//vvYu3cvLrvsMnzwwQd4/PHHcd1118V8jTGGbPBSgwpvHDhwAK+99hpmZmZw7bXXYteuXYKnugwODmJ0dBSrVq2C2WxmEtjU80vt7Wm7aSyGUfh3ltFSmVKpRGdnp6jJOtGAHlVUKlVYY//kk0/wrW99C2+88QbLklPx00gQji4LAK+88goTy1i3bh1eeOGFiO4VBrLBxxp6vR6vvfYa/vznP8NkMmHHjh3YvXt3QDYeFX6cmppCVVWVTxmPMuTGxsbgcDiY8UerHx/PUc20VNbX1we9Xg+dTof8/HxJlWSD3be1tRVKpRLLli0L+fc6ceIEHnroIRw6dAilpaVR31sIXbajowO33HIL/v73v0On00Gv18eqzCYbfDwxPj6OgwcP4sCBAxgdHcVVV12FL37xi1i1ahVjeiUkJITVwvc/L4uhx/JBeerx6p0HwHTja2tr4XK5oNfrYTQafXIXUnp8KivGcVxYyvPp06fxta99Da+99hqjt0aLTz75BHv37sVf//pXAMCTTz4JAHj88cfZNf/+7/+O5cuX4ytf+Yok9wyBxce0m09kZWXh7rvvxt13342JiQm8/vrreOKJJ9DV1QWVSoXLL78c3/ve98KyplQqFfLz85Gfn8/osQMDA7BYLGHVcChoqS/YGOpYgNJza2trodFooNFoUF5ejvLyctbg09TUxNqToz2+UAoygLDG3tjYiPvuuw/79++XzNiBwOqxR48e9bmGrvHCCy+Ex+PB3r174zE6OqaQDd4PGRkZuOOOO7B7927s2rULS5YsQW9vLy688EJcfvnl2LVrFzZu3BjW+P3psVSBtbW1NezwCyHUVakwNjaG/v7+oPTcQA0+VEaach/ERDBUKpsQEra82NLSgq985St46aWXsHz58og/Y6Rwu93o6OjAe++9h8HBQWzbto0lUM9VyAYfBAqFAo899hgjT9Ce/t/97nd48MEHsX37duzatQtbtmwJS83lD4+gybKxsTG0t7ezWfdqtZq1t8aa4EIxOjqKgYEB1NTUCCIqqdVqn7HXRqMR/f39rDkmWIMPBSUOeTyesIMk29vb8eUvfxnPP/+8z7laKgihyxYXF2Pz5s1Qq9UoLy/H8uXL0dHRMWdU2rkESc7w4bKdDocDd955Jz777DNkZWXh5ZdfjrrENJ+w2+14++23sX//fnz22We44IIL8MUvfhEXXnihKGou5cZTYY7MzEwUFhYiOzs75tLcdK5cTU1N1KOvaXOMXq/HxMQE28T4M++psbtcrrC6+D09Pbj99tvx7LPPBhw8KgWE0GWPHDmCF198kSnm1NbW4vTp07Eg+pw7STsh2c5f//rXqK+vx29/+1u89NJLeO211/Dyyy9LsPz5h9PpxD/+8Q/s378fn3zyCevp37Ztm6AMN02WVVdXM4bc+Pg4k/AOpd8fKYaGhpjck9QbC93E+DPvc3JyYLFYWNtwKGPv7+/Hnj178Pvf/z7mnjQcXZYQgm9+85s4cuQIlEolvvOd78RKE+/cMXgh2c6rrroKe/fuxdatW+F2u5Gfn88kns4nuN1ufPDBB3j11Vfxz3/+k/X0X3rppQEz3MFGNQeS8KYTV6Ntg6UaguvWrYvLgA+r1Yq2tjZYrVbm+YM1+AwNDeGWW27Br371K1xwwQUxXdsCw7mTpReS7eRfo1KpkJ6ejvHx8bg0nMQTKpUKl112GS677DJ4PB589NFHOHDgAPbu3Ys1a9Zg9+7d+MIXvoCkpCQ0NTXBbrcHnCnHn7m2dOlS2Gw2NvYqmq64/v5+mEwmyUZbhQPHcTAYDNBqtdiwYQNTI66vrwcAltRMTEzE6Ogo9uzZg1/84heLzdjjCjlpFyMolUps27YN27Ztg9frxbFjx7B//348+eSTSElJgVKpxMsvvywoXE9OTmZlMtoVV19fz0QwhdTIe3t7MTk5ierq6riJMvT09MBms7FxWklJSSgrK0NZWRmTw/roo4/w7//+7/B6vfj2t7+Niy++OC5rW6yI2uCFZDvpNcXFxXC73ZicnDwfOpwEQ6FQYMuWLdiyZQu+//3v48MPP0RtbS127NiB0tJS1tMvpBSXmJjoYzR6vZ7VyKnx+ze7dHd3w2q1oqqqKq7GbrFYgs7OS0hIQHFxMbRaLTIyMrB9+3b87W9/w2effYbf/va3cVnjYkTUZ3gh2c5f/epXaGhoYEm7P//5z3jllVckWP65hyNHjuCKK66AUqlkPf2vvvoqDh8+jJycHOzatQvXXXedaNIN7SUfGxuDy+ViI6PGxsZgt9ujlt0Sg97eXkxNTWHt2rUhNxiz2YwbbrgB3/3ud3H99dcDiI4bDwjjxwPAgQMHcNNNN+H48ePYuHFjxPeTCOdO0g4In+202+244447cOrUKWRmZuKll14Ky5oK98X97Gc/w+9//3s2D+0Pf/iD5PJS8QTlle/fv5+pt+zcuRPXXXed6J5+SpDp6emBy+VCUVFRUAlvqdHX14eJiYmw0cTk5CRuvPFGfOtb38INN9wgyb2FVIyA2Xbqa6+9Fk6nE88884xs8DzMC5deyBf3j3/8A5s3b0ZSUhJ+85vf4L333jtvSn10XDbt6U9ISMD1118vuKefUle9Xi8qKythMpkwNjYGm83G+P3h+uEjQX9/P8xmc1hjt1gsuOmmm/DAAw9gz549kt1fSMUIAL7+9a/jiiuuwI9//GP85Cc/WVQGv7AkNc+CrwOu0WiYDjgfl156KTurbtmyhc7ZPi/AcRwqKyvx6KOP4qOPPsKzzz4LALjrrrtw9dVX45e//CUGBgYQaLOmkQIArFy5Emq1Gnl5eaiursamTZug0+kwODiITz/9FK2trTCZTAHfRyxoBSCcsdtsNtx666249957JTV2IHDFaGjId/T6yZMnMTAwgGuvvVbSe58rWJBZeiGlPj727duHa665Jh5Lizs4jkNZWRkeeeQRfOMb32A9/ffddx/sdjuuu+467Nq1C+Xl5Ww6S0pKSsDecirhnZOTw9hxo6OjaGtrQ3p6OvLy8kTrxgOztf3x8XGsW7cu5GtnZmZw66234o477sAdd9wR0d8jGni9XjzyyCNsA12MWJAGLwZ/+tOfcOLECbz//vvzvZSYg+M4FBYW4sEHH8QDDzzAevofeeQRmM1mqFQqXHLJJfjOd74TNlz3l46m1Nj29nakpqYiLy/PhxobDIODgzAajWGN3W634//8n/+Dm2++GV/+8pcj+vzhEK5iRHUIL7nkEgCzvQQ7d+7EoUOHFkJYHxcsyDO80LPYO++8gwcffBDvv//+gtP/jifcbjf27NkDr9cLp9OJsbExn55+MWd1PjV2fHwcycnJyMvLC8jvHxwchF6vD8vaczqd+Jd/+RdceeWVePDBB2OWOBRSMeLjkksuWXRn+AXp4evq6tDR0YGenh4UFRXhpZdemiMtdOrUKfzrv/4rjhw5sqiNHZgNlXft2oU777wTwKwm3aFDh/DDH/4QfX19uOKKK7B7925BpBuO45CRkYGMjAymhENHbGu1Wmb8dIR3OGN3uVz48pe/jEsuuSSmxg7MMh2feeYZXHXVVaxitGbNGp+K0WLHgvTwQPhS3xe+8AU0NDQw+afS0lIcOnQo5HueozXaqGCxWPDmm2/iwIEDaG9vZz39GzZsEH1Wt1qt0Ov1GB4ehtvtxtKlS5GXlxe0ScjtduOee+5BbW0tHn/88fOud0JCLO6yXCxwDtdoJcP09DTeeustHDhwAI2Njaynf/PmzYIbaUZGRjA8PIzly5ezYZF8sQ/K7/d4PLjvvvtQWVnJRCBlBIVs8FLjHK7RxgT8nv6TJ0+ynv4LLrggKL+fGrt/Wy2V8Nbr9TCbzfj4448xMDCAsrIyPPHEE7Kxh8firsPHAnKN1hdarRbXX389nnvuOXz22Wf44he/iAMHDuCCCy7Agw8+iHfffRdOp5NdPzo6iqGhoYBndq1Wi9LSUmzcuBHr1q1Da2srPv30U7z//vv4n//5n3h/NBkhsCCTdvOBxVyj1Wg0uPrqq3H11Vf79PQ//vjjWL9+PfLy8mCxWPD000+HHe/8k5/8BCUlJXjttdcwMTGB3t7eqNe32GjWMQUhJNR/5w0+/vhjcuWVV7J/P/HEE+SJJ55g/56YmCBZWVmkrKyMlJWVkYSEBFJQUECOHz8+H8tdEHC73eQHP/gBKS4uJjU1NeTWW28lL774IjEYDMRms/n8Z7FYyDe+8Q3y1a9+lXg8HknXUFFRQbq6uojD4SDV1dWkqanJ55q///3vxGazEUII+fWvf01uueUWye4fJ4SzQ8n+WzQG73K5SHl5Oenu7mYPTmNjY9Drt2/fvqiNnZDZv9mXv/xlMjExQTweD/nkk0/II488Qqqrq8mNN95I/vjHP5KxsTFitVrJo48+Sr70pS8Rt9st6RrCbdT+OHnyJLngggskXUMcEDeDXzRneH6NdtWqVbjllltYjTZcOS8Ujhw5ghUrVqCyshJPPfVUwGteeeUVrF69GmvWrMHtt98e8b3iDZVKhT/84Q9IT09nPf0//elPcerUKTz++ONobGzEVVddhU2bNqGtrQ379u2TXDZLSO6Fj/OZZi0JwuwIMkJASLjZ3t5OampqiMlkIoQQMjY2Nh9LjRk8Hg85ePAgsVgsMXn/V199ldxzzz3s3//7v/9L7r///oDX/vGPfySbN28mdrs9JmuJIWQPfy5ASFff7373O9x///3Q6XQAcN6xAhUKBXbu3BkzLX0hikrALM36Rz/6EQ4dOiRa628xQTb4KCAk3Gxvb0d7ezsuvPBCbNmyBUeOHIn3Ms9p8GnWTqcTL7300hyKLKVZHzp06LzbUKWGXJaLMc7HcUXxhBB+/Le//W1YrVbcfPPNAITRrBcrZIOPAot1XFG8sWPHDuzYscPnZ9///vfZ/7/zzjvxXtI5CzmkjwJCws3du3fjvffeAzA7Zaa9vV3SKagyZIiBbPBRQEip76qrrkJWVhZWr16NSy+9FD/+8Y8FSXSHK/f19/fj0ksvRW1tLaqrq3H48GHJP5+M8w+LpnnmXIKQzr57770XtbW1+NrXvobm5mbs2LFDEhqrjHmB3DyzmCGk3MdxHKampgDMSj4XFhbOx1JlnGOQDX4BQki5b+/evfjTn/6E4uJi7NixA7/85S/jvcyIEe644nA4sGfPHlRWVmLz5s1y5CIhZIM/R/Hiiy/irrvuwuDgIA4fPow77rgDXq93vpcVFh6PB/fffz/eeustNDc348UXX0Rzc7PPNfv27YNOp0NnZye+8Y1v4NFHH52n1Z5/kA1+AUJIuW/fvn245ZZbAABbt26F3W6H0WiM6zojgZDjysGDB/GlL30JAHDTTTfh3XfflUQ7X4Zs8AsSQsp9paWlePfddwEALS0tsNvtyMnJmY/lioKQ40qw8eIyoods8AsQQsp9P/3pT/G73/0O69atw2233YZnn31WlpKSERbhynIyziNwHPcHANcB0BNC1gb4PQfgFwB2AJgGcBch5KTEa9gKYC8h5Kqz/34cAAghT/Ku+evZaz7hOE4FYBRADpEf1qghe/jFhWcBXB3i99cAWHb2v3sB/CYGazgOYBnHceUcx2kA3ArAn/h+CMCXzv7/TQD+Lhu7NJANfhGBEPIBAFOIS3YB+N+zPdqfAsjgOK5A4jW4ATwA4K8AWgC8Qghp4jju+xzH0UTFPgBZHMd1AngEQOABAjJEQ26ekcFHEYAB3r8Hz/5sRMqbEEIOAzjs97Pv8f7fDuBmKe8pYxayh5chYxFBNngZfAwBKOH9u/jsz2ScJ5ANXgYfhwDcyc1iC4BJQoik4byM+YV8hl9E4DjuRQCXAMjmOG4QwH8AUAMAIeS3mD1X7wDQidmyXGwGucuYN8h1eBkyFhHkkF6GjEUE2eBlyFhEkA1ehoxFBNngZchYRJANXoaMRQTZ4GXIWESQDV6GjEUE2eBlyFhE+P8B8h8HBNpQHOwAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "plot_vectors(ax, xy_plane.T)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "eb8c5c19", + "metadata": {}, + "source": [ + "What we want to do is to put this slice into a 3D space - in our case it is convenient to represent this 3D space as an $n\\times n\\times n$ cube of voxels. For the purposes of `griddata`, we need that cube as a set of vectors pointing to each point (the same as how we represented `xy_plane`). So, we define the $8$ vectors needed and store them in `xyz_cube`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "ba3ae521", + "metadata": {}, + "outputs": [], + "source": [ + "xyz_cube = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "81fcd24c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "plot_vectors(ax, xyz_cube.T)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6e079ef8", + "metadata": {}, + "source": [ + "Now we want to use `griddata` to interpolate this slice. What is happening here is that `griddata` takes the points and values from `xy_plane` and `test_slice` and assumes they are in a 3D space that is otherwise filled with zeroes. Then, it polls each of the points in `xyz_cube` within this space, linearly interpolating their values based on what we inserted via `xy_plane` and `test_slice`. However..." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "3be269fd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QH6154 Qhull precision error: Initial simplex is flat (facet 1 is coplanar with the interior point)\n", + "\n", + "While executing: | qhull d Qz Q12 Qt Qc Qbb\n", + "Options selected for Qhull 2019.1.r 2019/06/21:\n", + " run-id 1683676233 delaunay Qz-infinity-point Q12-allow-wide Qtriangulate\n", + " Qcoplanar-keep Qbbound-last _pre-merge _zero-centrum Qinterior-keep\n", + " Pgood _max-width 1 Error-roundoff 2e-15 _one-merge 1.8e-14\n", + " Visible-distance 1.2e-14 U-max-coplanar 1.2e-14 Width-outside 2.4e-14\n", + " _wide-facet 7.3e-14 _maxoutside 2.4e-14\n", + "\n", + "precision problems (corrected unless 'Q0' or an error)\n", + " 1 degenerate hyperplanes recomputed with gaussian elimination\n", + " 1 nearly singular or axis-parallel hyperplanes\n", + " 1 zero divisors during back substitute\n", + " 2 zero divisors during gaussian elimination\n", + "\n", + "The input to qhull appears to be less than 4 dimensional, or a\n", + "computation has overflowed.\n", + "\n", + "Qhull could not construct a clearly convex simplex from points:\n", + "- p3(v5): 1 1 0 0.91\n", + "- p4(v4): 0.5 0.5 0 1\n", + "- p2(v3): 0 1 0 0.45\n", + "- p1(v2): 1 0 0 0.45\n", + "- p0(v1): 0 0 0 0\n", + "\n", + "The center point is coplanar with a facet, or a vertex is coplanar\n", + "with a neighboring facet. The maximum round off error for\n", + "computing distances is 2e-15. The center point, facets and distances\n", + "to the center point are as follows:\n", + "\n", + "center point 0.5 0.5 0 0.5636\n", + "\n", + "facet p4 p2 p1 p0 distance= 0\n", + "facet p3 p2 p1 p0 distance= 0\n", + "facet p3 p4 p1 p0 distance= 0\n", + "facet p3 p4 p2 p0 distance= 0\n", + "facet p3 p4 p2 p1 distance= 0\n", + "\n", + "These points either have a maximum or minimum x-coordinate, or\n", + "they maximize the determinant for k coordinates. Trial points\n", + "are first selected from points that maximize a coordinate.\n", + "\n", + "The min and max coordinates for each dimension are:\n", + " 0: 0 1 difference= 1\n", + " 1: 0 1 difference= 1\n", + " 2: 0 0 difference= 0\n", + " 3: 0 1 difference= 1\n", + "\n", + "If the input should be full dimensional, you have several options that\n", + "may determine an initial simplex:\n", + " - use 'QJ' to joggle the input and make it full dimensional\n", + " - use 'QbB' to scale the points to the unit cube\n", + " - use 'QR0' to randomly rotate the input for different maximum points\n", + " - use 'Qs' to search all points for the initial simplex\n", + " - use 'En' to specify a maximum roundoff error less than 2e-15.\n", + " - trace execution with 'T3' to see the determinant for each point.\n", + "\n", + "If the input is lower dimensional:\n", + " - use 'QJ' to joggle the input and make it full dimensional\n", + " - use 'Qbk:0Bk:0' to delete coordinate k from the input. You should\n", + " pick the coordinate with the least range. The hull will have the\n", + " correct topology.\n", + " - determine the flat containing the points, rotate the points\n", + " into a coordinate plane, and delete the other coordinates.\n", + " - add one or more points to make the input full dimensional.\n", + "\n" + ] + } + ], + "source": [ + "try:\n", + " inserted_slice = griddata(xy_plane, test_slice.reshape((4,)), xyz_cube, fill_value=0).reshape(2, 2, 2)\n", + "except QhullError as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "dcbcdc63", + "metadata": {}, + "source": [ + "Rest assured there's no syntax errors here. The problemn is outlined well by the line: \n", + "\n", + "```Qhull precision error: Initial simplex is flat (facet 1 is coplanar with the interior point)```\n", + "\n", + "\n", + "What that means is we've inserted a 2D object and expected it to interpolate 3D data. \n", + "\n", + "Why is this a problem? Let's drop a dimension and give this some thought. We have a line of points in black and we want to interpolate the value of a red point that is near the line:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "3f48b273", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes()\n", + "x = np.linspace(0,1)\n", + "y = np.linspace(0,1)\n", + "plt.plot(x,y,'k-')\n", + "plt.plot(0.5,0.5, 'r.')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "85cfeced", + "metadata": {}, + "source": [ + "It's easy for us to say that this point is clearly on the line. And in fact, a computer would probably agree in this case. But, this isn't generally so easy because of **float precision** (or rather a lack thereof). It is entirely reasonable that this point $(0.5,0.5)$ could be represented by the computer as, e.g., $(0.500000001,0.49999999)$. **Is this on the line?**\n", + "\n", + "You might, entirely reasonably, say yes, of course it's on the line. But, unless some tolerance is introduced, the computer would say no this isn't on the line. If we did want tolerance, the actual bounds of the line might look something like:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "2936dc2d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes()\n", + "x = np.linspace(0,1)\n", + "y = np.linspace(0,1)\n", + "plt.plot(x,y,'k-')\n", + "plt.plot(x-.05,y+.05, color='black', linestyle='dotted')\n", + "plt.plot(x+.05,y-.05, color='black', linestyle='dotted')\n", + "plt.plot(0.5,0.5, 'r.')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1f516d05", + "metadata": {}, + "source": [ + "And anything within the dotted lines is \"on\" the line. \n", + "\n", + "The problem we run in to with scipy's `griddata` is that it doesn't put this tolerance here by default. When you give it an underdimensioned object, it just throws an error as it will never interpolate anything to be \"in line\" with that underdimensioned object. \n", + "\n", + "We can get around this by adding our own tolerance, in much the same way as I did with those lines above. If we add coordinates to `xy_plane` by copying the plane with some small $z$ difference, say $z \\pm 0.05$, then the plane has \"bounds\" and interpolation will know with confidence what is and isn't on the plane.\n", + "\n", + "In doing this copying, we will also need to copy `test_slice` twice. We assume that `test_slice` values are constant along $z$, as in concept this slice is still 2D. " + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "b2f1ab3f", + "metadata": {}, + "outputs": [], + "source": [ + "z_tolerance = np.array([[0, 0, 0.05],])\n", + "xy_plane_tol = np.concatenate((xy_plane + z_tolerance, xy_plane, xy_plane - z_tolerance), axis=0)\n", + "test_slice_tiled = np.tile(test_slice, (3,))" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "85360c86", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "plot_vectors(ax, xy_plane_tol.T)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3fead8b8", + "metadata": {}, + "source": [ + "Do note the scale of the $z$ axis in the plot above. We have `xy_plane` with some very small tolerances in either direction of $z$. Now, when we try the interpolation again:" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "4f042633", + "metadata": {}, + "outputs": [], + "source": [ + "inserted_slice = griddata(xy_plane_tol, test_slice_tiled.reshape((12,)), xyz_cube, fill_value=0).reshape(2, 2, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "89539c4a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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VigUzIeaj4svvpjtNbdv307nLK8IcByHqqoqfP3rX8cHH3yA559/HgBw22234corr8RPf/pTjI2NwdfDmUYCALBu3TrI5XLk5+ejrq4OW7duRWZmJsbHx/Hxxx+ju7sbs7OzPo8jFjRDH4zcZrMZN954I+68886IkhvwXdGZmPAcvX3q1CmMjY3h6quvjui5lyuWZRZdSOmNj+eeew5XXXVVLC4t5uA4DmVlZXjwwQfxwAMPsJ72u+66C1arFddccw327t2L8vJyNv0jJSXFZ281tZTOzc1l6rPp6Wn09PQgPT0d+fn5on3LgYXaularRX19fcD3WiwW3Hjjjbjllltwyy23hPT7CAdutxsPPvgge2CeC1iWBBeD3/72tzhx4gTefffdpb6UqIPjOBQWFuK+++7Dvffey3raH3zwQeh0OshkMlx00UX45je/GTT89rYyplLT3t5epKamIj8/30Nq6vHeY8cgee89uHfswGhRETQaTVByW61W/Nu//Rs++9nP4vbbbw/7d+ELwSo61EfvoosuArCgxd+zZw8OHz68HML0qGBZ7sGF7qX++te/4r777sO777675P7TSwmn04n9+/fD7XbDbrdjZmbGo6ddzF6bLzXVarVITk5Gfn4+08dLjh1DwtVXA3Y7iFyOth//GBX/9m8BVXF2ux2f+9zncPnll+O+++6LWqJPSEWHj4suumjF78GX5Qre1NSEvr4+DA0NoaioCK+88soiq5zm5mb8+7//O44ePXpOkxtYCH337t2LW2+9FcCCp9rhw4fx/e9/HyMjI7jsssuwb98+QSIXjuOQkZGBjIwM5vRCRy4rlUqsfestJNjt4FwuEEKwbnoargDkdjgcuP3223HRRRdFldzAgpLw6aefxhVXXMEqOjU1NR4VnXMOhJBAf5YMb731FlmzZg2pqKgg3//+9wkhhPyf//N/yKFDhwghhFx66aUkLy+P1NfXk/r6erJ79+6gx3z77bdJdXU1qaysJI8//rjf1x04cIAAIMePH4/Mh1lCzM3NkZdffplcf/31pK6ujjzwwAPknXfeIUajkZjNZlF/ZmZmSP+LLxJnQgJxSSTEpVSSuT/9ye/rDQYDuf7668kPfvAD4na7l/pXsZwRjIch/1mWIXo0cBbXSCOG+fl5vP322zh48CDa29tZT/u2bdsEN55MTU3B+Oc/o3J8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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "plot_3d_matrix(ax, inserted_slice)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0d6bdc4b", + "metadata": {}, + "source": [ + "Note the red coordinates represent values of $1$ and the blue values of $0$. We have an inserted slice! And no errors popped up, because scipy knew how to handle the \"2D\" slice when we told it exactly how 2D we wanted that slice to be. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "635a31d1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 274681d2eb25e20a5e32a087b070350da2de9b7a Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 11:14:29 -0700 Subject: [PATCH 26/52] My demo float imprecision was bad --- Insert Slice Tolerance Demo.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Insert Slice Tolerance Demo.ipynb b/Insert Slice Tolerance Demo.ipynb index 901dc24..2f26ed6 100644 --- a/Insert Slice Tolerance Demo.ipynb +++ b/Insert Slice Tolerance Demo.ipynb @@ -266,7 +266,7 @@ "id": "85cfeced", "metadata": {}, "source": [ - "It's easy for us to say that this point is clearly on the line. And in fact, a computer would probably agree in this case. But, this isn't generally so easy because of **float precision** (or rather a lack thereof). It is entirely reasonable that this point $(0.5,0.5)$ could be represented by the computer as, e.g., $(0.500000001,0.49999999)$. **Is this on the line?**\n", + "It's easy for us to say that this point is clearly on the line. And in fact, a computer would probably agree in this case. But, this isn't generally so easy because of **float precision** (or rather a lack thereof). It is entirely reasonable that this point $(0.5,0.5)$ could be represented by the computer as, e.g., $(0.499999999,0.50000001)$. **Is this on the line?**\n", "\n", "You might, entirely reasonably, say yes, of course it's on the line. But, unless some tolerance is introduced, the computer would say no this isn't on the line. If we did want tolerance, the actual bounds of the line might look something like:" ] From 889caae28f7e96216f2e81d9fade01781556e257 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 11:20:45 -0700 Subject: [PATCH 27/52] Removed centering note from generate_slices docstring --- .../iterative_refinement/expectation_maximization.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ae5103c..b0fb7ea 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -458,11 +458,6 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): Interpolate values from map_3d_f onto 3D coordinates. - - Shift the space into a centered position before rotating and - revert shift after rotation. This preserves the bounds of the - space. - Parameters ---------- map_3d_f : arr, float (not complex) From 29284802dcf456ca3506d1328592d02d6b84a8c0 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 11:55:12 -0700 Subject: [PATCH 28/52] Test refactoring, reworked xy plane and cube into one fn --- .../expectation_maximization.py | 83 ++++++------------- tests/test_expectation_maximization.py | 39 ++++++--- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index b0fb7ea..c4f5fdd 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -131,7 +131,7 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): .reshape(map_shape) ) - xyz_voxels = IterativeRefinement.generate_xyz_voxels(n_pix) + xyz_voxels = IterativeRefinement.generate_cartesian_grid(n_pix, 3) insert_slice_v = np.vectorize( IterativeRefinement.insert_slice, @@ -160,7 +160,7 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): ) rots = IterativeRefinement.grid_SO3_uniform(n_rotations) - xy0_plane = IterativeRefinement.generate_xy_plane(n_pix) + xy0_plane = IterativeRefinement.generate_cartesian_grid(n_pix, 2) slices_1, xyz_rotated = IterativeRefinement.generate_slices( half_map_3d_f_1, xy0_plane, rots @@ -398,59 +398,45 @@ def grid_SO3_uniform(n_rotations): return rots @staticmethod - def generate_xy_plane(n_pix): - """Generate (x,y,0) plane. + def generate_cartesian_grid(n_pix, d): + """Generate (x,y,0) plane or (x,y,z) cube. - x, y axis values range [-n // 2, ..., n // 2 - 1] + Axis values range [-n // 2, ..., n // 2 - 1] Parameters ---------- n_pix : int Number of pixels along one edge of the plane. + d : int + Dimension of output. 2 or 3. Returns ------- - xy_plane : arr - Array describing xy plane in space. - Shape (3, n_pix**2) + xyz : arr + Array describing xy plane or xyz cube in space. + Shape (3, n_pix**d) """ axis_pts = np.arange(-n_pix // 2, n_pix // 2) - grid = np.meshgrid(axis_pts, axis_pts) - - xy_plane = np.zeros((3, n_pix**2)) - - for d in range(2): - xy_plane[d, :] = grid[d].flatten() - - return xy_plane - - @staticmethod - def generate_xyz_voxels(n_pix): - """Generate (x,y,z) cube. + if d == 2: + grid = np.meshgrid(axis_pts, axis_pts) - x, y, z axis values range [-n // 2, ..., n // 2 - 1] + xy_plane = np.zeros((3, n_pix**2)) - Parameters - ---------- - n_pix : int - Number of pixels along one edge of the cube. + for d in range(2): + xy_plane[d, :] = grid[d].flatten() - Returns - ------- - xyz : arr - Array describing xyz cube in space. - Shape (3, n_pix**3) - """ - axis_pts = np.arange(-n_pix // 2, n_pix // 2) - grid = np.meshgrid(axis_pts, axis_pts, axis_pts) + return xy_plane + if d == 3: + grid = np.meshgrid(axis_pts, axis_pts, axis_pts) - xyz = np.zeros((3, n_pix**3)) + xyz = np.zeros((3, n_pix**3)) - for di in range(3): - xyz[di] = grid[di].flatten() - xyz[[0, 1]] = xyz[[1, 0]] + for di in range(3): + xyz[di] = grid[di].flatten() + xyz[[0, 1, 2]] = xyz[[2, 0, 1]] - return xyz + return xyz + raise ValueError(f"Dimension {d} received was not 2 or 3.") @staticmethod def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): @@ -520,23 +506,9 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): """ n_rotations = len(rots) n_pix = len(map_3d_f) - slices = np.empty((n_rotations, n_pix, n_pix), dtype=np.complex_) - overwrite_empty_with_zero = 0 - slices[:, :, 0] = overwrite_empty_with_zero + slices = np.empty((n_rotations, n_pix, n_pix), dtype=float) xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) - offset = np.array( - [ - [ - 0, - ], - [ - 0, - ], - [ - z_offset, - ], - ] - ) + offset = np.array([[ 0, 0, z_offset ], ]).T xy_plane = np.concatenate( (xy_plane + offset, xy_plane, xy_plane - offset), axis=1 ) @@ -665,8 +637,7 @@ def insert_slice(slice_real, xy_rotated, xyz): Rotated slice in 3D voxel array. Shape (n_pix, n_pix, n_pix) count_3d : arr - Voxel array to count slice presence: 1 if slice present, - otherwise 0. + Voxel array to count slice presence. Shape (n_pix, n_pix, n_pix) """ n_pix = slice_real.shape[0] diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 8df8b5e..71458d9 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -75,19 +75,36 @@ def test_grid_SO3_uniform(test_ir, n_particles): assert rots.shape == (n_particles, 3, 3) -def test_generate_xy_plane(test_ir, n_pix): - """Test generation of xy plane.""" - xy_plane = test_ir.generate_xy_plane(n_pix) +def test_generate_cartesian_grid(test_ir, n_pix): + """Test generation of xy plane and xyz cube.""" + xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) assert xy_plane.shape == (3, n_pix**2) n_pix_2 = 2 plane_2 = np.array([[-1, 0, -1, 0], [-1, -1, 0, 0], [0, 0, 0, 0]]) - xy_plane = test_ir.generate_xy_plane(n_pix_2) + xy_plane = test_ir.generate_cartesian_grid(n_pix_2, 2) assert np.allclose(xy_plane, plane_2) assert np.isclose(xy_plane.max(), n_pix_2 // 2 - 1) assert np.isclose(xy_plane.min(), -n_pix_2 // 2) + xyz_cube = test_ir.generate_cartesian_grid(n_pix, 3) + assert xyz_cube.shape == (3, n_pix**3) + + n_pix_2 = 2 + cube_2 = np.array( + [ + [-1, 0, -1, 0, -1, 0, -1, 0], + [-1, -1, 0, 0, -1, -1, 0, 0], + [-1, -1, -1, -1, 0, 0, 0, 0] + ] + ) + + xyz_cube = test_ir.generate_cartesian_grid(n_pix_2, 3) + assert np.allclose(xyz_cube, cube_2) + assert np.isclose(xyz_cube.max(), n_pix_2 // 2 - 1) + assert np.isclose(xyz_cube.min(), -n_pix_2 // 2) + def test_generate_slices(test_ir, n_particles, n_pix): """Test generation of slices. @@ -113,7 +130,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): """ map_3d = np.ones((n_pix, n_pix, n_pix)) rots = test_ir.grid_SO3_uniform(n_particles) - xy_plane = test_ir.generate_xy_plane(n_pix) + xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) assert slices.shape == (n_particles, n_pix, n_pix) assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix**2) @@ -243,21 +260,17 @@ def test_insert_slice(test_ir, n_pix): Pull a slice out, put it back in. See if it's the same. """ - xy_plane = test_ir.generate_xy_plane(n_pix) + xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) map_plane_ones = np.zeros((n_pix, n_pix, n_pix)) map_plane_ones[n_pix // 2] = np.ones((n_pix, n_pix)) - rot_90deg_about_y = np.array( - [ - [[0, 0, 1], [0, 1, 0], [-1, 0, 0]], - ] - ) + rot_mat = test_ir.grid_SO3_uniform(1)[0] slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones, xy_plane, rot_90deg_about_y + map_plane_ones, xy_plane, rot_mat ) - xyz_voxels = test_ir.generate_xyz_voxels(n_pix) + xyz_voxels = test_ir.generate_cartesian_grid(n_pix, 3) inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], xyz_voxels) From 00657a59a6962e8de85c50ef07e6637ac2918d8a Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 18:55:44 +0000 Subject: [PATCH 29/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 6 +++++- tests/test_expectation_maximization.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index c4f5fdd..edca890 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -508,7 +508,11 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): n_pix = len(map_3d_f) slices = np.empty((n_rotations, n_pix, n_pix), dtype=float) xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) - offset = np.array([[ 0, 0, z_offset ], ]).T + offset = np.array( + [ + [0, 0, z_offset], + ] + ).T xy_plane = np.concatenate( (xy_plane + offset, xy_plane, xy_plane - offset), axis=1 ) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 71458d9..626571f 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -96,7 +96,7 @@ def test_generate_cartesian_grid(test_ir, n_pix): [ [-1, 0, -1, 0, -1, 0, -1, 0], [-1, -1, 0, 0, -1, -1, 0, 0], - [-1, -1, -1, -1, 0, 0, 0, 0] + [-1, -1, -1, -1, 0, 0, 0, 0], ] ) From 98c8e6bfec5840ad3c0f6246e13b8d8a4463c5e3 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 12:08:01 -0700 Subject: [PATCH 30/52] Fixed grid_SO3_uniform when n==1 --- reconstructSPI/iterative_refinement/expectation_maximization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index c4f5fdd..5fa8ce6 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -393,6 +393,8 @@ def grid_SO3_uniform(n_rotations): """ geom = special_orthogonal.SpecialOrthogonal(3, "matrix") rots = geom.random_uniform(n_rotations) + if n_rotations == 1: + rots = np.array((rots,)) negatives = np.tile(np.random.randint(2, size=n_rotations) * 2 - 1, (3, 3, 1)).T rots[:] *= negatives return rots From 5fbf7a8d04bce7810a20c1d931488c64a7fe390f Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 12:10:58 -0700 Subject: [PATCH 31/52] Fixed antipattern --- .../iterative_refinement/expectation_maximization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index e5f63da..188241c 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -424,8 +424,8 @@ def generate_cartesian_grid(n_pix, d): xy_plane = np.zeros((3, n_pix**2)) - for d in range(2): - xy_plane[d, :] = grid[d].flatten() + for di in range(2): + xy_plane[di, :] = grid[di].flatten() return xy_plane if d == 3: From fd32a445499cf3848ac533fd8a9633b70cd48d3f Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 12:19:42 -0700 Subject: [PATCH 32/52] Reverted test change --- tests/test_expectation_maximization.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 626571f..cd3c124 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -264,10 +264,14 @@ def test_insert_slice(test_ir, n_pix): map_plane_ones = np.zeros((n_pix, n_pix, n_pix)) map_plane_ones[n_pix // 2] = np.ones((n_pix, n_pix)) - rot_mat = test_ir.grid_SO3_uniform(1)[0] + rot_90deg_about_y = np.array( + [ + [[0, 0, 1], [0, 1, 0], [-1, 0, 0]], + ] + ) slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones, xy_plane, rot_mat + map_plane_ones, xy_plane, rot_90deg_about_y ) xyz_voxels = test_ir.generate_cartesian_grid(n_pix, 3) From 3e2877336fc5600e2d43a3c079663351cabc1914 Mon Sep 17 00:00:00 2001 From: Tyler Heim Date: Wed, 6 Apr 2022 13:15:30 -0700 Subject: [PATCH 33/52] Reverted change to cube coordinate formats --- .../iterative_refinement/expectation_maximization.py | 2 +- tests/test_expectation_maximization.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 188241c..27bdaa3 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -435,7 +435,7 @@ def generate_cartesian_grid(n_pix, d): for di in range(3): xyz[di] = grid[di].flatten() - xyz[[0, 1, 2]] = xyz[[2, 0, 1]] + xyz[[0, 1]] = xyz[[1, 0]] return xyz raise ValueError(f"Dimension {d} received was not 2 or 3.") diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index cd3c124..1a1c55e 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -94,9 +94,9 @@ def test_generate_cartesian_grid(test_ir, n_pix): n_pix_2 = 2 cube_2 = np.array( [ - [-1, 0, -1, 0, -1, 0, -1, 0], - [-1, -1, 0, 0, -1, -1, 0, 0], [-1, -1, -1, -1, 0, 0, 0, 0], + [-1, -1, 0, 0, -1, -1, 0, 0], + [-1, 0, -1, 0, -1, 0, -1, 0], ] ) From ac12ecc947b8b755baa6aefd4a65a038085a3de4 Mon Sep 17 00:00:00 2001 From: Tyler Heim Date: Wed, 6 Apr 2022 13:25:36 -0700 Subject: [PATCH 34/52] Testing edge cases --- tests/test_expectation_maximization.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 1a1c55e..ab94e31 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -74,6 +74,9 @@ def test_grid_SO3_uniform(test_ir, n_particles): rots = test_ir.grid_SO3_uniform(n_particles) assert rots.shape == (n_particles, 3, 3) + rot = test_ir.grid_SO3_uniform(1) + assert rot.shape == (1, 3, 3) + def test_generate_cartesian_grid(test_ir, n_pix): """Test generation of xy plane and xyz cube.""" @@ -105,6 +108,12 @@ def test_generate_cartesian_grid(test_ir, n_pix): assert np.isclose(xyz_cube.max(), n_pix_2 // 2 - 1) assert np.isclose(xyz_cube.min(), -n_pix_2 // 2) + exceptionThrown = False + try: + test_ir.generate_cartesian_grid(n_pix, 4) + except ValueError: + exceptionThrown = True + assert exceptionThrown def test_generate_slices(test_ir, n_particles, n_pix): """Test generation of slices. From ae89639f43e6a36ca2220a204644ddcd96ebbf55 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 20:25:53 +0000 Subject: [PATCH 35/52] Format code with black --- tests/test_expectation_maximization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index ab94e31..2792194 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -115,6 +115,7 @@ def test_generate_cartesian_grid(test_ir, n_pix): exceptionThrown = True assert exceptionThrown + def test_generate_slices(test_ir, n_particles, n_pix): """Test generation of slices. From 4d80ae9a8a156737f3acc857381dd9739ade156a Mon Sep 17 00:00:00 2001 From: Tyler Heim Date: Wed, 6 Apr 2022 14:42:39 -0700 Subject: [PATCH 36/52] Added explicit method to griddata calls. Added vectorized insert_slice in class, with test --- .../expectation_maximization.py | 50 +++++++++++++------ tests/test_expectation_maximization.py | 15 ++++++ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 27bdaa3..d8e6892 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -51,6 +51,13 @@ def __init__(self, map_3d_init, particles, ctf_info, max_itr=7): self.particles = particles self.ctf_info = ctf_info self.max_itr = max_itr + self.insert_slice_vectorized = np.vectorize( + IterativeRefinement.insert_slice, + excluded=[ + "xyz", + ], + signature="(n,n),(3,m),(3,k)->(n,n,n),(n,n,n)", + ) def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): """Perform iterative refinement. @@ -133,14 +140,6 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): xyz_voxels = IterativeRefinement.generate_cartesian_grid(n_pix, 3) - insert_slice_v = np.vectorize( - IterativeRefinement.insert_slice, - excluded=[ - "xyz", - ], - signature="(n,n),(3,m),(3,k)->(n,n,n),(n,n,n)", - ) - for _ in range(self.max_itr): half_map_3d_f_1 = ( @@ -222,10 +221,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_1)): xyz = xyz_rotated[one_slice_idx] - inserted_slice_3d_r, count_3d_r = insert_slice_v( + inserted_slice_3d_r, count_3d_r = self.insert_slice_v( particle_f_deconv_1.real, xyz, xyz_voxels ) - inserted_slice_3d_i, count_3d_i = insert_slice_v( + inserted_slice_3d_i, count_3d_i = self.insert_slice_v( particle_f_deconv_1.imag, xyz, xyz_voxels ) map_3d_f_updated_1 += np.sum( @@ -235,10 +234,10 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): for one_slice_idx in range(len(bayes_factors_2)): xyz = xyz_rotated[one_slice_idx] - inserted_slice_3d_r, count_3d_r = insert_slice_v( + inserted_slice_3d_r, count_3d_r = self.insert_slice_v( particle_f_deconv_2.real, xyz, xyz_voxels ) - inserted_slice_3d_i, count_3d_i = insert_slice_v( + inserted_slice_3d_i, count_3d_i = self.insert_slice_v( particle_f_deconv_2.imag, xyz, xyz_voxels ) map_3d_f_updated_2 += np.sum( @@ -650,15 +649,38 @@ def insert_slice(slice_real, xy_rotated, xyz): slice_values = np.tile(slice_real.reshape((n_pix**2,)), (3,)) inserted_slice_3d = griddata( - xy_rotated.T, slice_values, xyz.T, fill_value=0 + xy_rotated.T, slice_values, xyz.T, fill_value=0, method='linear' ).reshape((n_pix, n_pix, n_pix)) count_3d = griddata( - xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value=0 + xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value=0, method='linear' ).reshape((n_pix, n_pix, n_pix)) return inserted_slice_3d, count_3d + def insert_slice_v(self, slices_real, xy_rots, xyz): + """Vectorized version of insert_slice. + + Parameters + ---------- + slices_real : float64 arr + Shape (n_slices, n_pix, n_pix) the slices of interest. + xy_rots : arr + Shape (n_slices, 3, 3*n_pix**2) nonzero-depth "planes" of rotated slice coords. + xyz : arr + Shape (3, n_pix**3) voxels of 3D map. + + Returns + ------- + inserted_slices_3d : float64 arr + Rotated slices in 3D voxel arrays. + Shape (n_slices, n_pix, n_pix, n_pix) + counts_3d : arr + Voxel array to count slice presence. + Shape (n_slices, n_pix, n_pix, n_pix) + """ + return self.insert_slice_vectorized(slices_real, xy_rots, xyz) + @staticmethod def compute_fsc(map_3d_f_1, map_3d_f_2): """Compute the Fourier shell correlation. diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 2792194..0b471b3 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -300,6 +300,21 @@ def test_insert_slice(test_ir, n_pix): ) +def test_insert_slice_v(test_ir, n_pix): + """Test whether vectorized insert_slice produces the right shapes""" + n_slices = 5 + xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) + z_tol = np.array([[0, 0, 0.05],]).T + xy_plane_tol = np.concatenate((xy_plane + z_tol, xy_plane, xy_plane - z_tol), axis=0) + test_slices = np.ones((n_slices, n_pix, n_pix)) + xy_planes_tol = np.tile(np.expand_dims(xy_plane_tol, axis=0), (n_slices,)) + xyz = test_ir.generate_cartesian_grid(n_pix, 3) + + inserts, counts = test_ir.insert_slice_v(xy_planes_tol, test_slices, xyz) + assert inserts.shape == (n_slices, n_pix, n_pix, n_pix) + assert counts.shape == (n_slices, n_pix, n_pix, n_pix) + + def test_compute_fsc(test_ir, n_pix): """Test computation of FSC.""" map_1 = np.ones((n_pix, n_pix, n_pix)) From b9565892d12b70a85491c60911d3d2e7f6f80757 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 21:42:53 +0000 Subject: [PATCH 37/52] Format code with black --- .../iterative_refinement/expectation_maximization.py | 8 ++++++-- tests/test_expectation_maximization.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index d8e6892..ee6846b 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -649,11 +649,15 @@ def insert_slice(slice_real, xy_rotated, xyz): slice_values = np.tile(slice_real.reshape((n_pix**2,)), (3,)) inserted_slice_3d = griddata( - xy_rotated.T, slice_values, xyz.T, fill_value=0, method='linear' + xy_rotated.T, slice_values, xyz.T, fill_value=0, method="linear" ).reshape((n_pix, n_pix, n_pix)) count_3d = griddata( - xy_rotated.T, np.ones_like(slice_values), xyz.T, fill_value=0, method='linear' + xy_rotated.T, + np.ones_like(slice_values), + xyz.T, + fill_value=0, + method="linear", ).reshape((n_pix, n_pix, n_pix)) return inserted_slice_3d, count_3d diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 0b471b3..fd62372 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -304,8 +304,14 @@ def test_insert_slice_v(test_ir, n_pix): """Test whether vectorized insert_slice produces the right shapes""" n_slices = 5 xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) - z_tol = np.array([[0, 0, 0.05],]).T - xy_plane_tol = np.concatenate((xy_plane + z_tol, xy_plane, xy_plane - z_tol), axis=0) + z_tol = np.array( + [ + [0, 0, 0.05], + ] + ).T + xy_plane_tol = np.concatenate( + (xy_plane + z_tol, xy_plane, xy_plane - z_tol), axis=0 + ) test_slices = np.ones((n_slices, n_pix, n_pix)) xy_planes_tol = np.tile(np.expand_dims(xy_plane_tol, axis=0), (n_slices,)) xyz = test_ir.generate_cartesian_grid(n_pix, 3) From 6049c0eadb856c23447a8d3d8d52b58b9f2d2d9d Mon Sep 17 00:00:00 2001 From: Tyler Heim Date: Wed, 6 Apr 2022 15:09:01 -0700 Subject: [PATCH 38/52] Line length --- .../iterative_refinement/expectation_maximization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index ee6846b..fb8a5c5 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -670,7 +670,8 @@ def insert_slice_v(self, slices_real, xy_rots, xyz): slices_real : float64 arr Shape (n_slices, n_pix, n_pix) the slices of interest. xy_rots : arr - Shape (n_slices, 3, 3*n_pix**2) nonzero-depth "planes" of rotated slice coords. + Shape (n_slices, 3, 3*n_pix**2) nonzero-depth "planes" of rotated + slice coords. xyz : arr Shape (3, n_pix**3) voxels of 3D map. From 0af2f4d2c7e4c31bb7e0be596fb36cdc5627616a Mon Sep 17 00:00:00 2001 From: Tyler Heim Date: Wed, 6 Apr 2022 15:28:31 -0700 Subject: [PATCH 39/52] fixed docstring --- tests/test_expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index fd62372..e9b151d 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -301,7 +301,7 @@ def test_insert_slice(test_ir, n_pix): def test_insert_slice_v(test_ir, n_pix): - """Test whether vectorized insert_slice produces the right shapes""" + """Test whether vectorized insert_slice produces the right shapes.""" n_slices = 5 xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) z_tol = np.array( From d89fcc3ae1eb482fef429a6fc40ee6093d778e64 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Wed, 6 Apr 2022 23:14:38 -0700 Subject: [PATCH 40/52] Fixed test bug --- tests/test_expectation_maximization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index e9b151d..78db3f6 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -310,13 +310,13 @@ def test_insert_slice_v(test_ir, n_pix): ] ).T xy_plane_tol = np.concatenate( - (xy_plane + z_tol, xy_plane, xy_plane - z_tol), axis=0 + (xy_plane + z_tol, xy_plane, xy_plane - z_tol), axis=1 ) test_slices = np.ones((n_slices, n_pix, n_pix)) - xy_planes_tol = np.tile(np.expand_dims(xy_plane_tol, axis=0), (n_slices,)) + xy_planes_tol = np.tile(np.expand_dims(xy_plane_tol, axis=0), (n_slices, 1, 1)) xyz = test_ir.generate_cartesian_grid(n_pix, 3) - inserts, counts = test_ir.insert_slice_v(xy_planes_tol, test_slices, xyz) + inserts, counts = test_ir.insert_slice_v(test_slices, xy_planes_tol, xyz) assert inserts.shape == (n_slices, n_pix, n_pix, n_pix) assert counts.shape == (n_slices, n_pix, n_pix, n_pix) From 2fa5d271de1e5bad19aa306cdddcb1cd6fcbbf38 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 12:32:45 -0700 Subject: [PATCH 41/52] Modularized out xy plane formatting from generate_slices --- .../expectation_maximization.py | 104 ++++++++++-------- tests/test_expectation_maximization.py | 8 +- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index fb8a5c5..7728ed2 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -160,13 +160,15 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): rots = IterativeRefinement.grid_SO3_uniform(n_rotations) xy0_plane = IterativeRefinement.generate_cartesian_grid(n_pix, 2) + xyz_rotated_padded = IterativeRefinement.pad_and_rotate_xy_planes(xy0_plane, rots) + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] - slices_1, xyz_rotated = IterativeRefinement.generate_slices( - half_map_3d_f_1, xy0_plane, rots + slices_1 = IterativeRefinement.generate_slices( + half_map_3d_f_1, xyz_rotated ) - slices_2, xyz_rotated = IterativeRefinement.generate_slices( - half_map_3d_f_2, xy0_plane, rots + slices_2 = IterativeRefinement.generate_slices( + half_map_3d_f_2, xyz_rotated ) map_3d_f_updated_1 = np.zeros_like(half_map_3d_f_1) @@ -220,12 +222,12 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): ) for one_slice_idx in range(len(bayes_factors_1)): - xyz = xyz_rotated[one_slice_idx] + xyz_planes = xyz_rotated_padded[one_slice_idx] inserted_slice_3d_r, count_3d_r = self.insert_slice_v( - particle_f_deconv_1.real, xyz, xyz_voxels + particle_f_deconv_1.real, xyz_planes, xyz_voxels ) inserted_slice_3d_i, count_3d_i = self.insert_slice_v( - particle_f_deconv_1.imag, xyz, xyz_voxels + particle_f_deconv_1.imag, xyz_planes, xyz_voxels ) map_3d_f_updated_1 += np.sum( inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 @@ -233,12 +235,12 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): counts_3d_updated_1 += np.sum(count_3d_r + count_3d_i, axis=0) for one_slice_idx in range(len(bayes_factors_2)): - xyz = xyz_rotated[one_slice_idx] + xyz_planes = xyz_rotated_padded[one_slice_idx] inserted_slice_3d_r, count_3d_r = self.insert_slice_v( - particle_f_deconv_2.real, xyz, xyz_voxels + particle_f_deconv_2.real, xyz_planes, xyz_voxels ) inserted_slice_3d_i, count_3d_i = self.insert_slice_v( - particle_f_deconv_2.imag, xyz, xyz_voxels + particle_f_deconv_2.imag, xyz_planes, xyz_voxels ) map_3d_f_updated_2 += np.sum( inserted_slice_3d_r + 1j * inserted_slice_3d_i, axis=0 @@ -440,8 +442,8 @@ def generate_cartesian_grid(n_pix, d): raise ValueError(f"Dimension {d} received was not 2 or 3.") @staticmethod - def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): - """Generate slice coordinates by rotating xy plane. + def generate_slices(map_3d_f, xyz_rotated): + """Generate slice coordinates via rotated xy plane. Interpolate values from map_3d_f onto 3D coordinates. @@ -454,19 +456,9 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): 0,0,0 pixel at map_3d_f[n/2,n/2,n/2] n_pix/2-1,n_pix/2-1,n_pix/2-1 pixel at the final corner, i.e. map_3d_f[n_pix-1,n_pix-1,n_pix-1] - xy_plane : arr - Array describing xy plane in space. - Shape (3, n_pix**2) - Convention x,y,z, i.e. - xy_plane[0] is x coordinate - xy_plane[1] is y coordinate - xy_plane[2] is z coordinate, which is all zero - rots : arr - Array describing rotations. - Shape (n_rotations, n_pix**2, 3) - z_offset : float - Symmetrical z-depth given to the xy_plane before rotating. - 0 < z_offset < 1 + xyz_rotated : arr + Rotated xy planes. + Shape (n_rotations, 3, n_pix**2) Returns ------- @@ -474,9 +466,6 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): Slice of map_3d_f. Corresponds to Fourier transform of projection of rotated map_3d_f. Shape (n_rotations, n_pix, n_pix) - xyz_rotated : arr - Rotated xy planes, with 3D depth added according to z_offset. - Shape (n_rotations, 3, 3 * n_pix**2) Notes @@ -505,32 +494,61 @@ def generate_slices(map_3d_f, xy_plane, rots, z_offset=0.05): As far as the presence of noise in the edge pixels, masking that crops close enough to the centre will keeping a safe distance from the edge. """ - n_rotations = len(rots) + n_rotations = len(xyz_rotated) n_pix = len(map_3d_f) slices = np.empty((n_rotations, n_pix, n_pix), dtype=float) - xyz_rotated = np.empty((n_rotations, 3, 3 * n_pix**2)) - offset = np.array( - [ - [0, 0, z_offset], - ] - ).T - xy_plane = np.concatenate( - (xy_plane + offset, xy_plane, xy_plane - offset), axis=1 - ) for i in range(n_rotations): - xyz_rotated[i] = rots[i] @ xy_plane - slices[i] = map_coordinates( map_3d_f.real, - xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + xyz_rotated[i] + n_pix // 2, ).reshape((n_pix, n_pix)) + 1j * map_coordinates( map_3d_f.imag, - xyz_rotated[i, :, n_pix**2 : 2 * n_pix**2] + n_pix // 2, + xyz_rotated[i] + n_pix // 2, ).reshape( (n_pix, n_pix) ) + return slices - return slices, xyz_rotated + @staticmethod + def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): + """Rotate xy planes after padding them in z symmetrically by z_offset. + + Parameters + ---------- + xy_plane : arr + Array describing xy plane in space. + Shape (3, n_pix**2) + Convention x,y,z, i.e. + xy_plane[0] is x coordinate + xy_plane[1] is y coordinate + xy_plane[2] is z coordinate, which is all zero + rots : arr + Array describing rotations. + Shape (n_rotations, n_pix**2, 3) + z_offset : float + Symmetrical z-depth given to the xy_plane before rotating. + 0 < z_offset < 1 + + Returns + ------- + xyz_rotated : arr + Rotated xy planes, padded on either side by z_offset. + Shape (n_rotations, 3, 3 * n_pix**2) + """ + n_rotations = len(rots) + n_pix = xy_plane.shape[0] + offset = np.array( + [ + [0, 0, z_offset], + ] + ).T + xy_plane_padded = np.concatenate( + (xy_plane + offset, xy_plane, xy_plane - offset), axis=1 + ) + xyz_rotated_padded = np.empty((n_rotations, 3, 3 * n_pix**2)) + for i in range(n_rotations): + xyz_rotated_padded[i] = rots[i] @ xy_plane_padded + return xyz_rotated_padded @staticmethod def apply_ctf_to_slice(particle_slice, ctf): diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 78db3f6..bc7b7ce 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -280,13 +280,15 @@ def test_insert_slice(test_ir, n_pix): ] ) - slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones, xy_plane, rot_90deg_about_y + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_90deg_about_y) + + slices = test_ir.generate_slices( + map_plane_ones, xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] ) xyz_voxels = test_ir.generate_cartesian_grid(n_pix, 3) - inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_planes[0], xyz_voxels) + inserted, count = test_ir.insert_slice(slices[0], xyz_rotated_padded[0], xyz_voxels) omit_idx_artefact = 1 From 64cfc128397855d90c1a0657d29e72834f933986 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:33:03 +0000 Subject: [PATCH 42/52] Format code with black --- .../expectation_maximization.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 7728ed2..64e1535 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -160,16 +160,14 @@ def iterative_refinement(self, wiener_small_number=0.01, count_norm_const=1): rots = IterativeRefinement.grid_SO3_uniform(n_rotations) xy0_plane = IterativeRefinement.generate_cartesian_grid(n_pix, 2) - xyz_rotated_padded = IterativeRefinement.pad_and_rotate_xy_planes(xy0_plane, rots) + xyz_rotated_padded = IterativeRefinement.pad_and_rotate_xy_planes( + xy0_plane, rots + ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] - slices_1 = IterativeRefinement.generate_slices( - half_map_3d_f_1, xyz_rotated - ) + slices_1 = IterativeRefinement.generate_slices(half_map_3d_f_1, xyz_rotated) - slices_2 = IterativeRefinement.generate_slices( - half_map_3d_f_2, xyz_rotated - ) + slices_2 = IterativeRefinement.generate_slices(half_map_3d_f_2, xyz_rotated) map_3d_f_updated_1 = np.zeros_like(half_map_3d_f_1) map_3d_f_updated_2 = np.zeros_like(half_map_3d_f_2) @@ -512,7 +510,7 @@ def generate_slices(map_3d_f, xyz_rotated): @staticmethod def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): """Rotate xy planes after padding them in z symmetrically by z_offset. - + Parameters ---------- xy_plane : arr @@ -528,7 +526,7 @@ def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): z_offset : float Symmetrical z-depth given to the xy_plane before rotating. 0 < z_offset < 1 - + Returns ------- xyz_rotated : arr From d881f491a0a6bbd4682d4e8727a4df308eedb2f9 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 12:43:27 -0700 Subject: [PATCH 43/52] Fixed generate_slice test --- tests/test_expectation_maximization.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index bc7b7ce..1845874 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -141,15 +141,18 @@ def test_generate_slices(test_ir, n_particles, n_pix): map_3d = np.ones((n_pix, n_pix, n_pix)) rots = test_ir.grid_SO3_uniform(n_particles) xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) - slices, xyz_rotated_planes = test_ir.generate_slices(map_3d, xy_plane, rots) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots) + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + slices = test_ir.generate_slices(map_3d, xyz_rotated) + assert slices.shape == (n_particles, n_pix, n_pix) - assert xyz_rotated_planes.shape == (n_particles, 3, 3 * n_pix**2) + assert xyz_rotated_padded.shape == (n_particles, 3, 3 * n_pix**2) map_3d_dc = np.zeros((n_pix, n_pix, n_pix)) rand_val = np.random.uniform(low=1, high=2) map_3d_dc[n_pix // 2, n_pix // 2, n_pix // 2] = rand_val expected_dc = rand_val * np.ones(len(slices)) - slices, xyz_rotated_planes = test_ir.generate_slices(map_3d_dc, xy_plane, rots) + slices = test_ir.generate_slices(map_3d_dc, xyz_rotated) projected_dc = slices[:, n_pix // 2, n_pix // 2] assert np.allclose(projected_dc, expected_dc) @@ -166,8 +169,11 @@ def test_generate_slices(test_ir, n_particles, n_pix): expected_slice_line_y = np.zeros_like(slices[0]) expected_slice_line_y[n_pix // 2] = 1 - slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones_xzplane, xy_plane, rot_90deg_about_y + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_90deg_about_y) + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + + slices = test_ir.generate_slices( + map_plane_ones_xzplane, xyz_rotated ) omit_idx_artefact = 1 assert np.allclose( @@ -183,8 +189,12 @@ def test_generate_slices(test_ir, n_particles, n_pix): map_plane_ones_xyplane = np.zeros((n_pix, n_pix, n_pix)) map_plane_ones_xyplane[:, :, n_pix // 2] = 1 expected_slice = np.ones((n_pix, n_pix)) - slices, xyz_rotated_planes = test_ir.generate_slices( - map_plane_ones_xyplane, xy_plane, rot_180deg_about_z + + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_180deg_about_z) + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + + slices = test_ir.generate_slices( + map_plane_ones_xyplane, xyz_rotated ) assert np.allclose( slices[0, omit_idx_artefact:, omit_idx_artefact:], From 27bcdf5c15205529a65d479da73ba0f0debc4772 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:43:57 +0000 Subject: [PATCH 44/52] Format code with black --- tests/test_expectation_maximization.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 1845874..e47ffbc 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -172,9 +172,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_90deg_about_y) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] - slices = test_ir.generate_slices( - map_plane_ones_xzplane, xyz_rotated - ) + slices = test_ir.generate_slices(map_plane_ones_xzplane, xyz_rotated) omit_idx_artefact = 1 assert np.allclose( slices[0, omit_idx_artefact:, omit_idx_artefact:], @@ -193,9 +191,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_180deg_about_z) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] - slices = test_ir.generate_slices( - map_plane_ones_xyplane, xyz_rotated - ) + slices = test_ir.generate_slices(map_plane_ones_xyplane, xyz_rotated) assert np.allclose( slices[0, omit_idx_artefact:, omit_idx_artefact:], expected_slice[omit_idx_artefact:, omit_idx_artefact:], From 010e4b143bba31365a58618824d0e529a8d4490b Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 19:48:24 +0000 Subject: [PATCH 45/52] Format code with black --- reconstructSPI/iterative_refinement/expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 8b8bd6b..09ee695 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -137,7 +137,7 @@ def iterative_refinement(self, count_norm_const=1): ) xyz_voxels = IterativeRefinement.generate_cartesian_grid(n_pix, 3) - + wiener_small_numbers_1 = IterativeRefinement.get_wiener_small_numbers( particles_f_1, ctfs_1 ) From 1d6d8aab6faf26d9fc24df2919f7f6b731c6fc6d Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:30:11 -0700 Subject: [PATCH 46/52] Added n_pix parameter to pad_and_rotate_xy_planes --- .../expectation_maximization.py | 5 +++-- tests/test_expectation_maximization.py | 20 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index 64e1535..0ebe6cb 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -508,7 +508,7 @@ def generate_slices(map_3d_f, xyz_rotated): return slices @staticmethod - def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): + def pad_and_rotate_xy_planes(xy_plane, rots, n_pix, z_offset=0.05): """Rotate xy planes after padding them in z symmetrically by z_offset. Parameters @@ -523,6 +523,8 @@ def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): rots : arr Array describing rotations. Shape (n_rotations, n_pix**2, 3) + n_pix : int + Number of pixels per axis. z_offset : float Symmetrical z-depth given to the xy_plane before rotating. 0 < z_offset < 1 @@ -534,7 +536,6 @@ def pad_and_rotate_xy_planes(xy_plane, rots, z_offset=0.05): Shape (n_rotations, 3, 3 * n_pix**2) """ n_rotations = len(rots) - n_pix = xy_plane.shape[0] offset = np.array( [ [0, 0, z_offset], diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 1845874..7a74fbd 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -141,7 +141,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): map_3d = np.ones((n_pix, n_pix, n_pix)) rots = test_ir.grid_SO3_uniform(n_particles) xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) - xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots, n_pix) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] slices = test_ir.generate_slices(map_3d, xyz_rotated) @@ -169,7 +169,11 @@ def test_generate_slices(test_ir, n_particles, n_pix): expected_slice_line_y = np.zeros_like(slices[0]) expected_slice_line_y[n_pix // 2] = 1 - xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_90deg_about_y) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( + xy_plane, + rot_90deg_about_y, + n_pix + ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] slices = test_ir.generate_slices( @@ -190,7 +194,11 @@ def test_generate_slices(test_ir, n_particles, n_pix): map_plane_ones_xyplane[:, :, n_pix // 2] = 1 expected_slice = np.ones((n_pix, n_pix)) - xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_180deg_about_z) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( + xy_plane, + rot_180deg_about_z, + n_pix + ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] slices = test_ir.generate_slices( @@ -290,7 +298,11 @@ def test_insert_slice(test_ir, n_pix): ] ) - xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rot_90deg_about_y) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( + xy_plane, + rot_90deg_about_y, + n_pix + ) slices = test_ir.generate_slices( map_plane_ones, xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] From 0ce7552146ecf13d8253ed71a6bb536522bc6ad3 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 20:31:49 +0000 Subject: [PATCH 47/52] Format code with black --- tests/test_expectation_maximization.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 1a8828d..9da2256 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -172,9 +172,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): expected_slice_line_y[n_pix // 2] = 1 xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( - xy_plane, - rot_90deg_about_y, - n_pix + xy_plane, rot_90deg_about_y, n_pix ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] @@ -195,9 +193,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): expected_slice = np.ones((n_pix, n_pix)) xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( - xy_plane, - rot_180deg_about_z, - n_pix + xy_plane, rot_180deg_about_z, n_pix ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] @@ -297,9 +293,7 @@ def test_insert_slice(test_ir, n_pix): ) xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( - xy_plane, - rot_90deg_about_y, - n_pix + xy_plane, rot_90deg_about_y, n_pix ) slices = test_ir.generate_slices( From 032d34fa4d933ea53777dc596738ce5c54747555 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:36:44 -0700 Subject: [PATCH 48/52] Fixed bugs --- reconstructSPI/iterative_refinement/expectation_maximization.py | 2 +- tests/test_expectation_maximization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reconstructSPI/iterative_refinement/expectation_maximization.py b/reconstructSPI/iterative_refinement/expectation_maximization.py index cc7139c..514033e 100644 --- a/reconstructSPI/iterative_refinement/expectation_maximization.py +++ b/reconstructSPI/iterative_refinement/expectation_maximization.py @@ -166,7 +166,7 @@ def iterative_refinement(self, count_norm_const=1): rots = IterativeRefinement.grid_SO3_uniform(n_rotations) xy0_plane = IterativeRefinement.generate_cartesian_grid(n_pix, 2) xyz_rotated_padded = IterativeRefinement.pad_and_rotate_xy_planes( - xy0_plane, rots + xy0_plane, rots, n_pix ) xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 1a8828d..ccbbef5 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -144,7 +144,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): rots = test_ir.grid_SO3_uniform(n_particles) xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots, n_pix) - xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] slices = test_ir.generate_slices(map_3d, xyz_rotated) assert slices.shape == (n_particles, n_pix, n_pix) From 20eebd40fefd1cf827c9c88a4c8872972093fb48 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:40:39 -0700 Subject: [PATCH 49/52] Bug fixing --- tests/test_expectation_maximization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 2585e29..f943c0a 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -174,7 +174,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( xy_plane, rot_90deg_about_y, n_pix ) - xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] slices = test_ir.generate_slices(map_plane_ones_xzplane, xyz_rotated) omit_idx_artefact = 1 @@ -195,7 +195,7 @@ def test_generate_slices(test_ir, n_particles, n_pix): xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes( xy_plane, rot_180deg_about_z, n_pix ) - xyz_rotated = xyz_rotated_padded[:, :, n_pix**2, 2 * n_pix**2] + xyz_rotated = xyz_rotated_padded[:, :, n_pix**2 : 2 * n_pix**2] slices = test_ir.generate_slices(map_plane_ones_xyplane, xyz_rotated) assert np.allclose( From 99fcb791a7aab7757b70e03eb3c9d9f9baff9311 Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:47:04 -0700 Subject: [PATCH 50/52] Added shape test for pad_and_rotate_xy_plane --- tests/test_expectation_maximization.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index f943c0a..5c3b8cd 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -118,6 +118,15 @@ def test_generate_cartesian_grid(test_ir, n_pix): assert exceptionThrown +def test_pad_and_rotate_xy_plane(test_ir, n_pix, n_particles): + """Test shape after padding and rotating xy plane.""" + n_rotations = n_particles + xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) + rots = test_ir.grid_SO3_uniform(n_rotations) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_plane(xy_plane, rots) + assert xyz_rotated_padded.shape == (n_rotations, 3, 3 * n_pix**2) + + def test_generate_slices(test_ir, n_particles, n_pix): """Test generation of slices. From b0d930cc33cb128f8bbe803399cfdbd179a5feaa Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:47:53 -0700 Subject: [PATCH 51/52] Fixed name --- tests/test_expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 5c3b8cd..7ab60dd 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -123,7 +123,7 @@ def test_pad_and_rotate_xy_plane(test_ir, n_pix, n_particles): n_rotations = n_particles xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) rots = test_ir.grid_SO3_uniform(n_rotations) - xyz_rotated_padded = test_ir.pad_and_rotate_xy_plane(xy_plane, rots) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots) assert xyz_rotated_padded.shape == (n_rotations, 3, 3 * n_pix**2) From 288dc7155592064dc5d16aaf64bc89d55914dc0f Mon Sep 17 00:00:00 2001 From: thisTyler Date: Thu, 7 Apr 2022 13:55:08 -0700 Subject: [PATCH 52/52] Added missing parameter --- tests/test_expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expectation_maximization.py b/tests/test_expectation_maximization.py index 7ab60dd..deb40b0 100644 --- a/tests/test_expectation_maximization.py +++ b/tests/test_expectation_maximization.py @@ -123,7 +123,7 @@ def test_pad_and_rotate_xy_plane(test_ir, n_pix, n_particles): n_rotations = n_particles xy_plane = test_ir.generate_cartesian_grid(n_pix, 2) rots = test_ir.grid_SO3_uniform(n_rotations) - xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots) + xyz_rotated_padded = test_ir.pad_and_rotate_xy_planes(xy_plane, rots, n_pix) assert xyz_rotated_padded.shape == (n_rotations, 3, 3 * n_pix**2)