From 4a20aebd91c9a4dae54a6a5f28585686af76d3d0 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 23 Apr 2024 14:25:46 -0700 Subject: [PATCH 01/72] Phase reconstruction is invariant to voxel-size (#164) * fix bug finding focus in stack with only one slice * refactor for clarify * formatting * print -> warnings.warn * test single-slice case * fix test bugs * z-scale-invariant test object * no rescaling on output * forward simulation takes a "brightness" - simulating real microscope * fix example script * add background parameter for fluorescence forward model * test voxel-size invariance * rename I_norm -> direct_intensity * refactor to clarify discretization factor * remove comment * fix fluorescence example bug * improved docsring --------- Co-authored-by: Ivan Ivanov --- .../models/isotropic_fluorescent_thick_3d.py | 4 +- examples/models/phase_thick_3d.py | 5 +- tests/models/test_phase_thick_3d.py | 81 ++++++++++++++++++- .../models/isotropic_fluorescent_thick_3d.py | 21 ++++- waveorder/models/phase_thick_3d.py | 35 ++++---- waveorder/optics.py | 14 ++-- 6 files changed, 126 insertions(+), 34 deletions(-) diff --git a/examples/models/isotropic_fluorescent_thick_3d.py b/examples/models/isotropic_fluorescent_thick_3d.py index 21a6816..0184079 100644 --- a/examples/models/isotropic_fluorescent_thick_3d.py +++ b/examples/models/isotropic_fluorescent_thick_3d.py @@ -6,13 +6,13 @@ # Parameters # all lengths must use consistent units e.g. um simulation_arguments = { - "zyx_shape": (100, 256, 256), + "zyx_shape": (200, 256, 256), "yx_pixel_size": 6.5 / 63, "z_pixel_size": 0.25, } phantom_arguments = {"sphere_radius": 5} transfer_function_arguments = { - "wavelength_illumination": 0.532, + "wavelength_emission": 0.532, "z_padding": 0, "index_of_refraction_media": 1.3, "numerical_aperture_detection": 1.2, diff --git a/examples/models/phase_thick_3d.py b/examples/models/phase_thick_3d.py index aa9bd4a..54d7e43 100644 --- a/examples/models/phase_thick_3d.py +++ b/examples/models/phase_thick_3d.py @@ -14,12 +14,12 @@ "zyx_shape": (100, 256, 256), "yx_pixel_size": 6.5 / 63, "z_pixel_size": 0.25, - "wavelength_illumination": 0.532, "index_of_refraction_media": 1.3, } phantom_arguments = {"index_of_refraction_sample": 1.50, "sphere_radius": 5} transfer_function_arguments = { "z_padding": 0, + "wavelength_illumination": 0.532, "numerical_aperture_illumination": 0.9, "numerical_aperture_detection": 1.2, } @@ -61,6 +61,7 @@ zyx_phase, real_potential_transfer_function, transfer_function_arguments["z_padding"], + brightness=1e3, ) # Reconstruct @@ -69,8 +70,6 @@ real_potential_transfer_function, imag_potential_transfer_function, transfer_function_arguments["z_padding"], - simulation_arguments["z_pixel_size"], - simulation_arguments["wavelength_illumination"], ) # Display diff --git a/tests/models/test_phase_thick_3d.py b/tests/models/test_phase_thick_3d.py index 224c96c..d60c7fa 100644 --- a/tests/models/test_phase_thick_3d.py +++ b/tests/models/test_phase_thick_3d.py @@ -1,5 +1,5 @@ import pytest - +import numpy as np from waveorder.models import phase_thick_3d @@ -20,3 +20,82 @@ def test_calculate_transfer_function(invert_phase_contrast): assert H_re.shape == (20 + 2 * z_padding, 100, 101) assert H_im.shape == (20 + 2 * z_padding, 100, 101) + + +# Helper function for testing reconstruction invariances +def simulate_phase_recon( + z_pixel_size_um=0.1, + yx_pixel_size_um=6.5 / 63, +): + + z_fov_um = 50 + yx_fov_um = 50 + + n_z = np.int32(z_fov_um / z_pixel_size_um) + n_yx = np.int32(yx_fov_um / yx_pixel_size_um) + + # Parameters + # all lengths must use consistent units e.g. um + simulation_arguments = { + "zyx_shape": (n_z, n_yx, n_yx), + "yx_pixel_size": yx_pixel_size_um, + "z_pixel_size": z_pixel_size_um, + "index_of_refraction_media": 1.3, + } + phantom_arguments = { + "index_of_refraction_sample": 1.40, + "sphere_radius": 5, + } + transfer_function_arguments = { + "z_padding": 0, + "wavelength_illumination": 0.532, + "numerical_aperture_illumination": 0.9, + "numerical_aperture_detection": 1.3, + } + + # Create a phantom + zyx_phase = phase_thick_3d.generate_test_phantom( + **simulation_arguments, **phantom_arguments + ) + + # Calculate transfer function + ( + real_potential_transfer_function, + imag_potential_transfer_function, + ) = phase_thick_3d.calculate_transfer_function( + **simulation_arguments, **transfer_function_arguments + ) + + # Simulate + zyx_data = phase_thick_3d.apply_transfer_function( + zyx_phase, + real_potential_transfer_function, + transfer_function_arguments["z_padding"], + brightness=1000, + ) + + # Reconstruct + zyx_recon = phase_thick_3d.apply_inverse_transfer_function( + zyx_data, + real_potential_transfer_function, + imag_potential_transfer_function, + transfer_function_arguments["z_padding"], + regularization_strength=1e-3, + ) + + Z, Y, X = zyx_phase.shape + recon_center = zyx_recon[Z // 2, Y // 2, X // 2].numpy() + + return recon_center + + +def test_phase_invariance(): + recon = simulate_phase_recon() + + # test z pixel size invariance + recon1 = simulate_phase_recon(z_pixel_size_um=0.3) + assert np.abs((recon1 - recon) / recon) < 0.02 + + # test yx pixel size invariance + recon2 = simulate_phase_recon(yx_pixel_size_um=0.7 * 6.5 / 63) + assert np.abs((recon2 - recon) / recon) < 0.02 \ No newline at end of file diff --git a/waveorder/models/isotropic_fluorescent_thick_3d.py b/waveorder/models/isotropic_fluorescent_thick_3d.py index 5823485..b52e71b 100644 --- a/waveorder/models/isotropic_fluorescent_thick_3d.py +++ b/waveorder/models/isotropic_fluorescent_thick_3d.py @@ -81,7 +81,24 @@ def visualize_transfer_function(viewer, optical_transfer_function, zyx_scale): viewer.dims.order = (0, 1, 2) -def apply_transfer_function(zyx_object, optical_transfer_function, z_padding): +def apply_transfer_function( + zyx_object, optical_transfer_function, z_padding, background=10 +): + """Simulate imaging by applying a transfer function + + Parameters + ---------- + zyx_object : torch.Tensor + optical_transfer_function : torch.Tensor + z_padding : int + background : int, optional + constant background counts added to each voxel, by default 10 + + Returns + ------- + Simulated data : torch.Tensor + + """ if ( zyx_object.shape[0] + 2 * z_padding != optical_transfer_function.shape[0] @@ -99,7 +116,7 @@ def apply_transfer_function(zyx_object, optical_transfer_function, z_padding): zyx_data = zyx_obj_hat * optical_transfer_function data = torch.real(torch.fft.ifftn(zyx_data)) - data += 10 # Add a direct background + data += background # Add a direct background return data diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index 39555b5..ac29d35 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -12,7 +12,6 @@ def generate_test_phantom( zyx_shape, yx_pixel_size, z_pixel_size, - wavelength_illumination, index_of_refraction_media, index_of_refraction_sample, sphere_radius, @@ -24,12 +23,9 @@ def generate_test_phantom( radius=sphere_radius, blur_size=2 * yx_pixel_size, ) - zyx_phase = ( - sphere - * (index_of_refraction_sample - index_of_refraction_media) - * z_pixel_size - / wavelength_illumination - ) # phase in radians + zyx_phase = sphere * ( + index_of_refraction_sample - index_of_refraction_media + ) # refractive index increment return zyx_phase @@ -120,12 +116,19 @@ def visualize_transfer_function( def apply_transfer_function( - zyx_object, real_potential_transfer_function, z_padding + zyx_object, real_potential_transfer_function, z_padding, brightness ): # This simplified forward model only handles phase, so it resuses the fluorescence forward model # TODO: extend to absorption - return isotropic_fluorescent_thick_3d.apply_transfer_function( - zyx_object, real_potential_transfer_function, z_padding + return ( + isotropic_fluorescent_thick_3d.apply_transfer_function( + zyx_object, + real_potential_transfer_function, + z_padding, + background=0, + ) + * brightness + + brightness ) @@ -134,8 +137,6 @@ def apply_inverse_transfer_function( real_potential_transfer_function: Tensor, imaginary_potential_transfer_function: Tensor, z_padding: int, - z_pixel_size: float, # TODO: MOVE THIS PARAM TO OTF? (leaky param) - wavelength_illumination: float, # TOOD: MOVE THIS PARAM TO OTF? (leaky param) absorption_ratio: float = 0.0, reconstruction_algorithm: Literal["Tikhonov", "TV"] = "Tikhonov", regularization_strength: float = 1e-3, @@ -158,14 +159,6 @@ def apply_inverse_transfer_function( z_padding : int Padding for axial dimension. Use zero for defocus stacks that extend ~3 PSF widths beyond the sample. Pad by ~3 PSF widths otherwise. - z_pixel_size : float - spacing between axial samples in sample space - units must be consistent with wavelength_illumination - TODO: move this leaky parameter to calculate_transfer_function - wavelength_illumination : float, - illumination wavelength - units must be consistent with z_pixel_size - TODO: move this leaky parameter to calculate_transfer_function absorption_ratio : float, optional, Absorption-to-phase ratio in the sample. Use default 0 for purely phase objects. @@ -223,4 +216,4 @@ def apply_inverse_transfer_function( if z_padding != 0: f_real = f_real[z_padding:-z_padding] - return f_real * z_pixel_size / 4 / np.pi * wavelength_illumination + return f_real diff --git a/waveorder/optics.py b/waveorder/optics.py index 8033ab8..4151b9d 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -739,19 +739,23 @@ def compute_weak_object_transfer_function_3D( H1 = torch.fft.ifft2(torch.conj(SPHz_hat) * PG_hat, dim=(1, 2)) H1 = H1 * window[:, None, None] - H1 = torch.fft.fft(H1, dim=0) * z_pixel_size + H1 = torch.fft.fft(H1, dim=0) H2 = torch.fft.ifft2(SPHz_hat * torch.conj(PG_hat), dim=(1, 2)) H2 = H2 * window[:, None, None] - H2 = torch.fft.fft(H2, dim=0) * z_pixel_size + H2 = torch.fft.fft(H2, dim=0) - I_norm = torch.sum( + direct_intensity = torch.sum( illumination_pupil_support * detection_pupil * torch.conj(detection_pupil) ) - real_potential_transfer_function = (H1 + H2) / I_norm - imag_potential_transfer_function = 1j * (H1 - H2) / I_norm + real_potential_transfer_function = (H1 + H2) / direct_intensity + imag_potential_transfer_function = 1j * (H1 - H2) / direct_intensity + + # Discretization factor for unitless input and output + real_potential_transfer_function *= z_pixel_size + imag_potential_transfer_function *= z_pixel_size return real_potential_transfer_function, imag_potential_transfer_function From f662aaa6f1e40bc491eeaca59439dbe05cf5340d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Sun, 12 May 2024 19:25:00 -0700 Subject: [PATCH 02/72] poster scripting --- examples/models/anisotropic_thick_3d.py | 338 ++++++++++++++++++++++++ waveorder/optics.py | 135 ++++++++-- waveorder/util.py | 68 ++++- waveorder/waveorder_reconstructor.py | 11 +- 4 files changed, 523 insertions(+), 29 deletions(-) create mode 100644 examples/models/anisotropic_thick_3d.py diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py new file mode 100644 index 0000000..f73cbe3 --- /dev/null +++ b/examples/models/anisotropic_thick_3d.py @@ -0,0 +1,338 @@ +import torch +import napari +import numpy as np +from waveorder import optics, util +from waveorder.models import phase_thick_3d + +# Parameters +# all lengths must use consistent units e.g. um +margin = 50 +simulation_arguments = { + "zyx_shape": (129, 256, 256), + "yx_pixel_size": 6.5 / 65, + "z_pixel_size": 0.1, + "index_of_refraction_media": 1.25, +} +# phantom_arguments = {"index_of_refraction_sample": 1.50, "sphere_radius": 5} +transfer_function_arguments = { + "z_padding": 0, + "wavelength_illumination": 0.5, + "numerical_aperture_illumination": 0.75, # 75, + "numerical_aperture_detection": 1.0, +} +input_jones = torch.tensor([0.0 + 1.0j, 1.0 + 0j]) + +# # Create a phantom +# zyx_phase = phase_thick_3d.generate_test_phantom( +# **simulation_arguments, **phantom_arguments +# ) + +# Convert +zyx_shape = simulation_arguments["zyx_shape"] +yx_pixel_size = simulation_arguments["yx_pixel_size"] +z_pixel_size = simulation_arguments["z_pixel_size"] +index_of_refraction_media = simulation_arguments["index_of_refraction_media"] +z_padding = transfer_function_arguments["z_padding"] +wavelength_illumination = transfer_function_arguments[ + "wavelength_illumination" +] +numerical_aperture_illumination = transfer_function_arguments[ + "numerical_aperture_illumination" +] +numerical_aperture_detection = transfer_function_arguments[ + "numerical_aperture_detection" +] + +# Precalculations +z_total = zyx_shape[0] + 2 * z_padding +z_position_list = torch.fft.ifftshift( + (torch.arange(z_total) - z_total // 2) * z_pixel_size +) + +# Calculate frequencies +y_frequencies, x_frequencies = util.generate_frequencies( + zyx_shape[1:], yx_pixel_size +) +radial_frequencies = np.sqrt(x_frequencies**2 + y_frequencies**2) + +# 2D pupils +ill_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_illumination, + wavelength_illumination, +) +det_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_detection, + wavelength_illumination, +) +pupil = optics.generate_pupil( + radial_frequencies, + index_of_refraction_media, # largest possible NA + wavelength_illumination, +) + +# Defocus pupils +defocus_pupil = optics.generate_propagation_kernel( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, +) + +greens_functions_z = optics.generate_greens_function_z( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, +) + +# Calculate vector defocus pupils +S = optics.generate_vector_source_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + input_jones, + ill_pupil, + wavelength_illumination / index_of_refraction_media, +) + +# Simplified scalar pupil +sP = optics.generate_propagation_kernel( + radial_frequencies, + det_pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, +) + +P = optics.generate_vector_detection_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + det_pupil, + wavelength_illumination / index_of_refraction_media, +) + +G = optics.generate_defocus_greens_tensor( + x_frequencies, + y_frequencies, + greens_functions_z, + pupil, + lambda_in=wavelength_illumination / index_of_refraction_media, +) + +# window = torch.fft.ifftshift( +# torch.hann_window(z_position_list.shape[0], periodic=False) +# ) + +# ###### LATEST + +# # abs() and *(1j) are hacks to correct for tricky phase shifts +# P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) +# G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) +# S_3D = torch.fft.ifft(S, dim=-3) + +# # Normalize +# P_3D /= torch.amax(torch.abs(P_3D)) +# G_3D /= torch.amax(torch.abs(G_3D)) +# S_3D /= torch.amax(torch.abs(S_3D)) + +# # Main part +# PG_3D = torch.einsum("ijzyx,jpzyx->ipzyx", P_3D, G_3D) +# PS_3D = torch.einsum("jlzyx,lzyx,kzyx->jlzyx", P_3D, S_3D, torch.conj(S_3D)) + +# # PG_3D /= torch.amax(torch.abs(PG_3D)) +# # PS_3D /= torch.amax(torch.abs(PS_3D)) + +# pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) +# ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) + +# H1 = torch.fft.ifftn( +# torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), +# dim=(-3, -2, -1), +# ) + +# H2 = torch.fft.ifftn( +# torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), +# dim=(-3, -2, -1), +# ) + +# MAY 12 Simplified +P_3D = torch.abs(torch.fft.ifft(sP, dim=-3)).type(torch.complex64) +G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) +S_3D = torch.fft.ifft(S, dim=-3) + +# Normalize +P_3D /= torch.amax(torch.abs(P_3D)) +G_3D /= torch.amax(torch.abs(G_3D)) +S_3D /= torch.amax(torch.abs(S_3D)) + +# Main part +PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) +PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) + +PG_3D /= torch.amax(torch.abs(PG_3D)) +PS_3D /= torch.amax(torch.abs(PS_3D)) + +pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) +ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) + +H1 = torch.fft.ifftn( + torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), + dim=(-3, -2, -1), +) + +H2 = torch.fft.ifftn( + torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), + dim=(-3, -2, -1), +) + +H_re = H1[1:, 1:] + H2[1:, 1:] +# H_im = 1j * (H1 - H2) + +s = util.pauli() +Y = util.gellmann() + +H_re_stokes = torch.einsum("sik,ikpjzyx,lpj->slzyx", s, H_re, Y) + +print("H_re_stokes: (RE, IM, ABS)") +torch.set_printoptions(precision=1) +print(torch.log10(torch.sum(torch.real(H_re_stokes) ** 2, dim=(-3, -2, -1)))) +print(torch.log10(torch.sum(torch.imag(H_re_stokes) ** 2, dim=(-3, -2, -1)))) +print(torch.log10(torch.sum(torch.abs(H_re_stokes) ** 2, dim=(-3, -2, -1)))) + +# Display transfer function +v = napari.Viewer() + + +def view_transfer_function( + transfer_function, +): + shift_dims = (-3, -2, -1) + lim = 1e-3 + zyx_scale = np.array( + [ + zyx_shape[0] * z_pixel_size, + zyx_shape[1] * yx_pixel_size, + zyx_shape[2] * yx_pixel_size, + ] + ) + + v.add_image( + torch.fft.ifftshift(torch.real(transfer_function), dim=shift_dims) + .cpu() + .numpy(), + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 / zyx_scale, + ) + if transfer_function.dtype == torch.complex64: + v.add_image( + torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) + .cpu() + .numpy(), + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 / zyx_scale, + ) + + # v.dims.order = (2, 1, 0) + + +# view_transfer_function(H_re_stokes) +# view_transfer_function(G_3D) +# view_transfer_function(H_re) +# view_transfer_function(P_3D) + +# PLOT transfer function +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors + + +def plot_data(data, y_slices, filename): + fig, axs = plt.subplots(4, 9, figsize=(20, 10)) # Adjust the size as needed + + for i in range(data.shape[0]): # Stokes parameter + for j in range(data.shape[1]): # Object parameter + for k, y in enumerate(y_slices): # Y slices + z = data[i, j, :, y, :] + hue = np.angle(z) / (2 * np.pi) + 0.5 # Normalize and shift to make red at 0 + sat = np.abs(z) / np.amax(np.abs(z)) + hsv = np.stack((hue, sat, np.ones_like(sat)), axis=-1) + rgb = mcolors.hsv_to_rgb(hsv) + + ax = axs[i, j] + ax.imshow(rgb, aspect='auto') + ax.set_title('') # Remove titles + ax.set_xticks([]) # Remove x-axis ticks + ax.set_yticks([]) # Remove y-axis ticks + ax.spines['top'].set_visible(False) # Hide top spine + ax.spines['right'].set_visible(False) # Hide right spine + ax.spines['bottom'].set_visible(False) # Hide bottom spine + ax.spines['left'].set_visible(False) # Hide left spine + ax.set_xlabel('') # Remove x-axis labels + + plt.tight_layout() + plt.savefig(filename, format='pdf') + +# Adjust y_slices according to your index base (check if your array index starts at 0) +y_center = 128 # Assuming the middle index for Y dimension +y_slices = [y_center - 10, y_center, y_center + 10] +plot_data(torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), y_slices, './output.pdf') + +# Simulate +yx_star, yx_theta, _ = util.generate_star_target( + yx_shape=zyx_shape[1:], + blur_px=1, + margin=margin, +) +c00 = yx_star +c2_2 = -torch.sin(2 * yx_theta) * yx_star +c22 = torch.cos(2 * yx_theta) * yx_star + +# Put in in a center slices of a 3D object +center_slice_object = torch.stack((c00, c2_2, c22), dim=0) +object = torch.zeros((3,) + zyx_shape) +object[:, zyx_shape[0] // 2, ...] = center_slice_object + +# Simulate +object_spectrum = torch.fft.fftn(object, dim=(-3, -2, -1)) +data_spectrum = torch.einsum( + "slzyx,lzyx->szyx", H_re_stokes[:, (0, 4, 8), ...], object_spectrum +) +data = torch.fft.ifftn(data_spectrum, dim=(-3, -2, -1)) + +v.add_image(object.numpy()) +v.add_image(torch.real(data).numpy()) +v.add_image(torch.imag(data).numpy()) + +import pdb + +pdb.set_trace() + + +zyx_data = phase_thick_3d.apply_transfer_function( + zyx_phase, + real_potential_transfer_function, + transfer_function_arguments["z_padding"], + brightness=1e3, +) + +# Reconstruct +zyx_recon = phase_thick_3d.apply_inverse_transfer_function( + zyx_data, + real_potential_transfer_function, + imag_potential_transfer_function, + transfer_function_arguments["z_padding"], +) + +# Display +viewer.add_image(zyx_phase.numpy(), name="Phantom", scale=zyx_scale) +viewer.add_image(zyx_data.numpy(), name="Data", scale=zyx_scale) +viewer.add_image(zyx_recon.numpy(), name="Reconstruction", scale=zyx_scale) +input("Showing object, data, and recon. Press to quit...") + +# %% diff --git a/waveorder/optics.py b/waveorder/optics.py index 4151b9d..8df21b3 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -133,7 +133,7 @@ def generate_pupil(frr, NA, lamb_in): numerical aperture of the pupil function (normalized by the refractive index of the immersion media) lamb_in : float - wavelength of the light (inside the immersion media) + wavelength of the light in free space in units of length (inverse of frr's units) Returns @@ -225,6 +225,101 @@ def gen_sector_Pupil(fxx, fyy, NA, lamb_in, sector_angle, rotation_angle): return Pupil_sector +def rotation_matrix(nu_z, nu_y, nu_x, wavelength): + nu_perp_squared = nu_x**2 + nu_y**2 + nu_zz = wavelength * nu_z - 1 + + R_xx = (wavelength * nu_x**2 * nu_z + nu_y**2) / nu_perp_squared + R_yy = (wavelength * nu_y**2 * nu_z + nu_x**2) / nu_perp_squared + R_xy = nu_x * nu_y * nu_zz / nu_perp_squared + + row0 = torch.stack((-wavelength * nu_y, -wavelength * nu_x), dim=0) + row1 = torch.stack((R_yy, R_xy), dim=0) + row2 = torch.stack((R_xy, R_xx), dim=0) + + out = torch.stack((row0, row1, row2), dim=0) + + # KLUDGE to avoid fix nans + out[..., 0, 0] = torch.tensor([[0, 0], [1, 0], [0, 1]])[..., None] + + return out + + +def generate_vector_source_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + input_jones, + ill_pupil, + wavelength, +): + ill_pupil_3d = torch.einsum( + "zyx,yx->zyx", torch.fft.fft(defocus_pupil, dim=0), ill_pupil + ).abs() # make this real + + # Calculate zyx_frequency grid (inelegant) + z_frequencies = torch.fft.ifft(z_position_list) + freq_shape = z_frequencies.shape + x_frequencies.shape + z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) + y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) + x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) + + # Calculate rotation matrix + rotations = rotation_matrix( + z_broadcast, y_broadcast, x_broadcast, wavelength + ).type(torch.complex64) + + # Main calculation in the frequency domain + source_pupil = ( + torch.einsum( + "ijzyx,j,zyx->izyx", rotations, input_jones, ill_pupil_3d + ) # .abs() + # ** 2 + ) # abs here is critical...incoherent pupil + + # Convert back to defocus pupil + source_defocus_pupil = torch.fft.ifft(source_pupil, dim=-3) + + return source_defocus_pupil + + +def generate_vector_detection_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + det_defocus_pupil, + det_pupil, + wavelength, +): + # TODO: refactor redundancy with illumination pupil + det_pupil_3d = torch.einsum( + "zyx,yx->zyx", torch.fft.ifft(det_defocus_pupil, dim=0), det_pupil + ) + + # Calculate zyx_frequency grid (inelegant) + z_frequencies = torch.fft.ifft(z_position_list) + freq_shape = z_frequencies.shape + x_frequencies.shape + z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) + y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) + x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) + + # Calculate rotation matrix + rotations = rotation_matrix( + z_broadcast, y_broadcast, x_broadcast, wavelength + ).type(torch.complex64) + + # Main calculation in the frequency domain + vector_detection_pupil = torch.einsum( + "jizyx,zyx->ijzyx", rotations, det_pupil_3d + ) + + # Convert back to defocus pupil + detection_defocus_pupil = torch.fft.fft(vector_detection_pupil, dim=-3) + + return detection_defocus_pupil + + def Source_subsample(Source_cont, NAx_coord, NAy_coord, subsampled_NA=0.1): """ @@ -310,9 +405,10 @@ def generate_propagation_kernel( """ - oblique_factor = ( - (1 - wavelength**2 * radial_frequencies**2) * pupil_support - ) ** (1 / 2) / wavelength + oblique_factor = ((1 - wavelength**2 * radial_frequencies**2)) ** ( + 1 / 2 + ) / wavelength + oblique_factor = torch.nan_to_num(oblique_factor, nan=0.0) propagation_kernel = pupil_support[None, :, :] * torch.exp( 1j @@ -367,7 +463,7 @@ def generate_greens_function_z( 1j * 2 * np.pi - * torch.tensor(z_position_list)[:, None, None] + * torch.abs(torch.tensor(z_position_list)[:, None, None]) * oblique_factor[None, :, :] ) / (oblique_factor[None, :, :] + 1e-15) @@ -376,23 +472,25 @@ def generate_greens_function_z( return greens_function_z -def gen_dyadic_Greens_tensor_z(fxx, fyy, G_fun_z, Pupil_support, lambda_in): +def generate_defocus_greens_tensor( + fxx, fyy, G_fun_z, Pupil_support, lambda_in +): """ generate forward dyadic Green's function in u_x, u_y, z space Parameters ---------- - fxx : numpy.ndarray + fxx : tensor.Tensor x component of 2D spatial frequency array with the size of (Ny, Nx) - fyy : numpy.ndarray + fyy : tensor.Tensor y component of 2D spatial frequency array with the size of (Ny, Nx) - G_fun_z : numpy.ndarray - forward Green's function in u_x, u_y, z space with size of (Ny, Nx, Nz) + G_fun_z : tensor.Tensor + forward Green's function in u_x, u_y, z space with size of (Nz, Ny, Nx) - Pupil_support : numpy.ndarray + Pupil_support : tensor.Tensor the array that defines the support of the pupil function with the size of (Ny, Nx) lambda_in : float @@ -400,22 +498,21 @@ def gen_dyadic_Greens_tensor_z(fxx, fyy, G_fun_z, Pupil_support, lambda_in): Returns ------- - G_tensor_z : numpy.ndarray - forward dyadic Green's function in u_x, u_y, z space with the size of (3, 3, Ny, Nx, Nz) + G_tensor_z : tensor.Tensor + forward dyadic Green's function in u_x, u_y, z space with the size of (3, 3, Nz, Ny, Nx) """ - N, M = fxx.shape fr = (fxx**2 + fyy**2) ** (1 / 2) oblique_factor = ((1 - lambda_in**2 * fr**2) * Pupil_support) ** ( 1 / 2 ) / lambda_in - diff_filter = np.zeros((3,) + G_fun_z.shape, complex) - diff_filter[0] = (1j * 2 * np.pi * fxx * Pupil_support)[..., np.newaxis] - diff_filter[1] = (1j * 2 * np.pi * fyy * Pupil_support)[..., np.newaxis] - diff_filter[2] = (1j * 2 * np.pi * oblique_factor)[..., np.newaxis] + diff_filter = torch.zeros((3,) + G_fun_z.shape, dtype=torch.complex64) + diff_filter[0] = (1j * 2 * np.pi * oblique_factor)[None, ...] + diff_filter[1] = (1j * 2 * np.pi * fyy * Pupil_support)[None, ...] + diff_filter[2] = (1j * 2 * np.pi * fxx * Pupil_support)[None, ...] - G_tensor_z = np.zeros((3, 3) + G_fun_z.shape, complex) + G_tensor_z = torch.zeros((3, 3) + G_fun_z.shape, dtype=torch.complex64) for i in range(3): for j in range(3): diff --git a/waveorder/util.py b/waveorder/util.py index 613614e..c797863 100644 --- a/waveorder/util.py +++ b/waveorder/util.py @@ -331,12 +331,15 @@ def gen_coordinate(img_dim, ps): return (xx, yy, fxx, fyy) -def generate_radial_frequencies(img_dim, ps): +def generate_frequencies(img_dim, ps): fy = torch.fft.fftfreq(img_dim[0], ps) fx = torch.fft.fftfreq(img_dim[1], ps) - fyy, fxx = torch.meshgrid(fy, fx, indexing="ij") + return fyy, fxx + +def generate_radial_frequencies(img_dim, ps): + fyy, fxx = generate_frequencies(img_dim, ps) return torch.sqrt(fyy**2 + fxx**2) @@ -2239,3 +2242,64 @@ def orientation_3D_continuity_map( retardance_pr_avg /= np.max(retardance_pr_avg) return retardance_pr_avg + + +def pauli(): + # yx order + # trace-orthogonal normalization + # torch.einsum("kij,lji->kl", pauli(), pauli()) == torch.eye(4) + + # intensity, x-y, +45-(-45), LCP-RCP + # yx + # yx + a = 2**-0.5 + sigma = torch.tensor( + [ + [[a, 0], [0, a]], + [[-a, 0], [0, a]], + [[0, a], [a, 0]], + [[0, 1j * a], [-1j * a, 0]], + ] + ) + return sigma +# torch.allclose( +# torch.abs(torch.einsum("kij,lji->kl", s, s) - torch.eye(4)), +# torch.zeros((4, 4)), +# atol=1e-5, +# ) + + +def gellmann(): + # zyx order + # trace-orthogonal normalization + # torch.einsum("kij,lji->kl", gellmann(), gellmann()) == torch.eye(9) + # + # lexicographical order of the Gell-Mann matrices + # 00, 1-1, 10, 11, 2-2, 2-1, 20, 21, 22 + # + # zyx + # zyx + a = 3**-0.5 + b = -1j * 2**-0.5 + c = 2**-0.5 + d = -(6**-0.5) + e = 2 * (6**-0.5) + return torch.tensor( + [ + [[a, 0, 0], [0, a, 0], [0, 0, a]], + [[0, 0, -b], [0, 0, 0], [b, 0, 0]], + [[0, 0, 0], [0, 0, -b], [0, b, 0]], + [[0, -b, 0], [b, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, c], [0, c, 0]], # + [[0, c, 0], [c, 0, 0], [0, 0, 0]], + [[e, 0, 0], [0, d, 0], [0, 0, d]], + [[0, 0, c], [0, 0, 0], [c, 0, 0]], + [[0, 0, 0], [0, -c, 0], [0, 0, c]], # + ] + ) + + # torch.allclose( +# torch.abs(torch.einsum("kij,lji->kl", Y, Y) - torch.eye(9)), +# torch.zeros((9, 9)), +# atol=1e-5, +# ) diff --git a/waveorder/waveorder_reconstructor.py b/waveorder/waveorder_reconstructor.py index df62aaa..5faed08 100644 --- a/waveorder/waveorder_reconstructor.py +++ b/waveorder/waveorder_reconstructor.py @@ -160,7 +160,6 @@ def instrument_matrix_calibration(I_cali_norm, I_meas): class waveorder_microscopy: - """ waveorder_microscopy contains reconstruction algorithms for label-free @@ -732,9 +731,7 @@ def inclination_recon_setup(self, inc_recon): wave_vec_norm_x = self.lambda_illu * self.fxx wave_vec_norm_y = self.lambda_illu * self.fyy wave_vec_norm_z = ( - np.maximum( - 0, 1 - wave_vec_norm_x**2 - wave_vec_norm_y**2 - ) + np.maximum(0, 1 - wave_vec_norm_x**2 - wave_vec_norm_y**2) ) ** (0.5) incident_theta = np.arctan2( @@ -1005,7 +1002,7 @@ def gen_2D_vec_WOTF(self, inc_option=False): .numpy() .transpose((1, 2, 0)) ) - G_tensor_z = gen_dyadic_Greens_tensor_z( + G_tensor_z = generate_defocus_greens_tensor( self.fxx, self.fyy, G_fun_z, self.Pupil_support, self.lambda_illu ) @@ -4017,9 +4014,7 @@ def Fluor_anisotropy_recon(self, S1_stack, S2_stack): S1_stack = cp.array(S1_stack) S2_stack = cp.array(S2_stack) - anisotropy = cp.asnumpy( - 0.5 * cp.sqrt(S1_stack**2 + S2_stack**2) - ) + anisotropy = cp.asnumpy(0.5 * cp.sqrt(S1_stack**2 + S2_stack**2)) orientation = cp.asnumpy( (0.5 * cp.arctan2(S2_stack, S1_stack)) % np.pi ) From 945dbcba61637d669967fe4942ba15ad3e135a0a Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 14 May 2024 13:41:50 -0700 Subject: [PATCH 03/72] may 14 poster draft --- examples/models/anisotropic_thick_3d.py | 159 ++++++++++++++++++++---- 1 file changed, 135 insertions(+), 24 deletions(-) diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py index f73cbe3..63cf05e 100644 --- a/examples/models/anisotropic_thick_3d.py +++ b/examples/models/anisotropic_thick_3d.py @@ -199,9 +199,9 @@ print("H_re_stokes: (RE, IM, ABS)") torch.set_printoptions(precision=1) -print(torch.log10(torch.sum(torch.real(H_re_stokes) ** 2, dim=(-3, -2, -1)))) -print(torch.log10(torch.sum(torch.imag(H_re_stokes) ** 2, dim=(-3, -2, -1)))) -print(torch.log10(torch.sum(torch.abs(H_re_stokes) ** 2, dim=(-3, -2, -1)))) +print(torch.amax(torch.real(H_re_stokes), dim=(-3, -2, -1))) +print(torch.amax(torch.imag(H_re_stokes), dim=(-3, -2, -1))) +print(torch.amax(torch.abs(H_re_stokes), dim=(-3, -2, -1))) # Display transfer function v = napari.Viewer() @@ -253,35 +253,44 @@ def view_transfer_function( def plot_data(data, y_slices, filename): - fig, axs = plt.subplots(4, 9, figsize=(20, 10)) # Adjust the size as needed + fig, axs = plt.subplots( + 4, 9, figsize=(20, 10) + ) # Adjust the size as needed for i in range(data.shape[0]): # Stokes parameter for j in range(data.shape[1]): # Object parameter for k, y in enumerate(y_slices): # Y slices z = data[i, j, :, y, :] - hue = np.angle(z) / (2 * np.pi) + 0.5 # Normalize and shift to make red at 0 + hue = ( + np.angle(z) / (2 * np.pi) + 0.5 + ) # Normalize and shift to make red at 0 sat = np.abs(z) / np.amax(np.abs(z)) hsv = np.stack((hue, sat, np.ones_like(sat)), axis=-1) rgb = mcolors.hsv_to_rgb(hsv) - + ax = axs[i, j] - ax.imshow(rgb, aspect='auto') - ax.set_title('') # Remove titles + ax.imshow(rgb, aspect="auto") + ax.set_title("") # Remove titles ax.set_xticks([]) # Remove x-axis ticks ax.set_yticks([]) # Remove y-axis ticks - ax.spines['top'].set_visible(False) # Hide top spine - ax.spines['right'].set_visible(False) # Hide right spine - ax.spines['bottom'].set_visible(False) # Hide bottom spine - ax.spines['left'].set_visible(False) # Hide left spine - ax.set_xlabel('') # Remove x-axis labels + ax.spines["top"].set_visible(False) # Hide top spine + ax.spines["right"].set_visible(False) # Hide right spine + ax.spines["bottom"].set_visible(False) # Hide bottom spine + ax.spines["left"].set_visible(False) # Hide left spine + ax.set_xlabel("") # Remove x-axis labels plt.tight_layout() - plt.savefig(filename, format='pdf') + plt.savefig(filename, format="pdf") + # Adjust y_slices according to your index base (check if your array index starts at 0) -y_center = 128 # Assuming the middle index for Y dimension -y_slices = [y_center - 10, y_center, y_center + 10] -plot_data(torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), y_slices, './output.pdf') +y_center = 128 # Assuming the middle index for Y dimension +y_slices = [y_center, y_center, y_center] +plot_data( + torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), + y_slices, + "./output.pdf", +) # Simulate yx_star, yx_theta, _ = util.generate_star_target( @@ -299,21 +308,123 @@ def plot_data(data, y_slices, filename): object[:, zyx_shape[0] // 2, ...] = center_slice_object # Simulate +H = H_re_stokes[:, (0, 4, 8), ...] # for transverse linear birefringence object_spectrum = torch.fft.fftn(object, dim=(-3, -2, -1)) -data_spectrum = torch.einsum( - "slzyx,lzyx->szyx", H_re_stokes[:, (0, 4, 8), ...], object_spectrum -) +data_spectrum = torch.einsum("slzyx,lzyx->szyx", H, object_spectrum) data = torch.fft.ifftn(data_spectrum, dim=(-3, -2, -1)) -v.add_image(object.numpy()) -v.add_image(torch.real(data).numpy()) -v.add_image(torch.imag(data).numpy()) +# Simple measurement space +Hsvd = torch.movedim(H, (0, 1), (-2, -1)) +# Computing SVD...can simplify this +print("Calculating SVD") +_, Ssvd, _ = torch.linalg.svd(Hsvd, full_matrices=False) +Hinv = torch.linalg.pinv(Hsvd) + +S_trunc = Ssvd > 5 # cutoff small singular values +recon_spectrum = torch.einsum( + "zyxl,zyxls,szyx->lzyx", S_trunc, Hinv, data_spectrum +) +recon_object = torch.fft.ifftn(recon_spectrum, dim=(-3, -2, -1)) + +# Tikhonov-regularized reconstruction (aka project onto measurement space) +# print("Computing SVD") + +# # Correct reconstruction +# reg = 1 +# S_reg = S / (S**2 + reg**2) +# # recon_object_spectrum = torch.einsum("jkzyx,jzyx,ijzyx,izyx->kzyx", Vh, S_reg, torch.conj(U), data_spectrum) +# recon_object_spectrum = torch.einsum( +# "kjzyx,jzyx,jizyx,izyx->kzyx", U, S_reg, Vh, data_spectrum +# ) + +# recon_object = torch.fft.ifftn(recon_object_spectrum, dim=(-3, -2, -1)) + + +# v.add_image(object.numpy()) +# v.add_image(torch.real(data).numpy()) +# v.add_image(torch.real(recon_object).numpy()) +# v.add_image(torch.imag(recon_object).numpy()) + +# Plot +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + + +def plot_3d_array_slices(data, filename="output.pdf"): + # Dimensions of the data + z_dim, y_dim, x_dim = data.shape + + vmax = np.max(data) + vmin = np.min(data) + + # Create the figure and define the subplots using GridSpec + fig = plt.figure(figsize=(10, 10)) # Adjust the overall size as necessary + gs = gridspec.GridSpec( + 2, 2, height_ratios=[y_dim, z_dim], width_ratios=[x_dim, z_dim] + ) + + # XY view (Z slice through the middle) + ax1 = fig.add_subplot(gs[0, 0]) + xy_slice = np.copy(data[z_dim // 2, :, :]) + xy_slice[:, : x_dim // 3] = data[z_dim // 2 + 2, :, : x_dim // 3] + xy_slice[:, -x_dim // 3 :] = data[z_dim // 2 - 2, :, -x_dim // 3 :] + + ax1.imshow( + xy_slice, + origin="upper", + cmap="gray", + extent=[0, x_dim, y_dim, 0], + vmin=vmin, + vmax=vmax, + ) + ax1.axis("off") + + # YZ view (X slice through the middle) - need to transpose and adjust extent + ax2 = fig.add_subplot(gs[0, 1]) + yz_slice = np.flip(data[:, :, x_dim // 2].T, axis=1) + ax2.imshow( + yz_slice, + origin="upper", + cmap="gray", + extent=[0, z_dim, y_dim, 0], + vmin=vmin, + vmax=vmax, + ) + ax2.axis("off") + + # XZ view (Y slice through the middle) - need to transpose and adjust extent + ax3 = fig.add_subplot(gs[1, 0]) + xz_slice = data[:, y_dim // 2, :] + ax3.imshow( + xz_slice, + origin="lower", + cmap="gray", + extent=[0, x_dim, z_dim, 0], + vmin=vmin, + vmax=vmax, + ) + ax3.axis("off") + + # Adjust layout + plt.tight_layout() + plt.savefig(filename, format="pdf") + plt.close(fig) + + +for i, plot_data in enumerate(object): + plot_3d_array_slices(torch.real(plot_data).numpy(), f"object{i}.pdf") + +for i, plot_data in enumerate(data): + plot_3d_array_slices(torch.real(plot_data).numpy(), f"data{i}.pdf") + +for i, plot_data in enumerate(recon_object): + plot_3d_array_slices(torch.real(plot_data).numpy(), f"recon{i}.pdf") import pdb pdb.set_trace() - zyx_data = phase_thick_3d.apply_transfer_function( zyx_phase, real_potential_transfer_function, From 05db85d8615fa2881b113696089de8114bfec830 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Sun, 26 May 2024 17:19:28 -0700 Subject: [PATCH 04/72] last-minute poster changes --- examples/models/anisotropic_thick_3d.py | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py index 63cf05e..63d49b5 100644 --- a/examples/models/anisotropic_thick_3d.py +++ b/examples/models/anisotropic_thick_3d.py @@ -286,11 +286,11 @@ def plot_data(data, y_slices, filename): # Adjust y_slices according to your index base (check if your array index starts at 0) y_center = 128 # Assuming the middle index for Y dimension y_slices = [y_center, y_center, y_center] -plot_data( - torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), - y_slices, - "./output.pdf", -) +# plot_data( +# torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), +# y_slices, +# "./output.pdf", +# ) # Simulate yx_star, yx_theta, _ = util.generate_star_target( @@ -351,7 +351,7 @@ def plot_data(data, y_slices, filename): import matplotlib.gridspec as gridspec -def plot_3d_array_slices(data, filename="output.pdf"): +def plot_3d_array_slices(data, z_shift=0, filename="output.pdf"): # Dimensions of the data z_dim, y_dim, x_dim = data.shape @@ -366,9 +366,7 @@ def plot_3d_array_slices(data, filename="output.pdf"): # XY view (Z slice through the middle) ax1 = fig.add_subplot(gs[0, 0]) - xy_slice = np.copy(data[z_dim // 2, :, :]) - xy_slice[:, : x_dim // 3] = data[z_dim // 2 + 2, :, : x_dim // 3] - xy_slice[:, -x_dim // 3 :] = data[z_dim // 2 - 2, :, -x_dim // 3 :] + xy_slice = np.copy(data[z_dim // 2 - z_shift, :, :]) ax1.imshow( xy_slice, @@ -412,14 +410,21 @@ def plot_3d_array_slices(data, filename="output.pdf"): plt.close(fig) -for i, plot_data in enumerate(object): - plot_3d_array_slices(torch.real(plot_data).numpy(), f"object{i}.pdf") +for z_shift in [0, -2, 2]: + for i, plot_data in enumerate(object): + plot_3d_array_slices( + torch.real(plot_data).numpy(), z_shift, f"object{i}{z_shift}.pdf" + ) -for i, plot_data in enumerate(data): - plot_3d_array_slices(torch.real(plot_data).numpy(), f"data{i}.pdf") + for i, plot_data in enumerate(data): + plot_3d_array_slices( + torch.real(plot_data).numpy(), z_shift, f"data{i}{z_shift}.pdf" + ) -for i, plot_data in enumerate(recon_object): - plot_3d_array_slices(torch.real(plot_data).numpy(), f"recon{i}.pdf") + for i, plot_data in enumerate(recon_object): + plot_3d_array_slices( + torch.real(plot_data).numpy(), z_shift, f"recon{i}{z_shift}.pdf" + ) import pdb From 15d2fbd97d9424ea7f6ad1309f51a529661dc03b Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 10 Jun 2024 08:54:06 -0700 Subject: [PATCH 05/72] quick clearning --- examples/models/anisotropic_thick_3d.py | 77 +++---------------------- 1 file changed, 8 insertions(+), 69 deletions(-) diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py index 63d49b5..3c3e41c 100644 --- a/examples/models/anisotropic_thick_3d.py +++ b/examples/models/anisotropic_thick_3d.py @@ -1,11 +1,12 @@ +# %% + import torch import napari import numpy as np from waveorder import optics, util -from waveorder.models import phase_thick_3d # Parameters -# all lengths must use consistent units e.g. um +# all lengths use consistent units e.g. um margin = 50 simulation_arguments = { "zyx_shape": (129, 256, 256), @@ -13,20 +14,14 @@ "z_pixel_size": 0.1, "index_of_refraction_media": 1.25, } -# phantom_arguments = {"index_of_refraction_sample": 1.50, "sphere_radius": 5} transfer_function_arguments = { "z_padding": 0, "wavelength_illumination": 0.5, - "numerical_aperture_illumination": 0.75, # 75, + "numerical_aperture_illumination": 0.75, "numerical_aperture_detection": 1.0, } input_jones = torch.tensor([0.0 + 1.0j, 1.0 + 0j]) -# # Create a phantom -# zyx_phase = phase_thick_3d.generate_test_phantom( -# **simulation_arguments, **phantom_arguments -# ) - # Convert zyx_shape = simulation_arguments["zyx_shape"] yx_pixel_size = simulation_arguments["yx_pixel_size"] @@ -123,42 +118,7 @@ lambda_in=wavelength_illumination / index_of_refraction_media, ) -# window = torch.fft.ifftshift( -# torch.hann_window(z_position_list.shape[0], periodic=False) -# ) - -# ###### LATEST - -# # abs() and *(1j) are hacks to correct for tricky phase shifts -# P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) -# G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) -# S_3D = torch.fft.ifft(S, dim=-3) - -# # Normalize -# P_3D /= torch.amax(torch.abs(P_3D)) -# G_3D /= torch.amax(torch.abs(G_3D)) -# S_3D /= torch.amax(torch.abs(S_3D)) - -# # Main part -# PG_3D = torch.einsum("ijzyx,jpzyx->ipzyx", P_3D, G_3D) -# PS_3D = torch.einsum("jlzyx,lzyx,kzyx->jlzyx", P_3D, S_3D, torch.conj(S_3D)) - -# # PG_3D /= torch.amax(torch.abs(PG_3D)) -# # PS_3D /= torch.amax(torch.abs(PS_3D)) - -# pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) -# ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) - -# H1 = torch.fft.ifftn( -# torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), -# dim=(-3, -2, -1), -# ) - -# H2 = torch.fft.ifftn( -# torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), -# dim=(-3, -2, -1), -# ) - +# %% # MAY 12 Simplified P_3D = torch.abs(torch.fft.ifft(sP, dim=-3)).type(torch.complex64) G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) @@ -241,11 +201,12 @@ def view_transfer_function( # v.dims.order = (2, 1, 0) -# view_transfer_function(H_re_stokes) +view_transfer_function(H_re_stokes) # view_transfer_function(G_3D) # view_transfer_function(H_re) # view_transfer_function(P_3D) +# %% # PLOT transfer function import numpy as np import matplotlib.pyplot as plt @@ -345,6 +306,7 @@ def plot_data(data, y_slices, filename): # v.add_image(torch.real(recon_object).numpy()) # v.add_image(torch.imag(recon_object).numpy()) +# %% # Plot import numpy as np import matplotlib.pyplot as plt @@ -429,26 +391,3 @@ def plot_3d_array_slices(data, z_shift=0, filename="output.pdf"): import pdb pdb.set_trace() - -zyx_data = phase_thick_3d.apply_transfer_function( - zyx_phase, - real_potential_transfer_function, - transfer_function_arguments["z_padding"], - brightness=1e3, -) - -# Reconstruct -zyx_recon = phase_thick_3d.apply_inverse_transfer_function( - zyx_data, - real_potential_transfer_function, - imag_potential_transfer_function, - transfer_function_arguments["z_padding"], -) - -# Display -viewer.add_image(zyx_phase.numpy(), name="Phantom", scale=zyx_scale) -viewer.add_image(zyx_data.numpy(), name="Data", scale=zyx_scale) -viewer.add_image(zyx_recon.numpy(), name="Reconstruction", scale=zyx_scale) -input("Showing object, data, and recon. Press to quit...") - -# %% From 6034cbb90fad3449035a5ceddf2320fc69c01eeb Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 12 Jun 2024 11:57:52 -0700 Subject: [PATCH 06/72] checkpoint before svd refactor --- .../models/inplane_oriented_thick_pol3d.py | 62 +++++ .../inplane_oriented_thick_pol3d_vector.py | 253 ++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 examples/models/inplane_oriented_thick_pol3d.py create mode 100644 waveorder/models/inplane_oriented_thick_pol3d_vector.py diff --git a/examples/models/inplane_oriented_thick_pol3d.py b/examples/models/inplane_oriented_thick_pol3d.py new file mode 100644 index 0000000..a1235c2 --- /dev/null +++ b/examples/models/inplane_oriented_thick_pol3d.py @@ -0,0 +1,62 @@ +import napari + +from waveorder.models import inplane_oriented_thick_pol3d + +# Parameters +# all lengths must use consistent units e.g. um +simulation_arguments = {"yx_shape": (256, 256)} +transfer_function_arguments = {"swing": 0.1, "scheme": "5-State"} + +# Create a phantom +inplane_oriented_parameters = ( + inplane_oriented_thick_pol3d.generate_test_phantom(**simulation_arguments) +) + +# Calculate transfer function +intensity_to_stokes_matrix = ( + inplane_oriented_thick_pol3d.calculate_transfer_function( + **transfer_function_arguments + ) +) + +# Display transfer function +viewer = napari.Viewer() +inplane_oriented_thick_pol3d.visualize_transfer_function( + viewer, intensity_to_stokes_matrix +) +input("Showing transfer functions. Press to continue...") +viewer.layers.select_all() +viewer.layers.remove_selected() + +# Simulate +czyx_data = inplane_oriented_thick_pol3d.apply_transfer_function( + *inplane_oriented_parameters, + intensity_to_stokes_matrix, +) + +# Reconstruct +inplane_oriented_parameters_recon = ( + inplane_oriented_thick_pol3d.apply_inverse_transfer_function( + czyx_data, intensity_to_stokes_matrix + ) +) + +# Display +arrays = [ + (inplane_oriented_parameters_recon[3], "Depolarization - recon"), + (inplane_oriented_parameters_recon[2], "Transmittance - recon"), + (inplane_oriented_parameters_recon[1], "Orientation (rad) - recon"), + (inplane_oriented_parameters_recon[0], "Retardance (rad) - recon"), + (czyx_data, "Data"), + (inplane_oriented_parameters[3], "Depolarization"), + (inplane_oriented_parameters[2], "Transmittance"), + (inplane_oriented_parameters[1], "Orientation (rad)"), + (inplane_oriented_parameters[0], "Retardance (rad)"), +] + +for array in arrays: + viewer.add_image(array[0].cpu().numpy(), name=array[1]) + +viewer.grid.enabled = True +viewer.grid.shape = (2, 5) +input("Showing object, data, and recon. Press to quit...") diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py new file mode 100644 index 0000000..3649656 --- /dev/null +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -0,0 +1,253 @@ +from waveorder import optics, stokes, util +from waveorder.models import inplane_oriented_thick_pol3d +from torch import Tensor + +import torch +import numpy as np + + +def generate_test_phantom(zyx_shape): + + # Simulate + yx_star, yx_theta, _ = util.generate_star_target( + yx_shape=zyx_shape[1:], + blur_px=1, + margin=50, + ) + c00 = yx_star + c2_2 = -torch.sin(2 * yx_theta) * yx_star + c22 = torch.cos(2 * yx_theta) * yx_star + + # Put in a center slices of a 3D object + center_slice_object = torch.stack((c00, c2_2, c22), dim=0) + object = torch.zeros((3,) + zyx_shape) + object[:, zyx_shape[0] // 2, ...] = center_slice_object + return object + + +def calculate_transfer_function( + swing, + scheme, + zyx_shape, + yx_pixel_size, + z_pixel_size, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=False, +): + intensity_to_stokes_matrix = stokes.calculate_intensity_to_stokes_matrix( + swing, scheme=scheme + ) + + input_jones = torch.tensor([0.0 + 1.0j, 1.0 + 0j]) # circular + + # Calculate frequencies + y_frequencies, x_frequencies = util.generate_frequencies( + zyx_shape[1:], yx_pixel_size + ) + radial_frequencies = torch.sqrt(x_frequencies**2 + y_frequencies**2) + + z_total = zyx_shape[0] + 2 * z_padding + z_position_list = torch.fft.ifftshift( + (torch.arange(z_total) - z_total // 2) * z_pixel_size + ) + if invert_phase_contrast: + z_position_list = torch.flip(z_position_list, dims=(0,)) + + # 2D pupils + ill_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_illumination, + wavelength_illumination, + ) + det_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_detection, + wavelength_illumination, + ) + pupil = optics.generate_pupil( + radial_frequencies, + index_of_refraction_media, # largest possible NA + wavelength_illumination, + ) + + # Defocus pupils + defocus_pupil = optics.generate_propagation_kernel( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + ) + + greens_functions_z = optics.generate_greens_function_z( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + ) + + # Calculate vector defocus pupils + S = optics.generate_vector_source_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + input_jones, + ill_pupil, + wavelength_illumination / index_of_refraction_media, + ) + + # Simplified scalar pupil + P = optics.generate_propagation_kernel( + radial_frequencies, + det_pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + ) + + # TODO consider testing this instead of sP + """ + P = optics.generate_vector_detection_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + det_pupil, + wavelength_illumination / index_of_refraction_media, + ) + """ + + G = optics.generate_defocus_greens_tensor( + x_frequencies, + y_frequencies, + greens_functions_z, + pupil, + lambda_in=wavelength_illumination / index_of_refraction_media, + ) + + P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) + G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) + S_3D = torch.fft.ifft(S, dim=-3) + + # Normalize + P_3D /= torch.amax(torch.abs(P_3D)) + G_3D /= torch.amax(torch.abs(G_3D)) + S_3D /= torch.amax(torch.abs(S_3D)) + + # Main part + PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) + PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) + + PG_3D /= torch.amax(torch.abs(PG_3D)) + PS_3D /= torch.amax(torch.abs(PS_3D)) + + pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) + ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) + + H1 = torch.fft.ifftn( + torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), + dim=(-3, -2, -1), + ) + + H2 = torch.fft.ifftn( + torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), + dim=(-3, -2, -1), + ) + + H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components + # H_im = 1j * (H1 - H2) # ignore absorptive terms + + s = util.pauli()[[0, 1, 2]] # select s0, s1, and s2 (drop s3) + Y = util.gellmann()[[0, 4, 8]] + # select phase f00 and transverse linear isotropic terms 2-2, and f22 + + sfZYX_transfer_function = torch.einsum( + "sik,ikpjzyx,lpj->slzyx", s, H_re, Y + ) + + # transfer function + return ( + sfZYX_transfer_function, + intensity_to_stokes_matrix, + ) # (3 stokes, 3 object, Z, Y, X) + + +def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): + shift_dims = (-3, -2, -1) + lim = torch.max(torch.abs(sfZYX_transfer_function)) * 0.9 + + viewer.add_image( + torch.fft.ifftshift( + torch.real(sfZYX_transfer_function), dim=shift_dims + ) + .cpu() + .numpy(), + name="Real. TF", + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 + / (np.array(zyx_scale) * np.array(sfZYX_transfer_function.shape[-3:])), + ) + + viewer.add_image( + torch.fft.ifftshift( + torch.imag(sfZYX_transfer_function), dim=shift_dims + ) + .cpu() + .numpy(), + name="Imag. TF", + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 + / (np.array(zyx_scale) * np.array(sfZYX_transfer_function.shape[-3:])), + ) + + _, _, Z, Y, X = sfZYX_transfer_function.shape + viewer.dims.current_step = (0, 0, Z // 2, Y // 2, X // 2) + viewer.dims.order = (4, 0, 1, 2, 3) + + +def apply_transfer_function( + fzyx_object, + sfZYX_transfer_function, + intensity_to_stokes_matrix, # TODO use this to simulate intensities +): + fZYX_object = torch.fft.fftn(fzyx_object, dim=(1, 2, 3)) + sZYX_data = torch.einsum( + "fzyx,sfzyx->szyx", fZYX_object, sfZYX_transfer_function + ) + szyx_data = torch.fft.ifftn(sZYX_data, dim=(1, 2, 3)) + + return (5 * szyx_data) + 0.1 * torch.randn(szyx_data.shape) + + +def apply_inverse_transfer_function( + szyx_data: Tensor, + sfZYX_transfer_function: Tensor, + intensity_to_stokes_matrix: Tensor, + regularization_strength: float = 1e-3, +): + sZYX_data = torch.fft.fftn(szyx_data, dim=(1, 2, 3)) + ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) + + # Compute regularized inverse filter + print("Computing SVD") + U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) + S /= torch.max(S) + + # Key computation + print("Computing inverse filter") + S_reg = S / (S**2 + regularization_strength**2) + ZYXsf_inverse_filter = -torch.einsum( + "zyxij,zyxj,zyxjk->zyxki", U, S_reg, Vh + ) + + # Apply inverse filter + fZYX_reconstructed = torch.einsum( + "szyx,zyxsf->fzyx", sZYX_data, ZYXsf_inverse_filter + ) + + return torch.real(torch.fft.ifftn(fZYX_reconstructed, dim=(1, 2, 3))) From 592675d71cc80673647ff222cb8ac9b81c7aa68e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 17 Jun 2024 10:35:44 -0700 Subject: [PATCH 07/72] SVD refactor --- examples/models/anisotropic_thick_3d.py | 3 +- .../models/inplane_oriented_thick_pol3D.py | 62 -------------- .../inplane_oriented_thick_pol3d_vector.py | 85 +++++++++++++++++++ 3 files changed, 87 insertions(+), 63 deletions(-) delete mode 100644 examples/models/inplane_oriented_thick_pol3D.py create mode 100644 examples/models/inplane_oriented_thick_pol3d_vector.py diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py index 3c3e41c..f7d669b 100644 --- a/examples/models/anisotropic_thick_3d.py +++ b/examples/models/anisotropic_thick_3d.py @@ -1,3 +1,4 @@ +# WIP for the most general case # %% import torch @@ -205,7 +206,7 @@ def view_transfer_function( # view_transfer_function(G_3D) # view_transfer_function(H_re) # view_transfer_function(P_3D) - +import pdb; pdb.set_trace() # %% # PLOT transfer function import numpy as np diff --git a/examples/models/inplane_oriented_thick_pol3D.py b/examples/models/inplane_oriented_thick_pol3D.py deleted file mode 100644 index a1235c2..0000000 --- a/examples/models/inplane_oriented_thick_pol3D.py +++ /dev/null @@ -1,62 +0,0 @@ -import napari - -from waveorder.models import inplane_oriented_thick_pol3d - -# Parameters -# all lengths must use consistent units e.g. um -simulation_arguments = {"yx_shape": (256, 256)} -transfer_function_arguments = {"swing": 0.1, "scheme": "5-State"} - -# Create a phantom -inplane_oriented_parameters = ( - inplane_oriented_thick_pol3d.generate_test_phantom(**simulation_arguments) -) - -# Calculate transfer function -intensity_to_stokes_matrix = ( - inplane_oriented_thick_pol3d.calculate_transfer_function( - **transfer_function_arguments - ) -) - -# Display transfer function -viewer = napari.Viewer() -inplane_oriented_thick_pol3d.visualize_transfer_function( - viewer, intensity_to_stokes_matrix -) -input("Showing transfer functions. Press to continue...") -viewer.layers.select_all() -viewer.layers.remove_selected() - -# Simulate -czyx_data = inplane_oriented_thick_pol3d.apply_transfer_function( - *inplane_oriented_parameters, - intensity_to_stokes_matrix, -) - -# Reconstruct -inplane_oriented_parameters_recon = ( - inplane_oriented_thick_pol3d.apply_inverse_transfer_function( - czyx_data, intensity_to_stokes_matrix - ) -) - -# Display -arrays = [ - (inplane_oriented_parameters_recon[3], "Depolarization - recon"), - (inplane_oriented_parameters_recon[2], "Transmittance - recon"), - (inplane_oriented_parameters_recon[1], "Orientation (rad) - recon"), - (inplane_oriented_parameters_recon[0], "Retardance (rad) - recon"), - (czyx_data, "Data"), - (inplane_oriented_parameters[3], "Depolarization"), - (inplane_oriented_parameters[2], "Transmittance"), - (inplane_oriented_parameters[1], "Orientation (rad)"), - (inplane_oriented_parameters[0], "Retardance (rad)"), -] - -for array in arrays: - viewer.add_image(array[0].cpu().numpy(), name=array[1]) - -viewer.grid.enabled = True -viewer.grid.shape = (2, 5) -input("Showing object, data, and recon. Press to quit...") diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py new file mode 100644 index 0000000..c77be88 --- /dev/null +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -0,0 +1,85 @@ +import torch +import napari + +from waveorder.models import ( + inplane_oriented_thick_pol3d, + inplane_oriented_thick_pol3d_vector, +) + +# Parameters +# all lengths must use consistent units e.g. um +oversample_factor = 2 +zyx_shape = (100, 256, 256) +swing = 0.1 +scheme = "5-State" +yx_pixel_size = 0.325 / oversample_factor # 0.325 +z_pixel_size = 2.0 / oversample_factor # 2.0 +wavelength_illumination = 0.532 +z_padding = 0 +index_of_refraction_media = 1.0 +numerical_aperture_illumination = 0.4 +numerical_aperture_detection = 0.55 + +# Create a phantom +fzyx_object = inplane_oriented_thick_pol3d_vector.generate_test_phantom( + zyx_shape +) + +# Calculate transfer function +sfZYX_transfer_function, intensity_to_stokes_matrix = ( + inplane_oriented_thick_pol3d_vector.calculate_transfer_function( + swing, + scheme, + zyx_shape, + yx_pixel_size, + z_pixel_size, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + ) +) + +# Display transfer function +viewer = napari.Viewer() +inplane_oriented_thick_pol3d_vector.visualize_transfer_function( + viewer, + sfZYX_transfer_function, + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), +) + +# input("Showing transfer functions. Press to continue...") +viewer.layers.select_all() +viewer.layers.remove_selected() + +# Simulate +szyx_data = inplane_oriented_thick_pol3d_vector.apply_transfer_function( + fzyx_object, + sfZYX_transfer_function, + intensity_to_stokes_matrix, +) + +# Reconstruct +fzyx_object_recon = ( + inplane_oriented_thick_pol3d_vector.apply_inverse_transfer_function( + szyx_data, + sfZYX_transfer_function, + intensity_to_stokes_matrix, + regularization_strength=1e-1, + ) +) + +# Display +arrays = [ + (fzyx_object_recon, "Object - recon"), + (szyx_data, "Data"), + (fzyx_object, "Object"), +] + +for array in arrays: + viewer.add_image(torch.real(array[0]).cpu().numpy(), name=array[1]) + +# viewer.grid.enabled = True +# viewer.grid.shape = (2, 5) +input("Showing object, data, and recon. Press to quit...") From 0fa4bbe69d68b67223f2ef5eaa7fc772a456f4be Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 17 Jun 2024 10:35:56 -0700 Subject: [PATCH 08/72] pass singular system --- .../inplane_oriented_thick_pol3d_vector.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 3649656..711fa76 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -168,9 +168,16 @@ def calculate_transfer_function( "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) + # Compute regularized inverse filter + print("Computing SVD") + ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) + U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) + S /= torch.max(S) + singular_system = (U, S, Vh) + # transfer function return ( - sfZYX_transfer_function, + singular_system, intensity_to_stokes_matrix, ) # (3 stokes, 3 object, Z, Y, X) @@ -226,20 +233,15 @@ def apply_transfer_function( def apply_inverse_transfer_function( szyx_data: Tensor, - sfZYX_transfer_function: Tensor, + singular_system: tuple[Tensor], intensity_to_stokes_matrix: Tensor, regularization_strength: float = 1e-3, ): sZYX_data = torch.fft.fftn(szyx_data, dim=(1, 2, 3)) - ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) - - # Compute regularized inverse filter - print("Computing SVD") - U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) - S /= torch.max(S) # Key computation print("Computing inverse filter") + U, S, Vh = singular_system S_reg = S / (S**2 + regularization_strength**2) ZYXsf_inverse_filter = -torch.einsum( "zyxij,zyxj,zyxjk->zyxki", U, S_reg, Vh From 63dd98cea7ed27f6468c9a20faa630fe8029fb44 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 14:32:23 -0700 Subject: [PATCH 09/72] update visualization script --- examples/models/inplane_oriented_thick_pol3d_vector.py | 8 ++++---- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index c77be88..05c8f1e 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -9,7 +9,7 @@ # Parameters # all lengths must use consistent units e.g. um oversample_factor = 2 -zyx_shape = (100, 256, 256) +zyx_shape = (50, 128, 128) # (100, 256, 256) swing = 0.1 scheme = "5-State" yx_pixel_size = 0.325 / oversample_factor # 0.325 @@ -26,7 +26,7 @@ ) # Calculate transfer function -sfZYX_transfer_function, intensity_to_stokes_matrix = ( +singular_system, sfZYX_transfer_function, intensity_to_stokes_matrix = ( inplane_oriented_thick_pol3d_vector.calculate_transfer_function( swing, scheme, @@ -49,7 +49,7 @@ zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), ) -# input("Showing transfer functions. Press to continue...") +input("Showing transfer functions. Press to continue...") viewer.layers.select_all() viewer.layers.remove_selected() @@ -64,7 +64,7 @@ fzyx_object_recon = ( inplane_oriented_thick_pol3d_vector.apply_inverse_transfer_function( szyx_data, - sfZYX_transfer_function, + singular_system, intensity_to_stokes_matrix, regularization_strength=1e-1, ) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 711fa76..e1d8f78 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -177,15 +177,16 @@ def calculate_transfer_function( # transfer function return ( - singular_system, + singular_system, # (3 stokes, 3 object, Z, Y, X) + sfZYX_transfer_function, intensity_to_stokes_matrix, - ) # (3 stokes, 3 object, Z, Y, X) - + ) def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): shift_dims = (-3, -2, -1) lim = torch.max(torch.abs(sfZYX_transfer_function)) * 0.9 + viewer.add_image( torch.fft.ifftshift( torch.real(sfZYX_transfer_function), dim=shift_dims From 96d8b850d7c5d515e64a25dc520111f1444a23d3 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 14:44:08 -0700 Subject: [PATCH 10/72] fix visualization script scaling --- waveorder/models/isotropic_fluorescent_thick_3d.py | 8 ++++++-- waveorder/models/phase_thick_3d.py | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/waveorder/models/isotropic_fluorescent_thick_3d.py b/waveorder/models/isotropic_fluorescent_thick_3d.py index b52e71b..7beaee9 100644 --- a/waveorder/models/isotropic_fluorescent_thick_3d.py +++ b/waveorder/models/isotropic_fluorescent_thick_3d.py @@ -1,5 +1,6 @@ from typing import Literal +import numpy as np import torch from torch import Tensor @@ -76,9 +77,12 @@ def visualize_transfer_function(viewer, optical_transfer_function, zyx_scale): name=array[1], colormap="bwr", contrast_limits=(-lim, lim), - scale=1 / zyx_scale, + scale=1/(np.array(zyx_scale) * np.array(optical_transfer_function.shape[-3:])), ) - viewer.dims.order = (0, 1, 2) + + Z, Y, X = optical_transfer_function.shape + viewer.dims.current_step = (Z // 2, Y // 2, X // 2) + viewer.dims.order = (2, 0, 1) def apply_transfer_function( diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index ac29d35..2a2903e 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -110,9 +110,14 @@ def visualize_transfer_function( name=array[1], colormap="bwr", contrast_limits=(-lim, lim), - scale=1 / zyx_scale, + scale=1 + / (np.array(zyx_scale) * np.array(real_potential_transfer_function.shape[-3:])), ) - viewer.dims.order = (0, 1, 2) + Z, Y, X = real_potential_transfer_function.shape + viewer.dims.current_step = (Z // 2, Y // 2, X // 2) + viewer.dims.order = (2, 0, 1) + + def apply_transfer_function( From 005ea5486848a61c9e06a9c29e52a39827d42b9b Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 15:15:35 -0700 Subject: [PATCH 11/72] correct phase recon regression, legacy recon assumes axially even green's function --- waveorder/models/phase_thick_3d.py | 1 + waveorder/optics.py | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index 2a2903e..5ecc373 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -72,6 +72,7 @@ def calculate_transfer_function( det_pupil, wavelength_illumination / index_of_refraction_media, z_position_list, + axially_even=False, ) ( diff --git a/waveorder/optics.py b/waveorder/optics.py index 5ec4eeb..65ac130 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -422,7 +422,8 @@ def generate_propagation_kernel( def generate_greens_function_z( - radial_frequencies, pupil_support, wavelength_illumination, z_position_list + radial_frequencies, pupil_support, wavelength_illumination, z_position_list, + axially_even=True, ): """ @@ -439,9 +440,14 @@ def generate_greens_function_z( wavelength_illumination : float wavelength of the light in the immersion media - z_position_list : torch.tensor or list + z_position_list : torch.tensor or list 1D array of defocused z position with the size of (Z,) + axially_even : bool + For backwards compatibility with legacy phase reconstruction. + Ideally the legacy phase reconstruction should be unified with + the new reconstructions, and this parameter should be removed. + Returns ------- greens_function_z : torch.tensor @@ -454,18 +460,17 @@ def generate_greens_function_z( * pupil_support ) ** (1 / 2) / wavelength_illumination + if axially_even: + z_positions = torch.abs(torch.tensor(z_position_list)[:, None, None]) + else: + z_positions = torch.tensor(z_position_list)[:, None, None] + greens_function_z = ( -1j / 4 / np.pi * pupil_support[None, :, :] - * torch.exp( - 1j - * 2 - * np.pi - * torch.abs(torch.tensor(z_position_list)[:, None, None]) - * oblique_factor[None, :, :] - ) + * torch.exp(1j * 2 * np.pi * z_positions * oblique_factor[None, :, :]) / (oblique_factor[None, :, :] + 1e-15) ) From 4034ce3959db584d19c515139ff697760a2542e1 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 17:14:44 -0700 Subject: [PATCH 12/72] helper functions --- waveorder/sampling.py | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 waveorder/sampling.py diff --git a/waveorder/sampling.py b/waveorder/sampling.py new file mode 100644 index 0000000..8352714 --- /dev/null +++ b/waveorder/sampling.py @@ -0,0 +1,91 @@ +import numpy as np +import torch + + +def transverse_nyquist( + wavelength_emission, + numerical_aperture_illumination, + numerical_aperture_detection, +): + """Transverse Nyquist sample spacing in `wavelength_emission` units. + + For widefield label-free imaging, the transverse Nyquist sample spacing is + lambda / (2 * (NA_ill + NA_det)). + + Perhaps surprisingly, the transverse Nyquist sample spacing for widefield + fluorescence is lambda / (4 * NA), which is equivalent to the above formula + when NA_ill = NA_det. + + Parameters + ---------- + wavelength_emission : float + Output units match these units + numerical_aperture_illumination : float + For widefield fluorescence, set to numerical_aperture_detection + numerical_aperture_detection : float + + Returns + ------- + float + Transverse Nyquist sample spacing + + """ + return wavelength_emission / ( + 2 * (numerical_aperture_detection + numerical_aperture_illumination) + ) + + +def axial_nyquist( + wavelength_emission, + numerical_aperture_detection, + index_of_refraction_media, +): + """Axial Nyquist sample spacing in `wavelength_emission` units. + + For widefield microscopes, the axial Nyquist sample spacing is: + + (n/lambda) - sqrt( (n/lambda)^2 - (NA_det/lambda)^2 ). + + Perhaps surprisingly, the axial Nyquist sample spacing is independent of + the illumination numerical aperture. + + Parameters + ---------- + wavelength_emission : float + Output units match these units + numerical_aperture_detection : float + index_of_refraction_media: float + + Returns + ------- + float + Axial Nyquist sample spacing + + """ + n_on_lambda = index_of_refraction_media / wavelength_emission + return n_on_lambda - np.sqrt( + n_on_lambda**2 + - (numerical_aperture_detection / wavelength_emission) ** 2 + ) + + +def nd_fourier_central_cuboid(source, target_shape): + """Central cuboid of an N-D Fourier transform. + + Parameters + ---------- + source : torch.Tensor + Source tensor + target_shape : tuple of int + + Returns + ------- + torch.Tensor + Center cuboid in Fourier space + + """ + center_slices = tuple( + slice((s - o) // 2, (s - o) // 2 + o) + for s, o in zip(source.shape, target_shape) + ) + return torch.fft.ifftshift(torch.fft.fftshift(source)[center_slices]) From 2e155fe5a3e25a94ccb2b127de44d8b2d88ff05a Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 17:15:02 -0700 Subject: [PATCH 13/72] fluorescence wrap safety --- .../models/isotropic_fluorescent_thick_3d.py | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/waveorder/models/isotropic_fluorescent_thick_3d.py b/waveorder/models/isotropic_fluorescent_thick_3d.py index b52e71b..d67dab8 100644 --- a/waveorder/models/isotropic_fluorescent_thick_3d.py +++ b/waveorder/models/isotropic_fluorescent_thick_3d.py @@ -1,9 +1,10 @@ from typing import Literal +import numpy as np import torch from torch import Tensor -from waveorder import optics, util +from waveorder import optics, sampling, util def generate_test_phantom( @@ -28,6 +29,49 @@ def calculate_transfer_function( index_of_refraction_media, numerical_aperture_detection, ): + + transverse_nyquist = sampling.transverse_nyquist( + wavelength_emission, + numerical_aperture_detection, # ill = det for fluorescence + numerical_aperture_detection, + ) + axial_nyquist = sampling.axial_nyquist( + wavelength_emission, + numerical_aperture_detection, + index_of_refraction_media, + ) + + yx_factor = int(np.ceil(yx_pixel_size / transverse_nyquist)) + z_factor = int(np.ceil(z_pixel_size / axial_nyquist)) + + optical_transfer_function = _calculate_wrap_unsafe_transfer_function( + ( + zyx_shape[0] * z_factor, + zyx_shape[1] * yx_factor, + zyx_shape[2] * yx_factor, + ), + yx_pixel_size / yx_factor, + z_pixel_size / z_factor, + wavelength_emission, + z_padding, + index_of_refraction_media, + numerical_aperture_detection, + ) + + return sampling.nd_fourier_central_cuboid( + optical_transfer_function, zyx_shape + ) + + +def _calculate_wrap_unsafe_transfer_function( + zyx_shape, + yx_pixel_size, + z_pixel_size, + wavelength_emission, + z_padding, + index_of_refraction_media, + numerical_aperture_detection, +): radial_frequencies = util.generate_radial_frequencies( zyx_shape[1:], yx_pixel_size ) @@ -97,7 +141,7 @@ def apply_transfer_function( Returns ------- Simulated data : torch.Tensor - + """ if ( zyx_object.shape[0] + 2 * z_padding From 783b916da53c014cf2acde767ba090f2deed40c8 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 17:27:48 -0700 Subject: [PATCH 14/72] 3d phase wrap safety --- waveorder/models/phase_thick_3d.py | 55 +++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index ac29d35..868b15a 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -4,7 +4,7 @@ import torch from torch import Tensor -from waveorder import optics, util +from waveorder import optics, sampling, util from waveorder.models import isotropic_fluorescent_thick_3d @@ -40,6 +40,59 @@ def calculate_transfer_function( numerical_aperture_illumination, numerical_aperture_detection, invert_phase_contrast=False, +): + transverse_nyquist = sampling.transverse_nyquist( + wavelength_illumination, + numerical_aperture_illumination, + numerical_aperture_detection, + ) + axial_nyquist = sampling.axial_nyquist( + wavelength_illumination, + numerical_aperture_detection, + index_of_refraction_media, + ) + + yx_factor = int(np.ceil(yx_pixel_size / transverse_nyquist)) + z_factor = int(np.ceil(z_pixel_size / axial_nyquist)) + + real_potential_transfer_function, imag_potential_transfer_function = ( + _calculate_wrap_unsafe_transfer_function( + ( + zyx_shape[0] * z_factor, + zyx_shape[1] * yx_factor, + zyx_shape[2] * yx_factor, + ), + yx_pixel_size / yx_factor, + z_pixel_size / z_factor, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=invert_phase_contrast, + ) + ) + + return ( + sampling.nd_fourier_central_cuboid( + real_potential_transfer_function, zyx_shape + ), + sampling.nd_fourier_central_cuboid( + imag_potential_transfer_function, zyx_shape + ), + ) + + +def _calculate_wrap_unsafe_transfer_function( + zyx_shape, + yx_pixel_size, + z_pixel_size, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=False, ): radial_frequencies = util.generate_radial_frequencies( zyx_shape[1:], yx_pixel_size From 55f8e5a29334549391e35eae789e7f4f3132738e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 17:42:52 -0700 Subject: [PATCH 15/72] fix axial nyquist bug --- waveorder/sampling.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/waveorder/sampling.py b/waveorder/sampling.py index 8352714..aab179d 100644 --- a/waveorder/sampling.py +++ b/waveorder/sampling.py @@ -42,9 +42,11 @@ def axial_nyquist( ): """Axial Nyquist sample spacing in `wavelength_emission` units. - For widefield microscopes, the axial Nyquist sample spacing is: + For widefield microscopes, the axial Nyquist cutoff frequency is: - (n/lambda) - sqrt( (n/lambda)^2 - (NA_det/lambda)^2 ). + (n/lambda) - sqrt( (n/lambda)^2 - (NA_det/lambda)^2 ), + + and the axial Nyquist sample spacing is 1 / (2 * cutoff_frequency). Perhaps surprisingly, the axial Nyquist sample spacing is independent of the illumination numerical aperture. @@ -63,10 +65,11 @@ def axial_nyquist( """ n_on_lambda = index_of_refraction_media / wavelength_emission - return n_on_lambda - np.sqrt( + cutoff_frequency = n_on_lambda - np.sqrt( n_on_lambda**2 - (numerical_aperture_detection / wavelength_emission) ** 2 ) + return 1 / (2 * cutoff_frequency) def nd_fourier_central_cuboid(source, target_shape): From 22a059b6fc323a7988540f61acc5975f9b77ade3 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 20:07:48 -0700 Subject: [PATCH 16/72] 2d phase wrap safety --- waveorder/models/isotropic_thin_3d.py | 61 ++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/waveorder/models/isotropic_thin_3d.py b/waveorder/models/isotropic_thin_3d.py index c1bb96a..1af9ed8 100644 --- a/waveorder/models/isotropic_thin_3d.py +++ b/waveorder/models/isotropic_thin_3d.py @@ -1,9 +1,10 @@ from typing import Literal, Tuple +import numpy as np import torch from torch import Tensor -from waveorder import optics, util +from waveorder import optics, sampling, util def generate_test_phantom( @@ -42,6 +43,64 @@ def calculate_transfer_function( numerical_aperture_illumination, numerical_aperture_detection, invert_phase_contrast=False, +): + transverse_nyquist = sampling.transverse_nyquist( + wavelength_illumination, + numerical_aperture_illumination, + numerical_aperture_detection, + ) + yx_factor = int(np.ceil(yx_pixel_size / transverse_nyquist)) + + absorption_2d_to_3d_transfer_function, phase_2d_to_3d_transfer_function = ( + _calculate_wrap_unsafe_transfer_function( + ( + yx_shape[0] * yx_factor, + yx_shape[1] * yx_factor, + ), + yx_pixel_size / yx_factor, + z_position_list, + wavelength_illumination, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=invert_phase_contrast, + ) + ) + + absorption_2d_to_3d_transfer_function_out = torch.zeros( + (len(z_position_list),) + tuple(yx_shape), dtype=torch.complex64 + ) + phase_2d_to_3d_transfer_function_out = torch.zeros( + (len(z_position_list),) + tuple(yx_shape), dtype=torch.complex64 + ) + + for z in range(len(z_position_list)): + absorption_2d_to_3d_transfer_function_out[z] = ( + sampling.nd_fourier_central_cuboid( + absorption_2d_to_3d_transfer_function[z], yx_shape + ) + ) + phase_2d_to_3d_transfer_function_out[z] = ( + sampling.nd_fourier_central_cuboid( + phase_2d_to_3d_transfer_function[z], yx_shape + ) + ) + + return ( + absorption_2d_to_3d_transfer_function_out, + phase_2d_to_3d_transfer_function_out, + ) + + +def _calculate_wrap_unsafe_transfer_function( + yx_shape, + yx_pixel_size, + z_position_list, + wavelength_illumination, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=False, ): if invert_phase_contrast: z_position_list = torch.flip(torch.tensor(z_position_list), dims=(0,)) From 7c7f2a5052fce5982e44eb998834b379ba61894b Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 9 Sep 2024 20:22:49 -0700 Subject: [PATCH 17/72] fix interaction between padding and wrap safety --- waveorder/models/isotropic_fluorescent_thick_3d.py | 4 ++-- waveorder/models/phase_thick_3d.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/waveorder/models/isotropic_fluorescent_thick_3d.py b/waveorder/models/isotropic_fluorescent_thick_3d.py index d67dab8..aebbdf2 100644 --- a/waveorder/models/isotropic_fluorescent_thick_3d.py +++ b/waveorder/models/isotropic_fluorescent_thick_3d.py @@ -57,9 +57,9 @@ def calculate_transfer_function( index_of_refraction_media, numerical_aperture_detection, ) - + zyx_out_shape = (zyx_shape[0] + 2 * z_padding,) + zyx_shape[1:] return sampling.nd_fourier_central_cuboid( - optical_transfer_function, zyx_shape + optical_transfer_function, zyx_out_shape ) diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index 868b15a..8ccf00d 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -73,12 +73,13 @@ def calculate_transfer_function( ) ) + zyx_out_shape = (zyx_shape[0] + 2 * z_padding,) + zyx_shape[1:] return ( sampling.nd_fourier_central_cuboid( - real_potential_transfer_function, zyx_shape + real_potential_transfer_function, zyx_out_shape ), sampling.nd_fourier_central_cuboid( - imag_potential_transfer_function, zyx_shape + imag_potential_transfer_function, zyx_out_shape ), ) From a977bfeba7e5c2d4425bef0721cef39fe9c48bbe Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 11:28:52 -0700 Subject: [PATCH 18/72] clean defaults --- .../models/inplane_oriented_thick_pol3d_vector.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 05c8f1e..969018b 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -2,18 +2,16 @@ import napari from waveorder.models import ( - inplane_oriented_thick_pol3d, inplane_oriented_thick_pol3d_vector, ) # Parameters # all lengths must use consistent units e.g. um -oversample_factor = 2 -zyx_shape = (50, 128, 128) # (100, 256, 256) +zyx_shape = (100, 256, 256) swing = 0.1 scheme = "5-State" -yx_pixel_size = 0.325 / oversample_factor # 0.325 -z_pixel_size = 2.0 / oversample_factor # 2.0 +yx_pixel_size = 0.150 +z_pixel_size = 1.0 wavelength_illumination = 0.532 z_padding = 0 index_of_refraction_media = 1.0 @@ -49,6 +47,9 @@ zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), ) +import pdb + +pdb.set_trace() input("Showing transfer functions. Press to continue...") viewer.layers.select_all() viewer.layers.remove_selected() From 6562ce7389361199bed492e92db956c095d34ef6 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 11:58:00 -0700 Subject: [PATCH 19/72] refactor singular system computation --- .../inplane_oriented_thick_pol3d_vector.py | 11 ++++++---- .../inplane_oriented_thick_pol3d_vector.py | 22 +++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 969018b..8e9311e 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -24,7 +24,7 @@ ) # Calculate transfer function -singular_system, sfZYX_transfer_function, intensity_to_stokes_matrix = ( +sfZYX_transfer_function, intensity_to_stokes_matrix = ( inplane_oriented_thick_pol3d_vector.calculate_transfer_function( swing, scheme, @@ -47,13 +47,16 @@ zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), ) -import pdb - -pdb.set_trace() input("Showing transfer functions. Press to continue...") viewer.layers.select_all() viewer.layers.remove_selected() +singular_system = ( + inplane_oriented_thick_pol3d_vector.calculate_singular_system( + sfZYX_transfer_function + ) +) + # Simulate szyx_data = inplane_oriented_thick_pol3d_vector.apply_transfer_function( fzyx_object, diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index e1d8f78..0a67695 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -1,10 +1,9 @@ -from waveorder import optics, stokes, util -from waveorder.models import inplane_oriented_thick_pol3d -from torch import Tensor - import torch import numpy as np +from torch import Tensor +from waveorder import optics, stokes, util + def generate_test_phantom(zyx_shape): @@ -168,25 +167,26 @@ def calculate_transfer_function( "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) + return ( + sfZYX_transfer_function, + intensity_to_stokes_matrix, + ) + + +def calculate_singular_system(sfZYX_transfer_function): # Compute regularized inverse filter print("Computing SVD") ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) S /= torch.max(S) singular_system = (U, S, Vh) + return singular_system - # transfer function - return ( - singular_system, # (3 stokes, 3 object, Z, Y, X) - sfZYX_transfer_function, - intensity_to_stokes_matrix, - ) def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): shift_dims = (-3, -2, -1) lim = torch.max(torch.abs(sfZYX_transfer_function)) * 0.9 - viewer.add_image( torch.fft.ifftshift( torch.real(sfZYX_transfer_function), dim=shift_dims From 242ceca8a552ec018c6eb3dfefde958a1323d743 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 15:19:00 -0700 Subject: [PATCH 20/72] remove accidental duplicate --- .../models/isotropic_thin_3d_resolution.py | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 examples/models/isotropic_thin_3d_resolution.py diff --git a/examples/models/isotropic_thin_3d_resolution.py b/examples/models/isotropic_thin_3d_resolution.py deleted file mode 100644 index bbe73f0..0000000 --- a/examples/models/isotropic_thin_3d_resolution.py +++ /dev/null @@ -1,88 +0,0 @@ -# 3D partially coherent optical diffraction tomography (ODT) simulation -# J. M. Soto, J. A. Rodrigo, and T. Alieva, "Label-free quantitative -# 3D tomographic imaging for partially coherent light microscopy," Opt. Express -# 25, 15699-15712 (2017) - -import napari -import numpy as np -from waveorder import util -from waveorder.models import isotropic_thin_3d - -# Parameters -# all lengths must use consistent units e.g. um -simulation_arguments = { - "yx_shape": (256, 256), - "yx_pixel_size": 6.5 / 63, - "wavelength_illumination": 0.532, - "index_of_refraction_media": 1.3, -} -phantom_arguments = {"index_of_refraction_sample": 1.4, "sphere_radius": 0.05} -z_shape = 100 -z_pixel_size = 0.25 -transfer_function_arguments = { - "z_position_list": (np.arange(z_shape) - z_shape // 2) * z_pixel_size, - "numerical_aperture_illumination": 0.9, - "numerical_aperture_detection": 1.2, -} - -# Create a phantom -yx_absorption, yx_phase = isotropic_thin_3d.generate_test_phantom( - **simulation_arguments, **phantom_arguments -) - -# Calculate transfer function -( - absorption_2d_to_3d_transfer_function, - phase_2d_to_3d_transfer_function, -) = isotropic_thin_3d.calculate_transfer_function( - **simulation_arguments, **transfer_function_arguments -) - -# Display transfer function -viewer = napari.Viewer() -zyx_scale = np.array( - [ - z_pixel_size, - simulation_arguments["yx_pixel_size"], - simulation_arguments["yx_pixel_size"], - ] -) -isotropic_thin_3d.visualize_transfer_function( - viewer, - absorption_2d_to_3d_transfer_function, - phase_2d_to_3d_transfer_function, -) -input("Showing OTFs. Press to continue...") -viewer.layers.select_all() -viewer.layers.remove_selected() - -# Simulate -zyx_data = isotropic_thin_3d.apply_transfer_function( - yx_absorption, - yx_phase, - absorption_2d_to_3d_transfer_function, - phase_2d_to_3d_transfer_function, -) - -# Reconstruct -( - yx_absorption_recon, - yx_phase_recon, -) = isotropic_thin_3d.apply_inverse_transfer_function( - zyx_data, - absorption_2d_to_3d_transfer_function, - phase_2d_to_3d_transfer_function, -) - -# Display -arrays = [ - (yx_absorption, "Phantom - absorption"), - (yx_phase, "Phantom - phase"), - (zyx_data, "Data"), - (yx_absorption_recon, "Reconstruction - absorption"), - (yx_phase_recon, "Reconstruction - phase"), -] - -for array in arrays: - viewer.add_image(array[0].cpu().numpy(), name=array[1]) -input("Showing object, data, and recon. Press to quit...") From bbe8935c2763ce578f3d300936bc2f09abb16db2 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 15:21:28 -0700 Subject: [PATCH 21/72] refactor visuals --- ...riment_Recon3D_anisotropic_target_small.py | 27 +- .../PTI_Simulation_Forward_2D3D.py | 20 +- .../PTI_simulation/PTI_Simulation_Recon2D.py | 16 +- .../PTI_simulation/PTI_Simulation_Recon3D.py | 22 +- .../QLIPP_simulation/2D_QLIPP_forward.py | 6 +- .../QLIPP_simulation/2D_QLIPP_recon.py | 14 +- examples/models/anisotropic_thick_3d.py | 394 ------------------ .../inplane_oriented_thick_pol3d_vector.py | 49 +-- .../models/isotropic_fluorescent_thick_3d.py | 22 +- waveorder/models/isotropic_thin_3d.py | 4 +- waveorder/models/phase_thick_3d.py | 35 +- .../{visual.py => visuals/jupyter_visuals.py} | 9 +- waveorder/visuals/napari_visuals.py | 49 +++ 13 files changed, 129 insertions(+), 538 deletions(-) delete mode 100644 examples/models/anisotropic_thick_3d.py rename waveorder/{visual.py => visuals/jupyter_visuals.py} (99%) create mode 100644 waveorder/visuals/napari_visuals.py diff --git a/examples/documentation/PTI_experiment/PTI_Experiment_Recon3D_anisotropic_target_small.py b/examples/documentation/PTI_experiment/PTI_Experiment_Recon3D_anisotropic_target_small.py index 39cdf5e..e259dc9 100644 --- a/examples/documentation/PTI_experiment/PTI_Experiment_Recon3D_anisotropic_target_small.py +++ b/examples/documentation/PTI_experiment/PTI_Experiment_Recon3D_anisotropic_target_small.py @@ -6,11 +6,12 @@ from numpy.fft import fftshift import waveorder as wo -from waveorder import optics, waveorder_reconstructor, util, visual +from waveorder import optics, waveorder_reconstructor, util import zarr from pathlib import Path from iohub import open_ome_zarr +from waveorder.visuals import jupyter_visuals # %% # Initialization @@ -110,7 +111,7 @@ Source_PolState[i, 1] = E_in[1] -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( fftshift(Source, axes=(1, 2)), origin="lower", num_col=5 ) @@ -162,7 +163,7 @@ # %% # browse raw intensity stacks (stack_idx_1: z index, stack_idx2: pattern index) -visual.parallel_5D_viewer( +jupyter_visuals.parallel_5D_viewer( np.transpose(I_meas[:, :, :, :, ::-1], (4, 1, 0, 2, 3)), num_col=4, size=10, @@ -171,7 +172,7 @@ # %% # browse uncorrected Stokes parameters (stack_idx_1: z index, stack_idx2: pattern index) -visual.parallel_5D_viewer( +jupyter_visuals.parallel_5D_viewer( np.transpose(S_image_recon, (4, 1, 0, 2, 3)), num_col=3, size=8, @@ -182,7 +183,7 @@ # %% # browse corrected Stokes parameters (stack_idx_1: z index, stack_idx2: pattern index) -visual.parallel_5D_viewer( +jupyter_visuals.parallel_5D_viewer( np.transpose(S_image_tm, (4, 1, 0, 2, 3)), num_col=3, size=8, @@ -213,7 +214,7 @@ # %% # browse the z-stack of components of scattering potential tensor -visual.parallel_4D_viewer( +jupyter_visuals.parallel_4D_viewer( np.transpose(f_tensor, (3, 0, 1, 2)), num_col=4, origin="lower", @@ -278,7 +279,7 @@ # %% # browse the reconstructed physical properties -visual.parallel_4D_viewer( +jupyter_visuals.parallel_4D_viewer( np.transpose( np.stack( [ @@ -546,7 +547,7 @@ # %% # browse XY planes of the phase and differential permittivity -visual.parallel_4D_viewer( +jupyter_visuals.parallel_4D_viewer( np.transpose( [ np.clip(phase_PT, phase_min, phase_max), @@ -585,7 +586,7 @@ ), (3, 1, 2, 0), ) -orientation_3D_image_RGB = visual.orientation_3D_to_rgb( +orientation_3D_image_RGB = jupyter_visuals.orientation_3D_to_rgb( orientation_3D_image, interp_belt=20 / 180 * np.pi, sat_factor=1 ) @@ -600,7 +601,7 @@ # plot the top view of 3D orientation colorsphere plt.figure(figsize=(3, 3)) -visual.orientation_3D_colorwheel( +jupyter_visuals.orientation_3D_colorwheel( wheelsize=256, circ_size=50, interp_belt=20 / 180 * np.pi, sat_factor=1 ) @@ -639,7 +640,7 @@ in_plane_orientation[:, y_layer], origin="lower", aspect=z_step / ps ) plt.figure(figsize=(3, 3)) -visual.orientation_2D_colorwheel() +jupyter_visuals.orientation_2D_colorwheel() # %% # out-of-plane tilt @@ -686,7 +687,7 @@ fig, ax = plt.subplots(1, 1, figsize=(15, 15)) -visual.plot3DVectorField( +jupyter_visuals.plot3DVectorField( np.abs(differential_permittivity_PT[1, :, :, z_layer]), azimuth[1, :, :, z_layer], theta[1, :, :, z_layer], @@ -722,7 +723,7 @@ # %% # Angular histogram of 3D orientation -visual.orientation_3D_hist( +jupyter_visuals.orientation_3D_hist( azimuth[1].flatten(), theta[1].flatten(), ret_mask.flatten(), diff --git a/examples/maintenance/PTI_simulation/PTI_Simulation_Forward_2D3D.py b/examples/maintenance/PTI_simulation/PTI_Simulation_Forward_2D3D.py index ee76e15..1430927 100644 --- a/examples/maintenance/PTI_simulation/PTI_Simulation_Forward_2D3D.py +++ b/examples/maintenance/PTI_simulation/PTI_Simulation_Forward_2D3D.py @@ -15,9 +15,9 @@ from waveorder import ( optics, waveorder_simulator, - visual, util, ) +from waveorder.visuals import jupyter_visuals ##################################################################### # Initialization - imaging system and sample # @@ -145,7 +145,7 @@ ### Visualize sample properties #### XY sections -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( [ target[:, :, z_layer], azimuth[:, :, z_layer] % (2 * np.pi), @@ -158,7 +158,7 @@ set_title=True, ) #### XZ sections -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( [ np.transpose(target[y_layer, :, :]), np.transpose(azimuth[y_layer, :, :]) % (2 * np.pi), @@ -197,7 +197,7 @@ ), (3, 1, 2, 0), ) -orientation_3D_image_RGB = visual.orientation_3D_to_rgb( +orientation_3D_image_RGB = jupyter_visuals.orientation_3D_to_rgb( orientation_3D_image, interp_belt=20 / 180 * np.pi, sat_factor=1 ) @@ -206,7 +206,7 @@ plt.figure(figsize=(10, 10)) plt.imshow(orientation_3D_image_RGB[:, y_layer], origin="lower") plt.figure(figsize=(3, 3)) -visual.orientation_3D_colorwheel( +jupyter_visuals.orientation_3D_colorwheel( wheelsize=128, circ_size=50, interp_belt=20 / 180 * np.pi, @@ -216,7 +216,7 @@ plt.show() #### Angular histogram of 3D orientation -visual.orientation_3D_hist( +jupyter_visuals.orientation_3D_hist( azimuth.flatten(), inclination.flatten(), np.abs(target).flatten(), @@ -258,7 +258,7 @@ epsilon_tensor[2, 2] = epsilon_mean + epsilon_del * np.cos(2 * inclination) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( [ epsilon_tensor[0, 0, :, :, z_layer], epsilon_tensor[0, 1, :, :, z_layer], @@ -334,7 +334,7 @@ ) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( [ del_f_component[0, :, :, z_layer], del_f_component[1, :, :, z_layer], @@ -425,11 +425,11 @@ #### Circularly polarized illumination patterns -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( fftshift(Source_cont, axes=(1, 2)), origin="lower", num_col=5, size=5 ) # discretized illumination patterns used in simulation (faster forward model) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( fftshift(Source, axes=(1, 2)), origin="lower", num_col=5, size=5 ) print(Source_PolState) diff --git a/examples/maintenance/PTI_simulation/PTI_Simulation_Recon2D.py b/examples/maintenance/PTI_simulation/PTI_Simulation_Recon2D.py index 9fb4518..0042a1a 100644 --- a/examples/maintenance/PTI_simulation/PTI_Simulation_Recon2D.py +++ b/examples/maintenance/PTI_simulation/PTI_Simulation_Recon2D.py @@ -16,8 +16,8 @@ from waveorder import ( optics, waveorder_reconstructor, - visual, ) +from waveorder.visuals import jupyter_visuals ## Initialization ## Load simulated images and parameters @@ -76,7 +76,7 @@ ## Visualize 2 D transfer functions as a function of illumination pattern # illumination patterns used -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( fftshift(Source_cont, axes=(1, 2)), origin="lower", num_col=5, size=5 ) plt.show() @@ -118,7 +118,7 @@ S_image_tm, reg_inc=reg_inc, cupy_det=True ) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( f_tensor, num_col=4, origin="lower", @@ -255,14 +255,14 @@ ), (1, 2, 0), ) -orientation_3D_image_RGB = visual.orientation_3D_to_rgb( +orientation_3D_image_RGB = jupyter_visuals.orientation_3D_to_rgb( orientation_3D_image, interp_belt=20 / 180 * np.pi, sat_factor=1 ) plt.figure(figsize=(5, 5)) plt.imshow(orientation_3D_image_RGB, origin="lower") plt.figure(figsize=(3, 3)) -visual.orientation_3D_colorwheel( +jupyter_visuals.orientation_3D_colorwheel( wheelsize=256, circ_size=50, interp_belt=20 / 180 * np.pi, sat_factor=1 ) plt.show() @@ -297,7 +297,7 @@ plt.figure(figsize=(5, 5)) plt.imshow(in_plane_orientation, origin="lower") plt.figure(figsize=(3, 3)) -visual.orientation_2D_colorwheel() +jupyter_visuals.orientation_2D_colorwheel() plt.show() # out-of-plane tilt @@ -339,7 +339,7 @@ plt.figure(figsize=(10, 10)) fig, ax = plt.subplots(1, 1, figsize=(20, 10)) -visual.plot3DVectorField( +jupyter_visuals.plot3DVectorField( np.abs(retardance_pr_nm[0]), azimuth[0], theta[0], @@ -362,7 +362,7 @@ plt.figure(figsize=(10, 10)) plt.imshow(ret_mask, cmap="gray", origin="lower") -visual.orientation_3D_hist( +jupyter_visuals.orientation_3D_hist( azimuth[0].flatten(), theta[0].flatten(), ret_mask.flatten(), diff --git a/examples/maintenance/PTI_simulation/PTI_Simulation_Recon3D.py b/examples/maintenance/PTI_simulation/PTI_Simulation_Recon3D.py index e1edbb3..78f0c54 100644 --- a/examples/maintenance/PTI_simulation/PTI_Simulation_Recon3D.py +++ b/examples/maintenance/PTI_simulation/PTI_Simulation_Recon3D.py @@ -14,8 +14,8 @@ from waveorder import ( optics, waveorder_reconstructor, - visual, ) +from waveorder.visuals import jupyter_visuals ## Initialization ## Load simulated images and parameters @@ -65,7 +65,7 @@ ### Illumination patterns used -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( fftshift(Source_cont, axes=(1, 2)), origin="lower", num_col=5, size=5 ) plt.show() @@ -113,7 +113,7 @@ S_image_tm, reg_inc=reg_inc, cupy_det=True ) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( f_tensor[..., L // 2], num_col=4, origin="lower", @@ -183,7 +183,7 @@ ### Reconstructed phase, absorption, principal retardance, azimuth, and inclination assuming (+) and (-) optic sign # browse the reconstructed physical properties -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( np.stack( [ phase_PT[..., L // 2], @@ -389,7 +389,7 @@ ), (3, 1, 2, 0), ) -orientation_3D_image_RGB = visual.orientation_3D_to_rgb( +orientation_3D_image_RGB = jupyter_visuals.orientation_3D_to_rgb( orientation_3D_image, interp_belt=20 / 180 * np.pi, sat_factor=1 ) @@ -402,7 +402,7 @@ ) # plot the top view of 3D orientation colorsphere plt.figure(figsize=(3, 3)) -visual.orientation_3D_colorwheel( +jupyter_visuals.orientation_3D_colorwheel( wheelsize=128, circ_size=50, interp_belt=20 / 180 * np.pi, @@ -440,7 +440,7 @@ plt.figure(figsize=(10, 10)) plt.imshow(in_plane_orientation[:, y_layer], origin="lower", aspect=psz / ps) plt.figure(figsize=(3, 3)) -visual.orientation_2D_colorwheel() +jupyter_visuals.orientation_2D_colorwheel() plt.show() @@ -511,7 +511,7 @@ fig, ax = plt.subplots(2, 2, figsize=(10, 10)) -visual.plot3DVectorField( +jupyter_visuals.plot3DVectorField( np.abs(retardance_pr_PT[0, :, :, z_layer]), azimuth[0, :, :, z_layer], theta[0, :, :, z_layer], @@ -529,7 +529,7 @@ ) ax[0, 0].set_title(f"XY section (z= {z_layer})") -visual.plot3DVectorField( +jupyter_visuals.plot3DVectorField( np.transpose(np.abs(retardance_pr_PT[0, :, x_layer, :])), np.transpose(azimuth_x[0, :, x_layer, :]), np.transpose(theta_x[0, :, x_layer, :]), @@ -547,7 +547,7 @@ ) ax[0, 1].set_title(f"YZ section (x = {x_layer})") -visual.plot3DVectorField( +jupyter_visuals.plot3DVectorField( np.transpose(np.abs(retardance_pr_PT[0, y_layer, :, :])), np.transpose(azimuth_y[0, y_layer, :, :]), np.transpose(theta_y[0, y_layer, :, :]), @@ -584,7 +584,7 @@ plt.figure(figsize=(10, 10)) plt.imshow(ret_mask[:, :, z_layer], cmap="gray", origin="lower") -visual.orientation_3D_hist( +jupyter_visuals.orientation_3D_hist( azimuth[0].flatten(), theta[0].flatten(), ret_mask.flatten(), diff --git a/examples/maintenance/QLIPP_simulation/2D_QLIPP_forward.py b/examples/maintenance/QLIPP_simulation/2D_QLIPP_forward.py index 9aed8af..3a7a5d3 100644 --- a/examples/maintenance/QLIPP_simulation/2D_QLIPP_forward.py +++ b/examples/maintenance/QLIPP_simulation/2D_QLIPP_forward.py @@ -16,9 +16,9 @@ from waveorder import ( optics, waveorder_simulator, - visual, util, ) +from waveorder.visuals import jupyter_visuals # Key parameters N = 256 # number of pixel in y dimension @@ -38,7 +38,7 @@ star, theta, _ = util.generate_star_target((N, M)) star = star.numpy() theta = theta.numpy() -visual.plot_multicolumn(np.array([star, theta]), num_col=2, size=5) +jupyter_visuals.plot_multicolumn(np.array([star, theta]), num_col=2, size=5) # Assign uniform phase, uniform retardance, and radial slow axes to the star pattern phase_value = 1 # average phase in radians (optical path length) @@ -50,7 +50,7 @@ t_eigen[0] = np.exp(-mu_s + 1j * phi_s) t_eigen[1] = np.exp(-mu_f + 1j * phi_f) sa = theta % np.pi # slow axes. -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( np.array([phi_s, phi_f, mu_s, sa]), num_col=2, size=5, diff --git a/examples/maintenance/QLIPP_simulation/2D_QLIPP_recon.py b/examples/maintenance/QLIPP_simulation/2D_QLIPP_recon.py index 33a83b0..742fb49 100644 --- a/examples/maintenance/QLIPP_simulation/2D_QLIPP_recon.py +++ b/examples/maintenance/QLIPP_simulation/2D_QLIPP_recon.py @@ -14,8 +14,8 @@ import matplotlib.pyplot as plt from waveorder import ( waveorder_reconstructor, - visual, ) +from waveorder.visuals import jupyter_visuals # ### Load simulated data @@ -57,7 +57,7 @@ S_image_tm ) # Without accounting for diffraction -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( np.array( [ Recon_para[0, :, :, L // 2], @@ -73,7 +73,7 @@ origin="lower", ) -visual.plot_hsv( +jupyter_visuals.plot_hsv( [Recon_para[1, :, :, L // 2], Recon_para[0, :, :, L // 2]], max_val=1, origin="lower", @@ -90,7 +90,7 @@ S1_stack, S2_stack, method="Tikhonov", reg_br=1e-3 ) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( np.array([retardance, azimuth]), num_col=2, size=10, @@ -98,7 +98,7 @@ titles=["Reconstructed retardance", "Reconstructed orientation"], origin="lower", ) -visual.plot_hsv([azimuth, retardance], size=10, origin="lower") +jupyter_visuals.plot_hsv([azimuth, retardance], size=10, origin="lower") plt.show() @@ -114,7 +114,7 @@ verbose=True, ) -visual.plot_multicolumn( +jupyter_visuals.plot_multicolumn( np.array([retardance_TV, azimuth_TV]), num_col=2, size=10, @@ -122,7 +122,7 @@ titles=["Reconstructed retardance", "Reconstructed orientation"], origin="lower", ) -visual.plot_hsv([azimuth_TV, retardance_TV], size=10, origin="lower") +jupyter_visuals.plot_hsv([azimuth_TV, retardance_TV], size=10, origin="lower") plt.show() diff --git a/examples/models/anisotropic_thick_3d.py b/examples/models/anisotropic_thick_3d.py deleted file mode 100644 index f7d669b..0000000 --- a/examples/models/anisotropic_thick_3d.py +++ /dev/null @@ -1,394 +0,0 @@ -# WIP for the most general case -# %% - -import torch -import napari -import numpy as np -from waveorder import optics, util - -# Parameters -# all lengths use consistent units e.g. um -margin = 50 -simulation_arguments = { - "zyx_shape": (129, 256, 256), - "yx_pixel_size": 6.5 / 65, - "z_pixel_size": 0.1, - "index_of_refraction_media": 1.25, -} -transfer_function_arguments = { - "z_padding": 0, - "wavelength_illumination": 0.5, - "numerical_aperture_illumination": 0.75, - "numerical_aperture_detection": 1.0, -} -input_jones = torch.tensor([0.0 + 1.0j, 1.0 + 0j]) - -# Convert -zyx_shape = simulation_arguments["zyx_shape"] -yx_pixel_size = simulation_arguments["yx_pixel_size"] -z_pixel_size = simulation_arguments["z_pixel_size"] -index_of_refraction_media = simulation_arguments["index_of_refraction_media"] -z_padding = transfer_function_arguments["z_padding"] -wavelength_illumination = transfer_function_arguments[ - "wavelength_illumination" -] -numerical_aperture_illumination = transfer_function_arguments[ - "numerical_aperture_illumination" -] -numerical_aperture_detection = transfer_function_arguments[ - "numerical_aperture_detection" -] - -# Precalculations -z_total = zyx_shape[0] + 2 * z_padding -z_position_list = torch.fft.ifftshift( - (torch.arange(z_total) - z_total // 2) * z_pixel_size -) - -# Calculate frequencies -y_frequencies, x_frequencies = util.generate_frequencies( - zyx_shape[1:], yx_pixel_size -) -radial_frequencies = np.sqrt(x_frequencies**2 + y_frequencies**2) - -# 2D pupils -ill_pupil = optics.generate_pupil( - radial_frequencies, - numerical_aperture_illumination, - wavelength_illumination, -) -det_pupil = optics.generate_pupil( - radial_frequencies, - numerical_aperture_detection, - wavelength_illumination, -) -pupil = optics.generate_pupil( - radial_frequencies, - index_of_refraction_media, # largest possible NA - wavelength_illumination, -) - -# Defocus pupils -defocus_pupil = optics.generate_propagation_kernel( - radial_frequencies, - pupil, - wavelength_illumination / index_of_refraction_media, - z_position_list, -) - -greens_functions_z = optics.generate_greens_function_z( - radial_frequencies, - pupil, - wavelength_illumination / index_of_refraction_media, - z_position_list, -) - -# Calculate vector defocus pupils -S = optics.generate_vector_source_defocus_pupil( - x_frequencies, - y_frequencies, - z_position_list, - defocus_pupil, - input_jones, - ill_pupil, - wavelength_illumination / index_of_refraction_media, -) - -# Simplified scalar pupil -sP = optics.generate_propagation_kernel( - radial_frequencies, - det_pupil, - wavelength_illumination / index_of_refraction_media, - z_position_list, -) - -P = optics.generate_vector_detection_defocus_pupil( - x_frequencies, - y_frequencies, - z_position_list, - defocus_pupil, - det_pupil, - wavelength_illumination / index_of_refraction_media, -) - -G = optics.generate_defocus_greens_tensor( - x_frequencies, - y_frequencies, - greens_functions_z, - pupil, - lambda_in=wavelength_illumination / index_of_refraction_media, -) - -# %% -# MAY 12 Simplified -P_3D = torch.abs(torch.fft.ifft(sP, dim=-3)).type(torch.complex64) -G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) -S_3D = torch.fft.ifft(S, dim=-3) - -# Normalize -P_3D /= torch.amax(torch.abs(P_3D)) -G_3D /= torch.amax(torch.abs(G_3D)) -S_3D /= torch.amax(torch.abs(S_3D)) - -# Main part -PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) -PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) - -PG_3D /= torch.amax(torch.abs(PG_3D)) -PS_3D /= torch.amax(torch.abs(PS_3D)) - -pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) -ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) - -H1 = torch.fft.ifftn( - torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), - dim=(-3, -2, -1), -) - -H2 = torch.fft.ifftn( - torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), - dim=(-3, -2, -1), -) - -H_re = H1[1:, 1:] + H2[1:, 1:] -# H_im = 1j * (H1 - H2) - -s = util.pauli() -Y = util.gellmann() - -H_re_stokes = torch.einsum("sik,ikpjzyx,lpj->slzyx", s, H_re, Y) - -print("H_re_stokes: (RE, IM, ABS)") -torch.set_printoptions(precision=1) -print(torch.amax(torch.real(H_re_stokes), dim=(-3, -2, -1))) -print(torch.amax(torch.imag(H_re_stokes), dim=(-3, -2, -1))) -print(torch.amax(torch.abs(H_re_stokes), dim=(-3, -2, -1))) - -# Display transfer function -v = napari.Viewer() - - -def view_transfer_function( - transfer_function, -): - shift_dims = (-3, -2, -1) - lim = 1e-3 - zyx_scale = np.array( - [ - zyx_shape[0] * z_pixel_size, - zyx_shape[1] * yx_pixel_size, - zyx_shape[2] * yx_pixel_size, - ] - ) - - v.add_image( - torch.fft.ifftshift(torch.real(transfer_function), dim=shift_dims) - .cpu() - .numpy(), - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 / zyx_scale, - ) - if transfer_function.dtype == torch.complex64: - v.add_image( - torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) - .cpu() - .numpy(), - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 / zyx_scale, - ) - - # v.dims.order = (2, 1, 0) - - -view_transfer_function(H_re_stokes) -# view_transfer_function(G_3D) -# view_transfer_function(H_re) -# view_transfer_function(P_3D) -import pdb; pdb.set_trace() -# %% -# PLOT transfer function -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.colors as mcolors - - -def plot_data(data, y_slices, filename): - fig, axs = plt.subplots( - 4, 9, figsize=(20, 10) - ) # Adjust the size as needed - - for i in range(data.shape[0]): # Stokes parameter - for j in range(data.shape[1]): # Object parameter - for k, y in enumerate(y_slices): # Y slices - z = data[i, j, :, y, :] - hue = ( - np.angle(z) / (2 * np.pi) + 0.5 - ) # Normalize and shift to make red at 0 - sat = np.abs(z) / np.amax(np.abs(z)) - hsv = np.stack((hue, sat, np.ones_like(sat)), axis=-1) - rgb = mcolors.hsv_to_rgb(hsv) - - ax = axs[i, j] - ax.imshow(rgb, aspect="auto") - ax.set_title("") # Remove titles - ax.set_xticks([]) # Remove x-axis ticks - ax.set_yticks([]) # Remove y-axis ticks - ax.spines["top"].set_visible(False) # Hide top spine - ax.spines["right"].set_visible(False) # Hide right spine - ax.spines["bottom"].set_visible(False) # Hide bottom spine - ax.spines["left"].set_visible(False) # Hide left spine - ax.set_xlabel("") # Remove x-axis labels - - plt.tight_layout() - plt.savefig(filename, format="pdf") - - -# Adjust y_slices according to your index base (check if your array index starts at 0) -y_center = 128 # Assuming the middle index for Y dimension -y_slices = [y_center, y_center, y_center] -# plot_data( -# torch.fft.ifftshift(H_re_stokes, dim=(-3, -2, -1)).numpy(), -# y_slices, -# "./output.pdf", -# ) - -# Simulate -yx_star, yx_theta, _ = util.generate_star_target( - yx_shape=zyx_shape[1:], - blur_px=1, - margin=margin, -) -c00 = yx_star -c2_2 = -torch.sin(2 * yx_theta) * yx_star -c22 = torch.cos(2 * yx_theta) * yx_star - -# Put in in a center slices of a 3D object -center_slice_object = torch.stack((c00, c2_2, c22), dim=0) -object = torch.zeros((3,) + zyx_shape) -object[:, zyx_shape[0] // 2, ...] = center_slice_object - -# Simulate -H = H_re_stokes[:, (0, 4, 8), ...] # for transverse linear birefringence -object_spectrum = torch.fft.fftn(object, dim=(-3, -2, -1)) -data_spectrum = torch.einsum("slzyx,lzyx->szyx", H, object_spectrum) -data = torch.fft.ifftn(data_spectrum, dim=(-3, -2, -1)) - -# Simple measurement space -Hsvd = torch.movedim(H, (0, 1), (-2, -1)) -# Computing SVD...can simplify this -print("Calculating SVD") -_, Ssvd, _ = torch.linalg.svd(Hsvd, full_matrices=False) -Hinv = torch.linalg.pinv(Hsvd) - -S_trunc = Ssvd > 5 # cutoff small singular values -recon_spectrum = torch.einsum( - "zyxl,zyxls,szyx->lzyx", S_trunc, Hinv, data_spectrum -) -recon_object = torch.fft.ifftn(recon_spectrum, dim=(-3, -2, -1)) - -# Tikhonov-regularized reconstruction (aka project onto measurement space) -# print("Computing SVD") - -# # Correct reconstruction -# reg = 1 -# S_reg = S / (S**2 + reg**2) -# # recon_object_spectrum = torch.einsum("jkzyx,jzyx,ijzyx,izyx->kzyx", Vh, S_reg, torch.conj(U), data_spectrum) -# recon_object_spectrum = torch.einsum( -# "kjzyx,jzyx,jizyx,izyx->kzyx", U, S_reg, Vh, data_spectrum -# ) - -# recon_object = torch.fft.ifftn(recon_object_spectrum, dim=(-3, -2, -1)) - - -# v.add_image(object.numpy()) -# v.add_image(torch.real(data).numpy()) -# v.add_image(torch.real(recon_object).numpy()) -# v.add_image(torch.imag(recon_object).numpy()) - -# %% -# Plot -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec - - -def plot_3d_array_slices(data, z_shift=0, filename="output.pdf"): - # Dimensions of the data - z_dim, y_dim, x_dim = data.shape - - vmax = np.max(data) - vmin = np.min(data) - - # Create the figure and define the subplots using GridSpec - fig = plt.figure(figsize=(10, 10)) # Adjust the overall size as necessary - gs = gridspec.GridSpec( - 2, 2, height_ratios=[y_dim, z_dim], width_ratios=[x_dim, z_dim] - ) - - # XY view (Z slice through the middle) - ax1 = fig.add_subplot(gs[0, 0]) - xy_slice = np.copy(data[z_dim // 2 - z_shift, :, :]) - - ax1.imshow( - xy_slice, - origin="upper", - cmap="gray", - extent=[0, x_dim, y_dim, 0], - vmin=vmin, - vmax=vmax, - ) - ax1.axis("off") - - # YZ view (X slice through the middle) - need to transpose and adjust extent - ax2 = fig.add_subplot(gs[0, 1]) - yz_slice = np.flip(data[:, :, x_dim // 2].T, axis=1) - ax2.imshow( - yz_slice, - origin="upper", - cmap="gray", - extent=[0, z_dim, y_dim, 0], - vmin=vmin, - vmax=vmax, - ) - ax2.axis("off") - - # XZ view (Y slice through the middle) - need to transpose and adjust extent - ax3 = fig.add_subplot(gs[1, 0]) - xz_slice = data[:, y_dim // 2, :] - ax3.imshow( - xz_slice, - origin="lower", - cmap="gray", - extent=[0, x_dim, z_dim, 0], - vmin=vmin, - vmax=vmax, - ) - ax3.axis("off") - - # Adjust layout - plt.tight_layout() - plt.savefig(filename, format="pdf") - plt.close(fig) - - -for z_shift in [0, -2, 2]: - for i, plot_data in enumerate(object): - plot_3d_array_slices( - torch.real(plot_data).numpy(), z_shift, f"object{i}{z_shift}.pdf" - ) - - for i, plot_data in enumerate(data): - plot_3d_array_slices( - torch.real(plot_data).numpy(), z_shift, f"data{i}{z_shift}.pdf" - ) - - for i, plot_data in enumerate(recon_object): - plot_3d_array_slices( - torch.real(plot_data).numpy(), z_shift, f"recon{i}{z_shift}.pdf" - ) - -import pdb - -pdb.set_trace() diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 0a67695..874afac 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -3,6 +3,7 @@ from torch import Tensor from waveorder import optics, stokes, util +from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer def generate_test_phantom(zyx_shape): @@ -107,17 +108,6 @@ def calculate_transfer_function( z_position_list, ) - # TODO consider testing this instead of sP - """ - P = optics.generate_vector_detection_defocus_pupil( - x_frequencies, - y_frequencies, - z_position_list, - defocus_pupil, - det_pupil, - wavelength_illumination / index_of_refraction_media, - ) - """ G = optics.generate_defocus_greens_tensor( x_frequencies, @@ -184,40 +174,13 @@ def calculate_singular_system(sfZYX_transfer_function): def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): - shift_dims = (-3, -2, -1) - lim = torch.max(torch.abs(sfZYX_transfer_function)) * 0.9 - - viewer.add_image( - torch.fft.ifftshift( - torch.real(sfZYX_transfer_function), dim=shift_dims - ) - .cpu() - .numpy(), - name="Real. TF", - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 - / (np.array(zyx_scale) * np.array(sfZYX_transfer_function.shape[-3:])), - ) - - viewer.add_image( - torch.fft.ifftshift( - torch.imag(sfZYX_transfer_function), dim=shift_dims - ) - .cpu() - .numpy(), - name="Imag. TF", - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 - / (np.array(zyx_scale) * np.array(sfZYX_transfer_function.shape[-3:])), + add_transfer_function_to_viewer( + viewer, + sfZYX_transfer_function, + zyx_scale=zyx_scale, + layer_name="Transfer Function", ) - _, _, Z, Y, X = sfZYX_transfer_function.shape - viewer.dims.current_step = (0, 0, Z // 2, Y // 2, X // 2) - viewer.dims.order = (4, 0, 1, 2, 3) - - def apply_transfer_function( fzyx_object, sfZYX_transfer_function, diff --git a/waveorder/models/isotropic_fluorescent_thick_3d.py b/waveorder/models/isotropic_fluorescent_thick_3d.py index a264738..6c78f3d 100644 --- a/waveorder/models/isotropic_fluorescent_thick_3d.py +++ b/waveorder/models/isotropic_fluorescent_thick_3d.py @@ -5,6 +5,7 @@ from torch import Tensor from waveorder import optics, sampling, util +from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer def generate_test_phantom( @@ -108,24 +109,9 @@ def _calculate_wrap_unsafe_transfer_function( def visualize_transfer_function(viewer, optical_transfer_function, zyx_scale): - arrays = [ - (torch.imag(optical_transfer_function), "Im(OTF)"), - (torch.real(optical_transfer_function), "Re(OTF)"), - ] - - for array in arrays: - lim = 0.1 * torch.max(torch.abs(array[0])) - viewer.add_image( - torch.fft.ifftshift(array[0]).cpu().numpy(), - name=array[1], - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1/(np.array(zyx_scale) * np.array(optical_transfer_function.shape[-3:])), - ) - - Z, Y, X = optical_transfer_function.shape - viewer.dims.current_step = (Z // 2, Y // 2, X // 2) - viewer.dims.order = (2, 0, 1) + add_transfer_function_to_viewer( + viewer, torch.real(optical_transfer_function), zyx_scale, clim_factor=0.05 + ) def apply_transfer_function( diff --git a/waveorder/models/isotropic_thin_3d.py b/waveorder/models/isotropic_thin_3d.py index 1af9ed8..e96a499 100644 --- a/waveorder/models/isotropic_thin_3d.py +++ b/waveorder/models/isotropic_thin_3d.py @@ -5,6 +5,7 @@ from torch import Tensor from waveorder import optics, sampling, util +from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer def generate_test_phantom( @@ -152,7 +153,6 @@ def visualize_transfer_function( absorption_2d_to_3d_transfer_function, phase_2d_to_3d_transfer_function, ): - # TODO: consider generalizing w/ phase_thick_3d.visualize_transfer_function arrays = [ (torch.imag(absorption_2d_to_3d_transfer_function), "Im(absorb TF)"), (torch.real(absorption_2d_to_3d_transfer_function), "Re(absorb TF)"), @@ -169,7 +169,7 @@ def visualize_transfer_function( contrast_limits=(-lim, lim), scale=(1, 1, 1), ) - viewer.dims.order = (0, 1, 2) + viewer.dims.order = (2, 0, 1) def visualize_point_spread_function( diff --git a/waveorder/models/phase_thick_3d.py b/waveorder/models/phase_thick_3d.py index 64849a6..ec6df00 100644 --- a/waveorder/models/phase_thick_3d.py +++ b/waveorder/models/phase_thick_3d.py @@ -6,6 +6,7 @@ from waveorder import optics, sampling, util from waveorder.models import isotropic_fluorescent_thick_3d +from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer def generate_test_phantom( @@ -150,29 +151,19 @@ def visualize_transfer_function( imag_potential_transfer_function, zyx_scale, ): - # TODO: consider generalizing w/ phase2Dto3D.visualize_TF - arrays = [ - (torch.real(imag_potential_transfer_function), "Re(imag pot. TF)"), - (torch.imag(imag_potential_transfer_function), "Im(imag pot. TF)"), - (torch.real(real_potential_transfer_function), "Re(real pot. TF)"), - (torch.imag(real_potential_transfer_function), "Im(real pot. TF)"), - ] - - for array in arrays: - lim = 0.5 * torch.max(torch.abs(array[0])) - viewer.add_image( - torch.fft.ifftshift(array[0]).cpu().numpy(), - name=array[1], - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 - / (np.array(zyx_scale) * np.array(real_potential_transfer_function.shape[-3:])), - ) - Z, Y, X = real_potential_transfer_function.shape - viewer.dims.current_step = (Z // 2, Y // 2, X // 2) - viewer.dims.order = (2, 0, 1) + add_transfer_function_to_viewer( + viewer, + imag_potential_transfer_function, + zyx_scale, + layer_name="Imag pot. TF", + ) - + add_transfer_function_to_viewer( + viewer, + real_potential_transfer_function, + zyx_scale, + layer_name="Real pot. TF", + ) def apply_transfer_function( diff --git a/waveorder/visual.py b/waveorder/visuals/jupyter_visuals.py similarity index 99% rename from waveorder/visual.py rename to waveorder/visuals/jupyter_visuals.py index ba86db7..bc41d7a 100644 --- a/waveorder/visual.py +++ b/waveorder/visuals/jupyter_visuals.py @@ -8,11 +8,7 @@ Image, Layout, interact, - interactive, - fixed, - interact_manual, HBox, - VBox, ) from matplotlib.colors import hsv_to_rgb from matplotlib.colors import Normalize @@ -176,8 +172,7 @@ def image_stack_viewer_fast( else: raise ValueError('origin can only be either "upper" or "lower"') - im_wgt = Image( - value=im_dict[0], + im_wgt = Image( value=im_dict[0], layout=Layout(height=str(size[0]) + "px", width=str(size[1]) + "px"), ) @@ -1928,4 +1923,4 @@ def orientation_3D_hist( if colorbar: fig.colorbar(img, ax=ax[row_idx, col_idx]) - return fig, ax \ No newline at end of file + return fig, ax diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py new file mode 100644 index 0000000..cc094d6 --- /dev/null +++ b/waveorder/visuals/napari_visuals.py @@ -0,0 +1,49 @@ +import napari +import numpy as np +import torch + + +def add_transfer_function_to_viewer( + viewer: napari.Viewer, + transfer_function: torch.Tensor, + zyx_scale: tuple[float, float, float], + layer_name: str = "Transfer Function", + clim_factor: float = 1.0, +): + zyx_shape = transfer_function.shape[-3:] + lim = torch.max(torch.abs(transfer_function))*clim_factor + voxel_scale = np.array( + [ + zyx_shape[0] * zyx_scale[0], + zyx_shape[1] * zyx_scale[1], + zyx_shape[2] * zyx_scale[2], + ] + ) + shift_dims = (-3, -2, -1) + + viewer.add_image( + torch.fft.ifftshift(torch.real(transfer_function), dim=shift_dims) + .cpu() + .numpy(), + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 / voxel_scale, + name="Re(" + layer_name + ")", + ) + if transfer_function.dtype == torch.complex64: + viewer.add_image( + torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) + .cpu() + .numpy(), + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 / voxel_scale, + name="Im(" + layer_name + ")", + ) + + viewer.dims.current_step = (0,)*(transfer_function.ndim - 3) + ( + zyx_shape[0] // 2, + zyx_shape[1] // 2, + zyx_shape[2] // 2, + ) + viewer.dims.order = (2, 0, 1) From ca4e7c597dcf880d0c2d2b7859ed0e685522308c Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 15:37:13 -0700 Subject: [PATCH 22/72] fix warnings from tensoring a tensor --- tests/test_optics.py | 2 +- waveorder/optics.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_optics.py b/tests/test_optics.py index 2d03dc2..4e82177 100644 --- a/tests/test_optics.py +++ b/tests/test_optics.py @@ -31,7 +31,7 @@ def test_generate_propagation_kernel(): def test_gen_Greens_function_z(): wavelength = 0.5 - z_position_list = [0, 1, -1] # note fftfreq coords + z_position_list = torch.tensor([0, 1, -1]) # note fftfreq coords radial_frequencies = util.generate_radial_frequencies((10, 10), 0.5) pupil = optics.generate_pupil(radial_frequencies, 0.5, 0.5) diff --git a/waveorder/optics.py b/waveorder/optics.py index 65ac130..1a7bab6 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -395,7 +395,7 @@ def generate_propagation_kernel( wavelength : float wavelength of the light in the immersion media - z_position_list : torch.tensor or list + z_position_list : torch.tensor 1D array of defocused z positions with the size of (Z) Returns @@ -414,7 +414,7 @@ def generate_propagation_kernel( 1j * 2 * np.pi - * torch.tensor(z_position_list)[:, None, None] + * z_position_list[:, None, None] * oblique_factor[None, :, :] ) @@ -440,7 +440,7 @@ def generate_greens_function_z( wavelength_illumination : float wavelength of the light in the immersion media - z_position_list : torch.tensor or list + z_position_list : torch.tensor 1D array of defocused z position with the size of (Z,) axially_even : bool @@ -461,9 +461,9 @@ def generate_greens_function_z( ) ** (1 / 2) / wavelength_illumination if axially_even: - z_positions = torch.abs(torch.tensor(z_position_list)[:, None, None]) + z_positions = torch.abs(z_position_list[:, None, None]) else: - z_positions = torch.tensor(z_position_list)[:, None, None] + z_positions = z_position_list[:, None, None] greens_function_z = ( -1j From 27450052781dbddd2e2d9bd06186130e2b5b5503 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 15:37:22 -0700 Subject: [PATCH 23/72] match defaults --- examples/models/inplane_oriented_thick_pol3d_vector.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 8e9311e..4cb4e74 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -10,13 +10,13 @@ zyx_shape = (100, 256, 256) swing = 0.1 scheme = "5-State" -yx_pixel_size = 0.150 -z_pixel_size = 1.0 +yx_pixel_size = 2 * 6.5 / 63 +z_pixel_size = 0.25 wavelength_illumination = 0.532 z_padding = 0 -index_of_refraction_media = 1.0 -numerical_aperture_illumination = 0.4 -numerical_aperture_detection = 0.55 +index_of_refraction_media = 1.3 +numerical_aperture_illumination = 0.9 +numerical_aperture_detection = 1.2 # Create a phantom fzyx_object = inplane_oriented_thick_pol3d_vector.generate_test_phantom( From 6143b6ad57c83cdd87c3286f4cfa955efa94ab9e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 15:37:28 -0700 Subject: [PATCH 24/72] readme type --- examples/models/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/models/README.md b/examples/models/README.md index a6ba8bf..7a486d9 100644 --- a/examples/models/README.md +++ b/examples/models/README.md @@ -1,9 +1,9 @@ -This folder of `models` examples consists of a set of simulations that are begin actively developed and will run as is. +This folder of `models` examples consists of a set of simulations that are being actively developed and will run as is. Each model is named with the schema `__`. For example, `isotropic_thin_3d.py` demonstrates a set of methods for simulating and reconstructing - an isotropic object (i.e. an attenuating-phase object that has no diattenuation or birefringence) that is - thin compared to the depth of field of the microscope from -- three-dimensional data (a defocus stack). +- three-dimensional data (a defocus stack). From f8d6b0c36166d224109bdbeaaa6dfcf3c348f7fe Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 16:01:57 -0700 Subject: [PATCH 25/72] handle napari dependency --- examples/models/README.md | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/models/README.md b/examples/models/README.md index 7a486d9..c570f3d 100644 --- a/examples/models/README.md +++ b/examples/models/README.md @@ -1,3 +1,5 @@ +Run `pip install napari[all]` before running these examples. + This folder of `models` examples consists of a set of simulations that are being actively developed and will run as is. Each model is named with the schema `__`. diff --git a/pyproject.toml b/pyproject.toml index 30dcade..0c4d14b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -dev = ["pytest", "pytest-cov"] +dev = ["pytest", "pytest-cov", "napari[all]"] [project.urls] Homepage = "https://github.com/mehta-lab/waveorder" From 790df573e82311226ca25f7c547c96a0d8fa7175 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 16:12:09 -0700 Subject: [PATCH 26/72] Revert "fix warnings from tensoring a tensor" This reverts commit ca4e7c597dcf880d0c2d2b7859ed0e685522308c. --- tests/test_optics.py | 2 +- waveorder/optics.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_optics.py b/tests/test_optics.py index 4e82177..2d03dc2 100644 --- a/tests/test_optics.py +++ b/tests/test_optics.py @@ -31,7 +31,7 @@ def test_generate_propagation_kernel(): def test_gen_Greens_function_z(): wavelength = 0.5 - z_position_list = torch.tensor([0, 1, -1]) # note fftfreq coords + z_position_list = [0, 1, -1] # note fftfreq coords radial_frequencies = util.generate_radial_frequencies((10, 10), 0.5) pupil = optics.generate_pupil(radial_frequencies, 0.5, 0.5) diff --git a/waveorder/optics.py b/waveorder/optics.py index 1a7bab6..65ac130 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -395,7 +395,7 @@ def generate_propagation_kernel( wavelength : float wavelength of the light in the immersion media - z_position_list : torch.tensor + z_position_list : torch.tensor or list 1D array of defocused z positions with the size of (Z) Returns @@ -414,7 +414,7 @@ def generate_propagation_kernel( 1j * 2 * np.pi - * z_position_list[:, None, None] + * torch.tensor(z_position_list)[:, None, None] * oblique_factor[None, :, :] ) @@ -440,7 +440,7 @@ def generate_greens_function_z( wavelength_illumination : float wavelength of the light in the immersion media - z_position_list : torch.tensor + z_position_list : torch.tensor or list 1D array of defocused z position with the size of (Z,) axially_even : bool @@ -461,9 +461,9 @@ def generate_greens_function_z( ) ** (1 / 2) / wavelength_illumination if axially_even: - z_positions = torch.abs(z_position_list[:, None, None]) + z_positions = torch.abs(torch.tensor(z_position_list)[:, None, None]) else: - z_positions = z_position_list[:, None, None] + z_positions = torch.tensor(z_position_list)[:, None, None] greens_function_z = ( -1j From 6fa39fdb3f9448e6460c846f7349ffcbefac18f9 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 16:34:21 -0700 Subject: [PATCH 27/72] Reapply "fix warnings from tensoring a tensor" This reverts commit 790df573e82311226ca25f7c547c96a0d8fa7175. --- tests/test_optics.py | 2 +- waveorder/optics.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_optics.py b/tests/test_optics.py index 2d03dc2..4e82177 100644 --- a/tests/test_optics.py +++ b/tests/test_optics.py @@ -31,7 +31,7 @@ def test_generate_propagation_kernel(): def test_gen_Greens_function_z(): wavelength = 0.5 - z_position_list = [0, 1, -1] # note fftfreq coords + z_position_list = torch.tensor([0, 1, -1]) # note fftfreq coords radial_frequencies = util.generate_radial_frequencies((10, 10), 0.5) pupil = optics.generate_pupil(radial_frequencies, 0.5, 0.5) diff --git a/waveorder/optics.py b/waveorder/optics.py index 65ac130..1a7bab6 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -395,7 +395,7 @@ def generate_propagation_kernel( wavelength : float wavelength of the light in the immersion media - z_position_list : torch.tensor or list + z_position_list : torch.tensor 1D array of defocused z positions with the size of (Z) Returns @@ -414,7 +414,7 @@ def generate_propagation_kernel( 1j * 2 * np.pi - * torch.tensor(z_position_list)[:, None, None] + * z_position_list[:, None, None] * oblique_factor[None, :, :] ) @@ -440,7 +440,7 @@ def generate_greens_function_z( wavelength_illumination : float wavelength of the light in the immersion media - z_position_list : torch.tensor or list + z_position_list : torch.tensor 1D array of defocused z position with the size of (Z,) axially_even : bool @@ -461,9 +461,9 @@ def generate_greens_function_z( ) ** (1 / 2) / wavelength_illumination if axially_even: - z_positions = torch.abs(torch.tensor(z_position_list)[:, None, None]) + z_positions = torch.abs(z_position_list[:, None, None]) else: - z_positions = torch.tensor(z_position_list)[:, None, None] + z_positions = z_position_list[:, None, None] greens_function_z = ( -1j From da6a40b151d765c85a3dfc02f8a14f485f881280 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 17:13:54 -0700 Subject: [PATCH 28/72] revive old version of greens tensor for backwards compatibility --- waveorder/optics.py | 57 ++++++++++++++++++++++++++-- waveorder/waveorder_reconstructor.py | 2 +- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/waveorder/optics.py b/waveorder/optics.py index 1a7bab6..f10a6e7 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -422,7 +422,10 @@ def generate_propagation_kernel( def generate_greens_function_z( - radial_frequencies, pupil_support, wavelength_illumination, z_position_list, + radial_frequencies, + pupil_support, + wavelength_illumination, + z_position_list, axially_even=True, ): """ @@ -445,7 +448,7 @@ def generate_greens_function_z( axially_even : bool For backwards compatibility with legacy phase reconstruction. - Ideally the legacy phase reconstruction should be unified with + Ideally the legacy phase reconstruction should be unified with the new reconstructions, and this parameter should be removed. Returns @@ -464,7 +467,7 @@ def generate_greens_function_z( z_positions = torch.abs(z_position_list[:, None, None]) else: z_positions = z_position_list[:, None, None] - + greens_function_z = ( -1j / 4 @@ -533,6 +536,54 @@ def generate_defocus_greens_tensor( return G_tensor_z +def gen_dyadic_Greens_tensor_z(fxx, fyy, G_fun_z, Pupil_support, lambda_in): + """ + keeping for backwards compatibility + + generate forward dyadic Green's function in u_x, u_y, z space + Parameters + ---------- + fxx : numpy.ndarray + x component of 2D spatial frequency array with the size of (Ny, Nx) + fyy : numpy.ndarray + y component of 2D spatial frequency array with the size of (Ny, Nx) + G_fun_z : numpy.ndarray + forward Green's function in u_x, u_y, z space with size of (Ny, Nx, Nz) + Pupil_support : numpy.ndarray + the array that defines the support of the pupil function with the size of (Ny, Nx) + lambda_in : float + wavelength of the light in the immersion media + Returns + ------- + G_tensor_z : numpy.ndarray + forward dyadic Green's function in u_x, u_y, z space with the size of (3, 3, Ny, Nx, Nz) + """ + + N, M = fxx.shape + fr = (fxx**2 + fyy**2) ** (1 / 2) + oblique_factor = ((1 - lambda_in**2 * fr**2) * Pupil_support) ** ( + 1 / 2 + ) / lambda_in + + diff_filter = np.zeros((3,) + G_fun_z.shape, complex) + diff_filter[0] = (1j * 2 * np.pi * fxx * Pupil_support)[..., np.newaxis] + diff_filter[1] = (1j * 2 * np.pi * fyy * Pupil_support)[..., np.newaxis] + diff_filter[2] = (1j * 2 * np.pi * oblique_factor)[..., np.newaxis] + + G_tensor_z = np.zeros((3, 3) + G_fun_z.shape, complex) + + for i in range(3): + for j in range(3): + G_tensor_z[i, j] = ( + G_fun_z + * diff_filter[i] + * diff_filter[j] + / (2 * np.pi / lambda_in) ** 2 + ) + if i == j: + G_tensor_z[i, i] += G_fun_z + return G_tensor_z + def gen_Greens_function_real(img_size, ps, psz, lambda_in): """ diff --git a/waveorder/waveorder_reconstructor.py b/waveorder/waveorder_reconstructor.py index 5faed08..7389f1e 100644 --- a/waveorder/waveorder_reconstructor.py +++ b/waveorder/waveorder_reconstructor.py @@ -1002,7 +1002,7 @@ def gen_2D_vec_WOTF(self, inc_option=False): .numpy() .transpose((1, 2, 0)) ) - G_tensor_z = generate_defocus_greens_tensor( + G_tensor_z = gen_dyadic_Greens_tensor_z( self.fxx, self.fyy, G_fun_z, self.Pupil_support, self.lambda_illu ) From 974a70012ca9ba4e29a18a3819bf8e19101dc437 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 10 Sep 2024 17:25:29 -0700 Subject: [PATCH 29/72] fix tests that fail because of napari on github --- pyproject.toml | 2 +- waveorder/visuals/napari_visuals.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0c4d14b..30dcade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ dynamic = ["version"] [project.optional-dependencies] -dev = ["pytest", "pytest-cov", "napari[all]"] +dev = ["pytest", "pytest-cov"] [project.urls] Homepage = "https://github.com/mehta-lab/waveorder" diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index cc094d6..f7d6094 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -1,17 +1,16 @@ -import napari import numpy as np import torch def add_transfer_function_to_viewer( - viewer: napari.Viewer, + viewer, # napari viewer transfer_function: torch.Tensor, zyx_scale: tuple[float, float, float], layer_name: str = "Transfer Function", clim_factor: float = 1.0, ): zyx_shape = transfer_function.shape[-3:] - lim = torch.max(torch.abs(transfer_function))*clim_factor + lim = torch.max(torch.abs(transfer_function)) * clim_factor voxel_scale = np.array( [ zyx_shape[0] * zyx_scale[0], @@ -41,7 +40,7 @@ def add_transfer_function_to_viewer( name="Im(" + layer_name + ")", ) - viewer.dims.current_step = (0,)*(transfer_function.ndim - 3) + ( + viewer.dims.current_step = (0,) * (transfer_function.ndim - 3) + ( zyx_shape[0] // 2, zyx_shape[1] // 2, zyx_shape[2] // 2, From 995c90df8a4b80d3168e9a75d98e75910a44bcd5 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Fri, 27 Sep 2024 10:00:08 -0700 Subject: [PATCH 30/72] wrap-safe vector transfer function --- .../inplane_oriented_thick_pol3d_vector.py | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 874afac..56da40f 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -2,7 +2,7 @@ import numpy as np from torch import Tensor -from waveorder import optics, stokes, util +from waveorder import optics, sampling, stokes, util from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer @@ -37,6 +37,61 @@ def calculate_transfer_function( numerical_aperture_illumination, numerical_aperture_detection, invert_phase_contrast=False, +): + transverse_nyquist = sampling.transverse_nyquist( + wavelength_illumination, + numerical_aperture_illumination, + numerical_aperture_detection, + ) + axial_nyquist = sampling.axial_nyquist( + wavelength_illumination, + numerical_aperture_detection, + index_of_refraction_media, + ) + + yx_factor = int(np.ceil(yx_pixel_size / transverse_nyquist)) + z_factor = int(np.ceil(z_pixel_size / axial_nyquist)) + + sfZYX_transfer_function, intensity_to_stokes_matrix = ( + _calculate_wrap_unsafe_transfer_function( + swing, + scheme, + ( + zyx_shape[0] * z_factor, + zyx_shape[1] * yx_factor, + zyx_shape[2] * yx_factor, + ), + yx_pixel_size / yx_factor, + z_pixel_size / z_factor, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=invert_phase_contrast, + ) + ) + sfzyx_out_shape = (3, 3, zyx_shape[0] + 2 * z_padding,) + zyx_shape[1:] + return ( + sampling.nd_fourier_central_cuboid( + sfZYX_transfer_function, sfzyx_out_shape + ), + intensity_to_stokes_matrix, + ) + + +def _calculate_wrap_unsafe_transfer_function( + swing, + scheme, + zyx_shape, + yx_pixel_size, + z_pixel_size, + wavelength_illumination, + z_padding, + index_of_refraction_media, + numerical_aperture_illumination, + numerical_aperture_detection, + invert_phase_contrast=False, ): intensity_to_stokes_matrix = stokes.calculate_intensity_to_stokes_matrix( swing, scheme=scheme @@ -108,7 +163,6 @@ def calculate_transfer_function( z_position_list, ) - G = optics.generate_defocus_greens_tensor( x_frequencies, y_frequencies, @@ -181,6 +235,7 @@ def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): layer_name="Transfer Function", ) + def apply_transfer_function( fzyx_object, sfZYX_transfer_function, From 69fea21e6dd5b2a53337e72d7189218ece2fd8c1 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Fri, 27 Sep 2024 11:05:18 -0700 Subject: [PATCH 31/72] sampling tests --- tests/test_sampling.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/test_sampling.py diff --git a/tests/test_sampling.py b/tests/test_sampling.py new file mode 100644 index 0000000..4dc6596 --- /dev/null +++ b/tests/test_sampling.py @@ -0,0 +1,13 @@ +import torch +import numpy as np + +from waveorder.sampling import ( + nd_fourier_central_cuboid, +) + + +def test_nd_fourier_central_cuboid(): + source = torch.randn(8, 8) + target_shape = (4, 4) + result = nd_fourier_central_cuboid(source, target_shape) + assert result.shape == target_shape From 93d5940d77e055ff51d3603fc6bd8817941a099e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Fri, 27 Sep 2024 11:05:41 -0700 Subject: [PATCH 32/72] fourier-space oversampling --- .../inplane_oriented_thick_pol3d_vector.py | 4 ++- .../inplane_oriented_thick_pol3d_vector.py | 30 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 4cb4e74..85c8e0d 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -10,13 +10,14 @@ zyx_shape = (100, 256, 256) swing = 0.1 scheme = "5-State" -yx_pixel_size = 2 * 6.5 / 63 +yx_pixel_size = 6.5 / 63 z_pixel_size = 0.25 wavelength_illumination = 0.532 z_padding = 0 index_of_refraction_media = 1.3 numerical_aperture_illumination = 0.9 numerical_aperture_detection = 1.2 +fourier_oversample_factor = 2 # Create a phantom fzyx_object = inplane_oriented_thick_pol3d_vector.generate_test_phantom( @@ -36,6 +37,7 @@ index_of_refraction_media, numerical_aperture_illumination, numerical_aperture_detection, + fourier_oversample_factor=fourier_oversample_factor, ) ) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 56da40f..5ec9dcb 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -2,6 +2,7 @@ import numpy as np from torch import Tensor +from torch.nn.functional import avg_pool3d from waveorder import optics, sampling, stokes, util from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer @@ -37,6 +38,7 @@ def calculate_transfer_function( numerical_aperture_illumination, numerical_aperture_detection, invert_phase_contrast=False, + fourier_oversample_factor=1, ): transverse_nyquist = sampling.transverse_nyquist( wavelength_illumination, @@ -57,9 +59,9 @@ def calculate_transfer_function( swing, scheme, ( - zyx_shape[0] * z_factor, - zyx_shape[1] * yx_factor, - zyx_shape[2] * yx_factor, + zyx_shape[0] * z_factor * fourier_oversample_factor, + zyx_shape[1] * yx_factor * fourier_oversample_factor, + zyx_shape[2] * yx_factor * fourier_oversample_factor, ), yx_pixel_size / yx_factor, z_pixel_size / z_factor, @@ -71,10 +73,28 @@ def calculate_transfer_function( invert_phase_contrast=invert_phase_contrast, ) ) - sfzyx_out_shape = (3, 3, zyx_shape[0] + 2 * z_padding,) + zyx_shape[1:] + + # avg_pool3d does not support complex numbers + pooled_sfZYX_transfer_function_real = avg_pool3d( + sfZYX_transfer_function.real, (fourier_oversample_factor,) * 3 + ) + pooled_sfZYX_transfer_function_imag = avg_pool3d( + sfZYX_transfer_function.imag, (fourier_oversample_factor,) * 3 + ) + pooled_sfZYX_transfer_function = ( + pooled_sfZYX_transfer_function_real + + 1j * pooled_sfZYX_transfer_function_imag + ) + + # Crop to original size + sfzyx_out_shape = ( + 3, + 3, + zyx_shape[0] + 2 * z_padding, + ) + zyx_shape[1:] return ( sampling.nd_fourier_central_cuboid( - sfZYX_transfer_function, sfzyx_out_shape + pooled_sfZYX_transfer_function, sfzyx_out_shape ), intensity_to_stokes_matrix, ) From 6ad3d02a481f21ae4ca9d677eb0e1684df3a6110 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 1 Oct 2024 08:47:04 -0700 Subject: [PATCH 33/72] better visualizations for debugging --- .../models/inplane_oriented_thick_pol3d_vector.py | 2 +- waveorder/visuals/napari_visuals.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 85c8e0d..8aee221 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -17,7 +17,7 @@ index_of_refraction_media = 1.3 numerical_aperture_illumination = 0.9 numerical_aperture_detection = 1.2 -fourier_oversample_factor = 2 +fourier_oversample_factor = 1 # Create a phantom fzyx_object = inplane_oriented_thick_pol3d_vector.generate_test_phantom( diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index f7d6094..0b559c0 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -45,4 +45,16 @@ def add_transfer_function_to_viewer( zyx_shape[1] // 2, zyx_shape[2] // 2, ) - viewer.dims.order = (2, 0, 1) + + # Show XZ view by default, and only allow rolling between XY and XZ + viewer.dims.order = list(range(transfer_function.ndim - 3)) + [ + transfer_function.ndim - 2, + transfer_function.ndim - 3, + transfer_function.ndim - 1, + ] + viewer.dims.rollable = (False,) * (transfer_function.ndim - 3) + ( + True, + True, + False, + ) + From 1f03a8f697b5ac45676e7ed88feef911747d0611 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 1 Oct 2024 14:01:50 -0700 Subject: [PATCH 34/72] complex-valued napari visuals --- waveorder/visuals/napari_visuals.py | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index 0b559c0..a0e6072 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -1,3 +1,5 @@ +from waveorder.visuals.utils import complex_tensor_to_rgb + import numpy as np import torch @@ -8,6 +10,7 @@ def add_transfer_function_to_viewer( zyx_scale: tuple[float, float, float], layer_name: str = "Transfer Function", clim_factor: float = 1.0, + complex_rgb: bool = False, ): zyx_shape = transfer_function.shape[-3:] lim = torch.max(torch.abs(transfer_function)) * clim_factor @@ -20,25 +23,33 @@ def add_transfer_function_to_viewer( ) shift_dims = (-3, -2, -1) - viewer.add_image( - torch.fft.ifftshift(torch.real(transfer_function), dim=shift_dims) - .cpu() - .numpy(), - colormap="bwr", - contrast_limits=(-lim, lim), - scale=1 / voxel_scale, - name="Re(" + layer_name + ")", - ) - if transfer_function.dtype == torch.complex64: + if complex_rgb: + rgb_transfer_function = complex_tensor_to_rgb(torch.fft.ifftshift(transfer_function, dim=shift_dims)) + viewer.add_image( + rgb_transfer_function, + scale=1 / voxel_scale, + name=layer_name, + ) + else: viewer.add_image( - torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) + torch.fft.ifftshift(torch.real(transfer_function), dim=shift_dims) .cpu() .numpy(), colormap="bwr", contrast_limits=(-lim, lim), scale=1 / voxel_scale, - name="Im(" + layer_name + ")", + name="Re(" + layer_name + ")", ) + if transfer_function.dtype == torch.complex64: + viewer.add_image( + torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) + .cpu() + .numpy(), + colormap="bwr", + contrast_limits=(-lim, lim), + scale=1 / voxel_scale, + name="Im(" + layer_name + ")", + ) viewer.dims.current_step = (0,) * (transfer_function.ndim - 3) + ( zyx_shape[0] // 2, From d86e0ab2d1355abc366027fb5e9e753b2383031b Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 1 Oct 2024 14:02:12 -0700 Subject: [PATCH 35/72] complex utils --- waveorder/visuals/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 waveorder/visuals/utils.py diff --git a/waveorder/visuals/utils.py b/waveorder/visuals/utils.py new file mode 100644 index 0000000..72eb7ec --- /dev/null +++ b/waveorder/visuals/utils.py @@ -0,0 +1,28 @@ +import numpy as np +import torch +import matplotlib.colors as mcolors + + +# Main function to convert a complex-valued torch tensor to RGB numpy array +def complex_tensor_to_rgb(tensor): + # Convert the torch tensor to a numpy array + tensor_np = tensor.numpy() + + # Calculate magnitude and phase for the entire array + magnitude = np.abs(tensor_np) + phase = np.angle(tensor_np) + + # Normalize phase to [0, 1] with red at 0 + hue = phase / (2 * np.pi) + 0.5 + + # Normalize magnitude to [0, 1] for saturation + max_abs_val = np.amax(magnitude) + sat = magnitude / max_abs_val if max_abs_val != 0 else magnitude + + # Create HSV array: hue, saturation, value (value is set to 1) + hsv = np.stack((hue, sat, np.ones_like(sat)), axis=-1) + + # Convert the entire HSV array to RGB using vectorized conversion + rgb_array = mcolors.hsv_to_rgb(hsv) + + return rgb_array From 4f32ac182ce607eee08373e03e5119b0ea2a3000 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 1 Oct 2024 20:18:49 -0700 Subject: [PATCH 36/72] fix colormaps --- waveorder/visuals/utils.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/waveorder/visuals/utils.py b/waveorder/visuals/utils.py index 72eb7ec..e63bba0 100644 --- a/waveorder/visuals/utils.py +++ b/waveorder/visuals/utils.py @@ -1,28 +1,29 @@ import numpy as np -import torch import matplotlib.colors as mcolors # Main function to convert a complex-valued torch tensor to RGB numpy array +# with red at +1, green at +i, blue at -1, and purple at -i def complex_tensor_to_rgb(tensor): # Convert the torch tensor to a numpy array tensor_np = tensor.numpy() - + # Calculate magnitude and phase for the entire array magnitude = np.abs(tensor_np) phase = np.angle(tensor_np) - - # Normalize phase to [0, 1] with red at 0 - hue = phase / (2 * np.pi) + 0.5 - + + # Normalize phase to [0, 1] + hue = (phase + np.pi) / (2 * np.pi) + hue = np.mod(hue + 0.5, 1) + # Normalize magnitude to [0, 1] for saturation max_abs_val = np.amax(magnitude) sat = magnitude / max_abs_val if max_abs_val != 0 else magnitude - + # Create HSV array: hue, saturation, value (value is set to 1) hsv = np.stack((hue, sat, np.ones_like(sat)), axis=-1) - + # Convert the entire HSV array to RGB using vectorized conversion rgb_array = mcolors.hsv_to_rgb(hsv) - + return rgb_array From f251db5d2bb5e38dd00d66404880970533b68cd2 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 1 Oct 2024 20:23:47 -0700 Subject: [PATCH 37/72] debugging coherent tfs --- .../inplane_oriented_thick_pol3d_vector.py | 13 ++++++++----- .../inplane_oriented_thick_pol3d_vector.py | 17 ++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 8aee221..5096572 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -7,15 +7,15 @@ # Parameters # all lengths must use consistent units e.g. um -zyx_shape = (100, 256, 256) +zyx_shape = (101, 256, 256) swing = 0.1 scheme = "5-State" yx_pixel_size = 6.5 / 63 -z_pixel_size = 0.25 +z_pixel_size = 0.15 wavelength_illumination = 0.532 z_padding = 0 index_of_refraction_media = 1.3 -numerical_aperture_illumination = 0.9 +numerical_aperture_illumination = 0.01 # 0.5 numerical_aperture_detection = 1.2 fourier_oversample_factor = 1 @@ -86,6 +86,9 @@ for array in arrays: viewer.add_image(torch.real(array[0]).cpu().numpy(), name=array[1]) -# viewer.grid.enabled = True -# viewer.grid.shape = (2, 5) +viewer.grid.enabled = True +viewer.grid.shape = (2, 5) +import pdb + +pdb.set_trace() input("Showing object, data, and recon. Press to quit...") diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 5ec9dcb..19bf381 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -17,7 +17,7 @@ def generate_test_phantom(zyx_shape): ) c00 = yx_star c2_2 = -torch.sin(2 * yx_theta) * yx_star - c22 = torch.cos(2 * yx_theta) * yx_star + c22 = -torch.cos(2 * yx_theta) * yx_star # Put in a center slices of a 3D object center_slice_object = torch.stack((c00, c2_2, c22), dim=0) @@ -195,10 +195,10 @@ def _calculate_wrap_unsafe_transfer_function( G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) S_3D = torch.fft.ifft(S, dim=-3) - # Normalize - P_3D /= torch.amax(torch.abs(P_3D)) - G_3D /= torch.amax(torch.abs(G_3D)) - S_3D /= torch.amax(torch.abs(S_3D)) + # # Normalize + # P_3D /= torch.amax(torch.abs(P_3D)) + # G_3D /= torch.amax(torch.abs(G_3D)) + # S_3D /= torch.amax(torch.abs(S_3D)) # Main part PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) @@ -223,6 +223,8 @@ def _calculate_wrap_unsafe_transfer_function( H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components # H_im = 1j * (H1 - H2) # ignore absorptive terms + H_re /= torch.amax(torch.abs(H_re)) + s = util.pauli()[[0, 1, 2]] # select s0, s1, and s2 (drop s3) Y = util.gellmann()[[0, 4, 8]] # select phase f00 and transverse linear isotropic terms 2-2, and f22 @@ -253,6 +255,7 @@ def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): sfZYX_transfer_function, zyx_scale=zyx_scale, layer_name="Transfer Function", + complex_rgb=True, ) @@ -267,7 +270,7 @@ def apply_transfer_function( ) szyx_data = torch.fft.ifftn(sZYX_data, dim=(1, 2, 3)) - return (5 * szyx_data) + 0.1 * torch.randn(szyx_data.shape) + return (50 * szyx_data) + 0.1 * torch.randn(szyx_data.shape) def apply_inverse_transfer_function( @@ -282,7 +285,7 @@ def apply_inverse_transfer_function( print("Computing inverse filter") U, S, Vh = singular_system S_reg = S / (S**2 + regularization_strength**2) - ZYXsf_inverse_filter = -torch.einsum( + ZYXsf_inverse_filter = torch.einsum( "zyxij,zyxj,zyxjk->zyxki", U, S_reg, Vh ) From 427386c3dde2c4d3780ff9f80d087299077dfee8 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 2 Oct 2024 08:21:15 -0700 Subject: [PATCH 38/72] formatting --- waveorder/visuals/napari_visuals.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index a0e6072..d41b392 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -24,7 +24,9 @@ def add_transfer_function_to_viewer( shift_dims = (-3, -2, -1) if complex_rgb: - rgb_transfer_function = complex_tensor_to_rgb(torch.fft.ifftshift(transfer_function, dim=shift_dims)) + rgb_transfer_function = complex_tensor_to_rgb( + torch.fft.ifftshift(transfer_function, dim=shift_dims) + ) viewer.add_image( rgb_transfer_function, scale=1 / voxel_scale, @@ -42,7 +44,9 @@ def add_transfer_function_to_viewer( ) if transfer_function.dtype == torch.complex64: viewer.add_image( - torch.fft.ifftshift(torch.imag(transfer_function), dim=shift_dims) + torch.fft.ifftshift( + torch.imag(transfer_function), dim=shift_dims + ) .cpu() .numpy(), colormap="bwr", @@ -68,4 +72,3 @@ def add_transfer_function_to_viewer( True, False, ) - From 2b7fd04dd0cf7fd8d88553b1b2eb0e3e9ecfbac3 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 2 Oct 2024 08:22:53 -0700 Subject: [PATCH 39/72] fix rotation matrices --- waveorder/optics.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/waveorder/optics.py b/waveorder/optics.py index f10a6e7..06de46e 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -239,10 +239,7 @@ def rotation_matrix(nu_z, nu_y, nu_x, wavelength): out = torch.stack((row0, row1, row2), dim=0) - # KLUDGE to avoid fix nans - out[..., 0, 0] = torch.tensor([[0, 0], [1, 0], [0, 1]])[..., None] - - return out + return torch.nan_to_num(out, nan=0.0) def generate_vector_source_defocus_pupil( @@ -258,12 +255,12 @@ def generate_vector_source_defocus_pupil( "zyx,yx->zyx", torch.fft.fft(defocus_pupil, dim=0), ill_pupil ).abs() # make this real - # Calculate zyx_frequency grid (inelegant) - z_frequencies = torch.fft.ifft(z_position_list) - freq_shape = z_frequencies.shape + x_frequencies.shape - z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) + + freq_shape = z_position_list.shape + x_frequencies.shape + y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) + z_broadcast = np.sqrt(wavelength**(-2) - x_broadcast**2 - y_broadcast**2) # Calculate rotation matrix rotations = rotation_matrix( @@ -274,10 +271,9 @@ def generate_vector_source_defocus_pupil( source_pupil = ( torch.einsum( "ijzyx,j,zyx->izyx", rotations, input_jones, ill_pupil_3d - ) # .abs() - # ** 2 - ) # abs here is critical...incoherent pupil - + ) + ) + # Convert back to defocus pupil source_defocus_pupil = torch.fft.ifft(source_pupil, dim=-3) From 902c69085115692a8bdf55310aab481138c5df4e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 2 Oct 2024 08:30:20 -0700 Subject: [PATCH 40/72] fix dc term --- waveorder/optics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/waveorder/optics.py b/waveorder/optics.py index 06de46e..6ae5c6c 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -239,6 +239,9 @@ def rotation_matrix(nu_z, nu_y, nu_x, wavelength): out = torch.stack((row0, row1, row2), dim=0) + # KLUDGE: fix the DC term manually, avoiding nan + out[..., 0, 0] = torch.tensor([[0, 0], [1, 0], [0, 1]])[..., None] + return torch.nan_to_num(out, nan=0.0) From 1cb7d53a135771368e26065d9e427535e0475858 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 2 Oct 2024 08:58:53 -0700 Subject: [PATCH 41/72] cleaning up --- examples/models/inplane_oriented_thick_pol3d_vector.py | 2 +- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 5 +---- waveorder/visuals/napari_visuals.py | 3 ++- waveorder/visuals/utils.py | 5 +++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 5096572..72f01d4 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -15,7 +15,7 @@ wavelength_illumination = 0.532 z_padding = 0 index_of_refraction_media = 1.3 -numerical_aperture_illumination = 0.01 # 0.5 +numerical_aperture_illumination = 0.5 numerical_aperture_detection = 1.2 fourier_oversample_factor = 1 diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 19bf381..d6a6f26 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -195,10 +195,6 @@ def _calculate_wrap_unsafe_transfer_function( G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) S_3D = torch.fft.ifft(S, dim=-3) - # # Normalize - # P_3D /= torch.amax(torch.abs(P_3D)) - # G_3D /= torch.amax(torch.abs(G_3D)) - # S_3D /= torch.amax(torch.abs(S_3D)) # Main part PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) @@ -256,6 +252,7 @@ def visualize_transfer_function(viewer, sfZYX_transfer_function, zyx_scale): zyx_scale=zyx_scale, layer_name="Transfer Function", complex_rgb=True, + clim_factor=0.5, ) diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index d41b392..0fbef9d 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -25,7 +25,8 @@ def add_transfer_function_to_viewer( if complex_rgb: rgb_transfer_function = complex_tensor_to_rgb( - torch.fft.ifftshift(transfer_function, dim=shift_dims) + torch.fft.ifftshift(transfer_function, dim=shift_dims), + saturate_clim_fraction=clim_factor, ) viewer.add_image( rgb_transfer_function, diff --git a/waveorder/visuals/utils.py b/waveorder/visuals/utils.py index e63bba0..c612ddf 100644 --- a/waveorder/visuals/utils.py +++ b/waveorder/visuals/utils.py @@ -4,7 +4,7 @@ # Main function to convert a complex-valued torch tensor to RGB numpy array # with red at +1, green at +i, blue at -1, and purple at -i -def complex_tensor_to_rgb(tensor): +def complex_tensor_to_rgb(tensor, saturate_clim_fraction=1.0): # Convert the torch tensor to a numpy array tensor_np = tensor.numpy() @@ -17,7 +17,8 @@ def complex_tensor_to_rgb(tensor): hue = np.mod(hue + 0.5, 1) # Normalize magnitude to [0, 1] for saturation - max_abs_val = np.amax(magnitude) + max_abs_val = np.amax(magnitude) * saturate_clim_fraction + sat = magnitude / max_abs_val if max_abs_val != 0 else magnitude # Create HSV array: hue, saturation, value (value is set to 1) From 3b0f2523c741e7523d0e425000b2aa301b106c10 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Sat, 5 Oct 2024 15:27:30 -0700 Subject: [PATCH 42/72] first-pass visuals checkpoint --- waveorder/visuals/matplotlib_visuals.py | 105 ++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 waveorder/visuals/matplotlib_visuals.py diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py new file mode 100644 index 0000000..984c86c --- /dev/null +++ b/waveorder/visuals/matplotlib_visuals.py @@ -0,0 +1,105 @@ +import matplotlib.pyplot as plt + +from waveorder.visuals.utils import complex_tensor_to_rgb +import os +import numpy as np +import re +import torch + + +def plot_transfer_function( + sfZYX_data, + filename, + zyx_scale, + z_slice, + s_labels, + f_labels, + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=1.0, +): + sfZYX_data = torch.fft.ifftshift(sfZYX_data, dim=(-3, -2, -1)) + sfZYX_rgb = complex_tensor_to_rgb(sfZYX_data, saturate_clim_fraction) + sfZYX_rgb[:, :, 0, 0, 0, :] = 0 + sfZYX_rgb[:, :, -1, -1, -1, :] = 0 + + S, F, Z, Y, X = sfZYX_data.shape + assert S == len(s_labels) + assert F == len(f_labels) + + X_size = 1 * inches_per_column + Y_size = (zyx_scale[2] / zyx_scale[1]) * inches_per_column + Z_size = (zyx_scale[2] / zyx_scale[0]) * inches_per_column + + n_rows = (S * 3) + 1 + n_cols = F + 1 + + height = S * (Y_size + (2 * Z_size)) + 1 + width = (F + 1) * X_size + + height_ratios = [1] + [ + item + for sublist in [[Y_size, Z_size, Z_size] for _ in range(S)] + for item in sublist + ] + + fig, axes = plt.subplots( + n_rows, + n_cols, + figsize=(width, height), + gridspec_kw={ + "width_ratios": [1] * n_cols, + "height_ratios": height_ratios, + "wspace": 0.1, # Adjust this value to reduce space between columns + "hspace": 0.1, # Adjust this value to reduce space between rows + }, + ) + + rose_path = os.path.join( + os.path.dirname(__file__), + f"./assets/rose.png", + ) + axes[0, 0].imshow(plt.imread(rose_path)) + + for i in range(n_rows): + for j in range(n_cols): + if (i == 0 and j > 0) or (j == 0 and (i - 1) % 3 == 0): + if i == 0: + folder = "gellman" + index = f_labels[j - 1] + else: + folder = "stokes" + index = s_labels[int((i - 1) / 3)] + image_path = os.path.join( + os.path.dirname(__file__), + f"./assets/{folder}/{index}.png", + ) + image = plt.imread(image_path) + axes[i, j].imshow(image) + + if i > 0 and j > 0: + if (i - 1) % 3 == 0: + axes[i, j].imshow( + sfZYX_rgb[int((i - 1) / 3), j - 1, z_slice], + aspect=zyx_scale[1] / zyx_scale[2], + ) + elif (i - 1) % 3 == 1: + axes[i, j].imshow( + sfZYX_rgb[int((i - 1) / 3), j - 1, :, :, X // 2, :], + aspect=zyx_scale[2] / zyx_scale[0], + ) + elif (i - 1) % 3 == 2: + axes[i, j].imshow( + sfZYX_rgb[int((i - 1) / 3), j - 1, :, Y // 2], + aspect=zyx_scale[1] / zyx_scale[0], + ) + + axes[i, j].tick_params( + left=False, bottom=False, labelleft=False, labelbottom=False + ) + # axes[i, j].spines["top"].set_visible(False) + # axes[i, j].spines["right"].set_visible(False) + # axes[i, j].spines["bottom"].set_visible(False) + # axes[i, j].spines["left"].set_visible(False) + + fig.savefig(filename, bbox_inches="tight") From cd081e9a46222a6fbf2b7b3291ddfd46cf8c16b5 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 13:43:58 -0700 Subject: [PATCH 43/72] matplotlib visuals --- waveorder/visuals/matplotlib_visuals.py | 269 ++++++++++++++++++++---- 1 file changed, 230 insertions(+), 39 deletions(-) diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py index 984c86c..391a3c9 100644 --- a/waveorder/visuals/matplotlib_visuals.py +++ b/waveorder/visuals/matplotlib_visuals.py @@ -1,9 +1,10 @@ import matplotlib.pyplot as plt from waveorder.visuals.utils import complex_tensor_to_rgb -import os +from waveorder.sampling import nd_fourier_central_cuboid + import numpy as np -import re +import os import torch @@ -17,13 +18,23 @@ def plot_transfer_function( rose_path=None, inches_per_column=1, saturate_clim_fraction=1.0, + trim_edges=0, ): + S, F, Z, Y, X = sfZYX_data.shape + voxel_scale = [ + Z * zyx_scale[0], + Y * zyx_scale[1], + X * zyx_scale[2], + ] + + sfZYX_data = nd_fourier_central_cuboid( + sfZYX_data, (S, F, Z - trim_edges, Y - trim_edges, X - trim_edges) + ) + + S, F, Z, Y, X = sfZYX_data.shape sfZYX_data = torch.fft.ifftshift(sfZYX_data, dim=(-3, -2, -1)) sfZYX_rgb = complex_tensor_to_rgb(sfZYX_data, saturate_clim_fraction) - sfZYX_rgb[:, :, 0, 0, 0, :] = 0 - sfZYX_rgb[:, :, -1, -1, -1, :] = 0 - S, F, Z, Y, X = sfZYX_data.shape assert S == len(s_labels) assert F == len(f_labels) @@ -31,25 +42,18 @@ def plot_transfer_function( Y_size = (zyx_scale[2] / zyx_scale[1]) * inches_per_column Z_size = (zyx_scale[2] / zyx_scale[0]) * inches_per_column - n_rows = (S * 3) + 1 - n_cols = F + 1 - height = S * (Y_size + (2 * Z_size)) + 1 - width = (F + 1) * X_size + n_rows = S + 1 + n_cols = (F * 3) + 1 - height_ratios = [1] + [ - item - for sublist in [[Y_size, Z_size, Z_size] for _ in range(S)] - for item in sublist - ] + height = n_rows + width = n_cols fig, axes = plt.subplots( n_rows, n_cols, figsize=(width, height), gridspec_kw={ - "width_ratios": [1] * n_cols, - "height_ratios": height_ratios, "wspace": 0.1, # Adjust this value to reduce space between columns "hspace": 0.1, # Adjust this value to reduce space between rows }, @@ -63,43 +67,230 @@ def plot_transfer_function( for i in range(n_rows): for j in range(n_cols): - if (i == 0 and j > 0) or (j == 0 and (i - 1) % 3 == 0): + if (i == 0 and (j - 1) % 3 == 1) or (j == 0 and i > 0): if i == 0: folder = "gellman" - index = f_labels[j - 1] + index = f_labels[int((j - 1) / 3)] else: folder = "stokes" - index = s_labels[int((i - 1) / 3)] - image_path = os.path.join( - os.path.dirname(__file__), - f"./assets/{folder}/{index}.png", - ) - image = plt.imread(image_path) - axes[i, j].imshow(image) + index = s_labels[i - 1] + + if isinstance(index, int): + image_path = os.path.join( + os.path.dirname(__file__), + f"./assets/{folder}/{index}.png", + ) + image = plt.imread(image_path) + axes[i, j].imshow(image) + else: + axes[i, j].text( + 0.5, + 0.5, + index, + horizontalalignment="center", + verticalalignment="center", + fontsize=10, + color="black", + ) if i > 0 and j > 0: - if (i - 1) % 3 == 0: + if (j - 1) % 3 == 0: axes[i, j].imshow( - sfZYX_rgb[int((i - 1) / 3), j - 1, z_slice], - aspect=zyx_scale[1] / zyx_scale[2], + sfZYX_rgb[i - 1, int((j - 1) / 3), (Z // 2) + z_slice], + aspect=voxel_scale[1] / voxel_scale[2], + #interpolation=None ) - elif (i - 1) % 3 == 1: + elif (j - 1) % 3 == 1: axes[i, j].imshow( - sfZYX_rgb[int((i - 1) / 3), j - 1, :, :, X // 2, :], - aspect=zyx_scale[2] / zyx_scale[0], + sfZYX_rgb[i - 1, int((j - 1) / 3), :, :, X // 2, :], + aspect=voxel_scale[2] / voxel_scale[0], ) - elif (i - 1) % 3 == 2: + elif (j - 1) % 3 == 2: axes[i, j].imshow( - sfZYX_rgb[int((i - 1) / 3), j - 1, :, Y // 2], - aspect=zyx_scale[1] / zyx_scale[0], + sfZYX_rgb[i - 1, int((j - 1) / 3), :, Y // 2], + aspect=voxel_scale[1] / voxel_scale[0], ) axes[i, j].tick_params( left=False, bottom=False, labelleft=False, labelbottom=False ) - # axes[i, j].spines["top"].set_visible(False) - # axes[i, j].spines["right"].set_visible(False) - # axes[i, j].spines["bottom"].set_visible(False) - # axes[i, j].spines["left"].set_visible(False) - fig.savefig(filename, bbox_inches="tight") + # Draw lines between rows and cols + top = axes[0, 0].get_position().y1 + bottom = axes[-1, -1].get_position().y0 + left = axes[0, 0].get_position().x0 + right = axes[-1, -1].get_position().x1 + if i == 0 and (j - 1) % 3 == 0: + left_edge = (axes[0, j].get_position().x0 + axes[0, j - 1].get_position().x1)/2 + fig.add_artist( + plt.Line2D( + [left_edge, left_edge], + [bottom, top], + transform=fig.transFigure, + color="black", + lw=0.5, + ) + ) + if j == 0 and i > 0: + top_edge = (axes[i, 0].get_position().y1 + axes[i - 1, 0].get_position().y0)/2 + fig.add_artist( + plt.Line2D( + [left, right], + [top_edge, top_edge], + transform=fig.transFigure, + color="black", + lw=0.5, + ) + ) + + axes[i, j].spines["top"].set_visible(False) + axes[i, j].spines["right"].set_visible(False) + axes[i, j].spines["bottom"].set_visible(False) + axes[i, j].spines["left"].set_visible(False) + + # Draw ortho view lines + fig.add_artist( + plt.Line2D( + #[(X/2) - 0.5*X/np.sqrt(2) , (X/2) + 0.5*X/np.sqrt(2)], + #[(Y/2) - 0.5*Y/np.sqrt(2) , (Y/2) + 0.5*Y/np.sqrt(2)], + [Y//2, Y//2], + [0, X], + transform=axes[1, 1].transData, # use axis coordinates + color="red", + lw=0.5, + ) + ) + fig.add_artist( + plt.Line2D( + [0, X], + [Y//2, Y//2], # from bottom to top in axis coordinates + transform=axes[1, 1].transData, # use axis coordinates + color="blue", + lw=0.5, + ) + ) + fig.add_artist( + plt.Rectangle( + (0, 0), # lower-left corner + X, # width + Y, # height + linewidth=0.5, + edgecolor='green', + facecolor='none', + transform=axes[1, 1].transData # use axis coordinates + ) + ) + axes[1, 1].text( + 0.1, 0.975, 'x', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 1].transAxes, + fontsize=5, + color='black' + ) + axes[1, 1].text( + 0.025, 0.9, 'y', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 1].transAxes, + fontsize=5, + color='black' + ) + + fig.add_artist( + plt.Line2D( + [0, X], + [Z//2 + z_slice, Z//2 + z_slice], # from bottom to top in axis coordinates + transform=axes[1, 2].transData, # use axis coordinates + color="green", + lw=0.5, + ) + ) + fig.add_artist( + plt.Line2D( + [X//2, X//2], + [0, Z], # from bottom to top in axis coordinates + transform=axes[1, 2].transData, # use axis coordinates + color="blue", + lw=0.5, + ) + ) + + rect = plt.Rectangle( + (0, 0), # lower-left corner + X, # width + Z, # height + linewidth=0.5, + edgecolor='red', + facecolor='none', + transform=axes[1, 2].transData # use axis coordinates + ) + fig.add_artist(rect) + + axes[1, 2].text( + 0.1, 0.975, 'y', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 2].transAxes, + fontsize=5, + color='black' + ) + axes[1, 2].text( + 0.025, 0.9, 'z', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 2].transAxes, + fontsize=5, + color='black' + ) + + + fig.add_artist( + plt.Line2D( + [0, X], + [Z//2 + z_slice, Z//2 + z_slice], # from bottom to top in axis coordinates + transform=axes[1, 3].transData, # use axis coordinates + color="green", + lw=0.5, + ) + ) + fig.add_artist( + plt.Line2D( + [X//2, X//2], + [0, Z], # from bottom to top in axis coordinates + transform=axes[1, 3].transData, # use axis coordinates + color="red", + lw=0.5, + ) + ) + + rect = plt.Rectangle( + (0, 0), # lower-left corner + X, # width + Z, # height + linewidth=0.5, + edgecolor='blue', + facecolor='none', + transform=axes[1, 3].transData # use axis coordinates + ) + fig.add_artist(rect) + + axes[1, 3].text( + 0.1, 0.975, 'x', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 3].transAxes, + fontsize=5, + color='black' + ) + axes[1, 3].text( + 0.025, 0.9, 'z', + horizontalalignment='left', + verticalalignment='top', + transform=axes[1, 3].transAxes, + fontsize=5, + color='black' + ) + + + fig.savefig(filename, dpi=300, format="pdf", bbox_inches="tight") From b3ec8c6dcbd7a4095cc0c45ab43fb90e6b3496d7 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 13:44:22 -0700 Subject: [PATCH 44/72] temporarily turn off rotations --- waveorder/optics.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/waveorder/optics.py b/waveorder/optics.py index 6ae5c6c..389191b 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -258,25 +258,28 @@ def generate_vector_source_defocus_pupil( "zyx,yx->zyx", torch.fft.fft(defocus_pupil, dim=0), ill_pupil ).abs() # make this real - freq_shape = z_position_list.shape + x_frequencies.shape y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) - z_broadcast = np.sqrt(wavelength**(-2) - x_broadcast**2 - y_broadcast**2) + z_broadcast = np.sqrt(wavelength ** (-2) - x_broadcast**2 - y_broadcast**2) # Calculate rotation matrix rotations = rotation_matrix( z_broadcast, y_broadcast, x_broadcast, wavelength ).type(torch.complex64) + # TEMPORARY SIMPLIFY ROTATIONS "TURN OFF ROTATIONS" + # 3x2 IDENTITY MATRIX + rotations = torch.zeros_like(rotations) + rotations[1, 0, ...] = 1 + rotations[2, 1, ...] = 1 + # Main calculation in the frequency domain - source_pupil = ( - torch.einsum( - "ijzyx,j,zyx->izyx", rotations, input_jones, ill_pupil_3d - ) + source_pupil = torch.einsum( + "ijzyx,j,zyx->izyx", rotations, input_jones, ill_pupil_3d ) - + # Convert back to defocus pupil source_defocus_pupil = torch.fft.ifft(source_pupil, dim=-3) @@ -583,6 +586,7 @@ def gen_dyadic_Greens_tensor_z(fxx, fyy, G_fun_z, Pupil_support, lambda_in): G_tensor_z[i, i] += G_fun_z return G_tensor_z + def gen_Greens_function_real(img_size, ps, psz, lambda_in): """ From 8e1f2925b27140d0a8d3641bd8d375db3c56ce79 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 13:44:56 -0700 Subject: [PATCH 45/72] debugging progress --- .../inplane_oriented_thick_pol3d_vector.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index d6a6f26..5853c49 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -2,6 +2,7 @@ import numpy as np from torch import Tensor +from typing import Literal from torch.nn.functional import avg_pool3d from waveorder import optics, sampling, stokes, util from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer @@ -16,8 +17,8 @@ def generate_test_phantom(zyx_shape): margin=50, ) c00 = yx_star - c2_2 = -torch.sin(2 * yx_theta) * yx_star - c22 = -torch.cos(2 * yx_theta) * yx_star + c2_2 = -torch.sin(2 * yx_theta) * yx_star #torch.zeros_like(c00) + c22 = -torch.cos(2 * yx_theta) * yx_star #torch.zeros_like(c00) # # Put in a center slices of a 3D object center_slice_object = torch.stack((c00, c2_2, c22), dim=0) @@ -117,7 +118,8 @@ def _calculate_wrap_unsafe_transfer_function( swing, scheme=scheme ) - input_jones = torch.tensor([0.0 + 1.0j, 1.0 + 0j]) # circular + input_jones = torch.tensor([0.0 - 1.0j, 1.0 + 0j]) # circular + # input_jones = torch.tensor([0 + 0j, 1 + 0j]) # linear # Calculate frequencies y_frequencies, x_frequencies = util.generate_frequencies( @@ -131,6 +133,7 @@ def _calculate_wrap_unsafe_transfer_function( ) if invert_phase_contrast: z_position_list = torch.flip(z_position_list, dims=(0,)) + z_frequencies = torch.fft.fftfreq(z_total, d=z_pixel_size) # 2D pupils ill_pupil = optics.generate_pupil( @@ -195,13 +198,28 @@ def _calculate_wrap_unsafe_transfer_function( G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) S_3D = torch.fft.ifft(S, dim=-3) + # cleanup + freq_shape = z_position_list.shape + x_frequencies.shape + + z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) + y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) + x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) + + nu_rr = torch.sqrt(z_broadcast**2 + y_broadcast**2 + x_broadcast**2) + wavelength = wavelength_illumination / index_of_refraction_media + nu_max = (17 / 16) / (wavelength) + nu_min = (15 / 16) / (wavelength) + + mask = torch.logical_and(nu_rr < nu_max, nu_rr > nu_min) + + P_3D *= mask + G_3D *= mask + S_3D *= mask # Main part PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) - PG_3D /= torch.amax(torch.abs(PG_3D)) - PS_3D /= torch.amax(torch.abs(PS_3D)) pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) @@ -267,14 +285,17 @@ def apply_transfer_function( ) szyx_data = torch.fft.ifftn(sZYX_data, dim=(1, 2, 3)) - return (50 * szyx_data) + 0.1 * torch.randn(szyx_data.shape) + return 50 * szyx_data # + 0.1 * torch.randn(szyx_data.shape) def apply_inverse_transfer_function( szyx_data: Tensor, singular_system: tuple[Tensor], intensity_to_stokes_matrix: Tensor, + reconstruction_algorithm: Literal["Tikhonov", "TV"] = "Tikhonov", regularization_strength: float = 1e-3, + TV_rho_strength: float = 1e-3, + TV_iterations: int = 10, ): sZYX_data = torch.fft.fftn(szyx_data, dim=(1, 2, 3)) From 7968e75774ebb22bb6eb18f653f549acf48648b1 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 13:45:16 -0700 Subject: [PATCH 46/72] example script to generate matplotlib tf figures --- examples/models/plot-vector-tf.py | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 examples/models/plot-vector-tf.py diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py new file mode 100644 index 0000000..61f8b06 --- /dev/null +++ b/examples/models/plot-vector-tf.py @@ -0,0 +1,190 @@ +import torch +import os +from waveorder import util, optics +from waveorder.visuals.matplotlib_visuals import plot_transfer_function + +output_folder = "2024-10-07" +os.makedirs(output_folder, exist_ok=True) + +# Parameters +# all lengths must use consistent units e.g. um +zyx_shape = (101, 128, 128) # (101, 256, 256) +swing = 0.1 +scheme = "5-State" +yx_pixel_size = 6.5 / 63 +z_pixel_size = 0.15 +wavelength_illumination = 0.532 +z_padding = 0 +index_of_refraction_media = 1.3 +numerical_aperture_detection = 1.2 + +for i, numerical_aperture_illumination in enumerate([0.01, 0.5]): + file_suffix = str(i) + + input_jones = torch.tensor([0.0 - 1.0j, 1.0 + 0j]) # circular + + # Calculate frequencies + y_frequencies, x_frequencies = util.generate_frequencies( + zyx_shape[1:], yx_pixel_size + ) + radial_frequencies = torch.sqrt(x_frequencies**2 + y_frequencies**2) + + z_total = zyx_shape[0] + 2 * z_padding + z_position_list = torch.fft.ifftshift( + (torch.arange(z_total) - z_total // 2) * z_pixel_size + ) + z_frequencies = torch.fft.fftfreq(z_total, d=z_pixel_size) + + # 2D pupils + ill_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_illumination, + wavelength_illumination, + ) + det_pupil = optics.generate_pupil( + radial_frequencies, + numerical_aperture_detection, + wavelength_illumination, + ) + pupil = optics.generate_pupil( + radial_frequencies, + index_of_refraction_media, # largest possible NA + wavelength_illumination, + ) + + # Defocus pupils + defocus_pupil = optics.generate_propagation_kernel( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + ) + + greens_functions_z = optics.generate_greens_function_z( + radial_frequencies, + pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + axially_even=True, + ) + + # Calculate vector defocus pupils + S = optics.generate_vector_source_defocus_pupil( + x_frequencies, + y_frequencies, + z_position_list, + defocus_pupil, + input_jones, + ill_pupil, + wavelength_illumination / index_of_refraction_media, + ) + + # Simplified scalar pupil + P = optics.generate_propagation_kernel( + radial_frequencies, + det_pupil, + wavelength_illumination / index_of_refraction_media, + z_position_list, + ) + + G = optics.generate_defocus_greens_tensor( + x_frequencies, + y_frequencies, + greens_functions_z, + pupil, + lambda_in=wavelength_illumination / index_of_refraction_media, + ) + + P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) + G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) + S_3D = torch.fft.ifft(S, dim=-3) + + ## CANDIDATE FOR REMOVAL + # cleanup some ringing + freq_shape = z_position_list.shape + x_frequencies.shape + + z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) + y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) + x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) + + nu_rr = torch.sqrt(z_broadcast**2 + y_broadcast**2 + x_broadcast**2) + wavelength = wavelength_illumination / index_of_refraction_media + nu_max = (17 / 16) / (wavelength) + nu_min = (15 / 16) / (wavelength) + + mask = torch.logical_and(nu_rr < nu_max, nu_rr > nu_min) + + P_3D *= mask + G_3D *= mask + S_3D *= mask + + ## CANDIDATE FOR REMOVAL + + # Main transfer function calculation + PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) + PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) + + pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) + ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) + + H1 = torch.fft.ifftn( + torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), + dim=(-3, -2, -1), + ) + + H2 = torch.fft.ifftn( + torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), + dim=(-3, -2, -1), + ) + + H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components + # H_im = 1j * (H1 - H2) # ignore absorptive terms + + H_re /= torch.amax(torch.abs(H_re)) + + s_labels = [0, 1, 2] + s = util.pauli()[s_labels] # select s0, s1, and s2 (drop s3) + Y = util.gellmann()[[0, 4, 8]] + # select phase f00 and transverse linear isotropic terms 2-2, and f22 + + sfZYX_transfer_function = torch.einsum("sik,ikpjzyx,lpj->slzyx", s, H_re, Y) + + # Make plots + plot_transfer_function( + G_3D, + filename=os.path.join(output_folder, f"G_{file_suffix}.pdf"), + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + z_slice=-20, + s_labels=["Z", "Y", "X"], + f_labels=["Z", "Y", "X"], + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=0.1, + trim_edges=0, + ) + + plot_transfer_function( + S_3D[None], + filename=os.path.join(output_folder, f"S_{file_suffix}.pdf"), + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + z_slice=-35, + s_labels=[""], + f_labels=["Z", "Y", "X"], + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=0.5, + trim_edges=0, + ) + + plot_transfer_function( + sfZYX_transfer_function, + filename=os.path.join(output_folder, f"H_{file_suffix}.pdf"), + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + z_slice=-10, + s_labels=s_labels, + f_labels=[0, 4, 8], + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=0.2, + trim_edges=40, + ) From 7d6be60d18ae8fafdf328c6a8faba052960ebfe0 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 15:52:48 -0700 Subject: [PATCH 47/72] add rose asset --- waveorder/visuals/assets/rose.png | Bin 0 -> 72138 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 waveorder/visuals/assets/rose.png diff --git a/waveorder/visuals/assets/rose.png b/waveorder/visuals/assets/rose.png new file mode 100644 index 0000000000000000000000000000000000000000..616668c7fcc659ae50a45751c99ea1712aa6d366 GIT binary patch literal 72138 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

rY_NdBu?CaEvBd9Jvodw-9|E49Fu7nxqyBr2A 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 OekKMM`jvXlG5-UP^dX}F literal 0 HcmV?d00001 From c61c491fbfc10bddec6478d9f4a0b02ed1ca450f Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 16:07:09 -0700 Subject: [PATCH 48/72] add all assets --- waveorder/visuals/assets/gellman/0.png | Bin 0 -> 32170 bytes waveorder/visuals/assets/gellman/1.png | Bin 0 -> 20902 bytes waveorder/visuals/assets/gellman/2.png | Bin 0 -> 18505 bytes waveorder/visuals/assets/gellman/3.png | Bin 0 -> 21411 bytes waveorder/visuals/assets/gellman/4.png | Bin 0 -> 19764 bytes waveorder/visuals/assets/gellman/5.png | Bin 0 -> 25169 bytes waveorder/visuals/assets/gellman/6.png | Bin 0 -> 28108 bytes waveorder/visuals/assets/gellman/7.png | Bin 0 -> 25692 bytes waveorder/visuals/assets/gellman/8.png | Bin 0 -> 20552 bytes waveorder/visuals/assets/stokes/0.png | Bin 0 -> 20999 bytes waveorder/visuals/assets/stokes/1.png | Bin 0 -> 16008 bytes waveorder/visuals/assets/stokes/2.png | Bin 0 -> 17892 bytes waveorder/visuals/assets/stokes/3.png | Bin 0 -> 16017 bytes 13 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 waveorder/visuals/assets/gellman/0.png create mode 100644 waveorder/visuals/assets/gellman/1.png create mode 100644 waveorder/visuals/assets/gellman/2.png create mode 100644 waveorder/visuals/assets/gellman/3.png create mode 100644 waveorder/visuals/assets/gellman/4.png create mode 100644 waveorder/visuals/assets/gellman/5.png create mode 100644 waveorder/visuals/assets/gellman/6.png create mode 100644 waveorder/visuals/assets/gellman/7.png create mode 100644 waveorder/visuals/assets/gellman/8.png create mode 100644 waveorder/visuals/assets/stokes/0.png create mode 100644 waveorder/visuals/assets/stokes/1.png create mode 100644 waveorder/visuals/assets/stokes/2.png create mode 100644 waveorder/visuals/assets/stokes/3.png diff --git a/waveorder/visuals/assets/gellman/0.png b/waveorder/visuals/assets/gellman/0.png new file mode 100644 index 0000000000000000000000000000000000000000..30a1c8708c9b8facf630eb83886aef0350ad762b GIT binary patch literal 32170 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 literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/1.png b/waveorder/visuals/assets/gellman/1.png new file mode 100644 index 0000000000000000000000000000000000000000..07057c1c9ef9f1cdefc656d1c41c713f7a83ea8b GIT binary patch literal 20902 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 E0FVl5U;qFB literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/2.png b/waveorder/visuals/assets/gellman/2.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcf9f3edbbf531ae8dfdf2a00c08efc1ab39427 GIT binary patch literal 18505 zcmeHvWmJ{z)-ELoEj{qwx94jgMO?&pqq&$zB@u3&XFMM45v0t^fcLS-d64GauSH2e*{ zhzqaOgda}9kE_N?8mbr=?razsulz7Dj^L$Nix?PAd>9zZ1{fG(?=di_?31b=NWdGo zMt2qEFwT+x)9P|!;1zs(C2a=`jHElrU(60xA`%P?#rMi`(weTLYZET64|mV-He7y1 zIcki`u$QY>NA|u8PouOisSRP)zd&Bx?DXYom3qJ%=I`=%O^c`eHLKT`e{3jOT5OX1!Zt*~iwKeR<^Og>F>vxAyLC#V1dwUphGS)_Zxd zu(36djA%P*%LxaY2gtpt-5q?|{`2Pr4BdK9novq^Qa#eBpT85BqA-+|l@|w#=<@UP z%U5)Dbt7YA{TdtZ^v;uv(5KLl;&1#8M!ibHHot}?>MyW>|r3%0I;L`C@POAO> zgv->{VXDs=jm!9pz(s-J=LG1;+zVP-TInA9f0gdttDP6_?d?67jgT@hH)pTbWln7B z%#!0?jJYpqdGjLXRVF6P^L?Ll_G(S$#AZR+49e)#)QFxQ^||@^rHR45J~H#}6#w}6 z_{GxT=UA{zo%SCIf`kMz7(Es@-;28xFGdGH$34K&4>e||i)nR_e zwNWj{L%By0umRi#%l&x@7nm?KA3rvRe`efeFU3d6G7FAXJKb>pJ3f+?I#K5#B5XIw zb#ytXkbyUbG~t1k7Dcu5TFBho9NT)Paobx_ucMp&R$ms;Or<&!f-C}=#8M|E+R_&> zu3x`SUm4r}!_`d5gpD>FH$6Qa=J#_lzN}1;l#C2HklsLflemB07c~39r+gXfj-};w zvmfu7jv|8!DPaXareS4^lXc?x1qG@V_x)vFdmZn+7}sDHoQ7kfxtA<9kOhy5W->^-n7)pM}kWBjm7)C-non37&v-Cw>#!AQ0a?1?jgm$ZqACc`iZyBZD zrrh?ZaJ}2TUVluqKjqV~h-NQ!h216#3J-6o_d3=&agvphu|L|e%+=)VwZn{(pEcFU zFdHo~^o_iV^Xd{6k3B4$3>N-CFj@2k*5gn51KK(29JFgk_Pfhks4HUSlSjM_C!{ko zGh!?KRzgWhNw!~k<5c`#y~60V!;X?SrY6OYOh~}@@i}*2iD0-L^8P*bKQ;~9LV71% z{bp6llxpxd1Nc}YA4@b<0Nezapqn8zX~#u$q!J2wp;Ancg_)l^Kn`m|;CfUoA1W9J zexc(MypI9z|EDh~z{p0(pwq)ya$C)OETpgEm4oN}4V73h;1T-BgW>J}^aTNAWM4xi zIXU477+Og-Ud7E!i3JxPaT!+OiOc|5Hr{{fh5udF*MXaAa{XJdj5o;s{nsD#e`X)l z{)ZpdFay`*|3BmZagLDz&5=Qfxp!Pdf(2st&ESZ)n!q;vz$zHQ_@&{b{^J)!h5vQZ zCVt>W{;}>EQw`?Be;x=IjTkKMDm?4I^#V!MPvojY==iS4YV z?6cqFZipT4ZEZEDN&A*}68K{=Ffgd+Y1%tGE36C_&CJc=n46oITMZEK@bFj+x)WGE+T9U2_fj6 z7he8ju(P%GrlCRV1Y-teaXwqBa{>n z6twwEnyr#8qpp5g%y~7}%g@ix;(TggK+7>$jyu3m=}~rRDd|FYsv=|t(b3WNe}2%g zu(C2;zmA`sot@EH#^LSly*U0=5N7sb%%(EkVWF#Ptzv9?ex9}ZCUcB2b#x0ve_B#f zQu~vG7i&J7~C! z{>fKXB_v*n5t?m1D!kKlv@5ofl#wxges&zmdE>$HP@Q~UKEaokmfWw-gkMZebGfrC&%M;{D^-sR$&v$qgQ=ZOpKPFpFb!#SQ;|Db**Um zGAcccD5meyYV@~}=E8isminqnZ_9y}?NAbK2EQ-9m+rYZ!S8XD?uGtIl_ zAG6X1gF3&rw6qL2_(<)oPkqu`n4R_YK0TV*8?#e9KRfNsR3y&T%zx+MyMpA&Dk>_z z5xKb>&xcE-pV+W4pd(F~Zr{EFDeHiw#>0n+<&6Hx6T6Y2VSZu)q{~4UbH!Z(IGM4W zi*$k|e~7hwU`G*pyTvUZ9g~st8eEBs=Si2ijz6EHmd<<~-xT@5Pa@>+kGm!%E>4GJ zXMcVsV(QwJ^c6hjMKMTF`{EMHOx1hMEG}9^8|M`j$-BFY@#xnEg;Mb{3khj0ocI0u zMO)`t|9qsB(ql?F#kD)qkAs7w5t6(T^KORKi5e{ zw(p;8poxd;y~K&=MGbUisj^9z+m)(zR`BAzPp8KVs5GOZ)Sqm5F(Wb#y|K#a)abTg52ek60b6;?GXc zi5lv?=3NrXzT!M%6Jk9UwwZQbud}b&*I#=P%g&lU*N7Q=vPY*vM1M#A<;%?D_Rdap zh0T~wxh)kHmBis(%$rG}XtDsN1Wj#i8YmEg3F(BVkJlR%rKMj>OXWDed~^5w%)V}g zr>eEJ`KyZ^2R*$U<6G^70?cqO4)US(5e0?jm1pNz7@7-nX|C(~&>J|C+fobucztgxGU`}?=isZ^GF<1IE^TiL*7oA%39 zJ8PHx8{Y-UJaM2FZmhR?>KaCa9l>vmfq@vG>%J}d+K6w4UX8lfu2tyJ&bUK&u=i6X zHtK+W6U7LImJdSK8v~D@F^PygUf)xpqJlYR475ktGfL7EVPa+wOLNg(LRZuKzL~!D zf?0xH4ads*!H39KzZrex_TF5`<=+UAaX?Y?VNBGxP@M`#-s3}caKI^{A&h*soe=Y_ zmO@T8;z4H5@@u?l0fSejBCh;G!lMU?8u$wgx2MN5@g&Z7d0AXm0`d#F%IfE~yQO%K za+MVhI^^87;0&S0ee&7p!rtGBg36XmP8OUZ2KS2$nF>v?v)+qz=zuPiT}!aDXg z6ZL}+g^!{B;K6IBmBATbV#&s(z8t%{bQ#RbCf}J`hFChcH!xzmx-#xo>Y~uC*U2r; z4;PdqUtq!EQynlPQpWC+w&c591 z&{Qtvc4#CV&q^DE@nvBuv^uXiF|p%t6^RNvd(7H4-~~ugA3$^^B-Q&!d50+)&df&B z*;!cMeTfwZ56^gSb)?)F0xQ%GsYkvY_Pt!-$MAwGFR(_PtJp~2akM|PZfJZl&Y4+zhw8k1qyspXV zs`ma>5E*-qWj6z2s2Awy-ZRkkQ2#joy3pgk`lM`C9DIMKLb!~h<4s}P@pd0ZpDO>y#Z0opW?=&t6W?tNFr=DUgjFx^I=i)Dwt@H z)~33(W`&ctLv@*%SK}yM(=BQJtJ_^QrSQ}TY!atPFSLDoVMfSO!`HW&@wPiT;)elM z2`6J$3zBpc5DZACfbyCQvJ`%|EfW(VzN|P-3Hr6ipA_c4I#`e~+Ie7`bY4K0DC+tbTda>*zXjOf=kUBj5CczWY+?;167nmU5yJ60RR@bljs3 zc*Jt+77YM7j0)kj{+9^vMa;sr*-Q0#8`PIGbIV(z4L^=)GDqOvbQ>f3y4dLSTHWps zwVC11*GA8$&h>~}*i&*vUdgGd5z&uzUXI~W&yJCxT*)MJ;hhX72n>pXn1=?JmZw= z+11~3%C2Dydh3T78O-+*g$ebl9Ss@fZ=m_g?dgP{-?6aZnDpHK=^k!O&Pg$Pi>s@iAqwp(fMW9j~6@oNW(c9%=7sgh{l|GAE=eB6P!HG*-< zo>8w<6eW8pE;%_I(uLWXKt=`luWn%?kXZ3s-ouS&RhOMpQZhQTaZ7|Ox81@R2-g)p z8fl1%HLv95e5m!QtXWq$_-u?=X^Gv~qlH^=Yn<0~9RH5fK&ni3`bb@nAI05sw7b%q zeV5Afc&&0b<)OddRhUoD=JEP1ov?9!lq$19FLTw-`qZ=oL#85nvMtArH%+tIp&3Vx zjXhIUSGR5q1pw1*YdE^Z5FPpM9ox}b8k-TdPGn&rR~Y>*(g=wyrMiUqI5oA6L~*K_ z95o@=PrfEWh8Cm*cz8HoyIQ`F@6@Jx4Vrv1UYL3PiuLYg_XED9N;c*Nia@4kc|dGE z@1f%kcQUfb@CUUr$_$&?pW`thDL1tnXxrT}Fdo6z-rl50JCA3I)+-F(UWc_dcFRcaf> ztd{cJ-nCLSQJ9I7lc3@3s9>?uAN7qd_H$Mi%g$0C-X%(|J2o~&T@JRg0at(TSeARu zQ>f)zrw_ks-8q@`rnB|MLwBLE86qUD_J$l__U*-b7L}fEb!FdfJT5lqNt1prVy9o% z@GP3qJLG$RYdBqRjmx?_`VKfYoRbBqiz8nyfd|Rx%=%+M{UNEji)d}A@O8KKr!V84 zi3AsF+3qWR>xi+Z5mjHvgL>gd(k(KVDKD40;}aHMlx#v23qH)Saj`e^fu?5Gb`o4> zLp+Zng6}?BB^ddwvif^ffa40(H(LKu?tEjAQNUUfNGT3S+z z+Fu{RpeY$47zEAs;WgTo9L*q8*&W8#sm%z54%sW>EaT}9YkHT z|2y&2G1!Es_MX)#Il)0!f-lremHNIZ;dCNK!zG4Q&TAhQIRUv%f5-ha0CDwD%!#ek zrMlrJ@2y&%@oBJFuj&|zOSFbbFMeuhhNLrj7xMZRJ(r0VH&WAAh8*q9{+Bi(LrVYn z@y~i3!G)7Q-HOwtrb*U4ln^6B9^OMW6J2>zZT&cTw=IIPZLMd?+f$Y_XzlqW)m^l8i}OLoBxG20QWh(N|cZJ)y}Q>03W z=g}k8%TP67^8pzqgW|prE}t@|CQs&OMkXLe7vnS|ivw)eCps=lrSp?pKEeKF2bgK->V`eYRA7>lI!BIhv`BYk+9b`yclblBZAM9%?8eYDbheLA@-9M^ zu%~)5S4$MM@!iA*Bb1+TFY8%m~TvxI$*l zd^gjrhl*!{*;9HBO`SG&*J#Pfmyw zi~dylJv7fB8`?!o@2;%;bXCQ9WvJwS*p-C8KZAA}{F!o_j{SZLx!#8XL+TIhB2%8e zU#y3g3%A|YgsM=~S7A~&D%e=PVpVX8w6XD|q}K8QjR|9vVt{flqo9lv)&VTWuEP5l ze+Mo}I{m#ux&(+kO=DLd*_!0>J@bjp**1Q=Nr~QUZHj<&{Mxgl6`+^~cy1&olaxDi zlM@kT+`UF)Y^@;>d%Q8Ti@U2|Vt@&}f_%K05Ou&$M}RCmA1yphoDyHZL&3lU^AQhV zqG>vpI)81f9_joQ;2Wq5$n@R~7*Gq#+SqWX_?)>V4`w1I@9An841O1|`)k8rYpxk| zkS03;N`l^78-w8~VEB!x^Rve;niu`C*4}Z?&OZ=9(e*?|>733oB1K@b)59D!Q)5~( z82SO^E`Pd`3(C42R-aU%=;63Z$t47{*e*V)eX?+Af27R(v-_@9uc@{~fFYUJ;e5Q9 z$fr0fA#fa2dXyyEx18i~YFB=k@cW#3CR_e)cSWJufS_p_vuh|S>$3jE;#|z0St4{@ zD6Tt=bh zt=Dc7wQ<|v!sEG~R@^fmS(!Q?q+xo~4p6F9Kt*4UY~Ca5_-?2Pik?=&s7Cy#@Yu-6 zw7xf*U>y~_^&Hh)rE>6DTsUS|wWQp#V%t=60SFUHPUcC@jQm(cuU}_$GT;4HOwoUD zlD9T(+oK#Ze%)$EWk+qwU_%Q(NWMlGIj zwJfCi@Kke!e&f5@42871u5O{;BY(ff(-aT6c=>lBhDvPHyE}biozFZsFMQ&=iHYvH zNGD?3uWftVxv=YAznlVi7<|a|2%(Z$+nVo;kBW@E3In3=zr>DZ_cib7BNb-igHvW|iGX7ChtsJ#~N(HV`ljnD2*?vpD%<2-T zc|sjwXpsh$-4a)&I~0zrQU1o*v;q|S>9Xnej{Q9BEG$>Kxm97w{_r_$JfcOe8saOQ z)hx`+7_R{Jv#+y^#(=cW=iD2tfjl4`li<>&7h7}6Yo*PT4F5RFP_8T8s185fzg4p4 zhZ{5KjJu^AFfn>DMvBG4?|hVhzQ;g* zTG|~)WlFTZBj!uM!6wFG6_kZQaF2|PRJreRW{v7hw|?spIn`%nWo^pQ6EC;y`!l#t z5loxr=sc*+bK?e?UUZ8A_414D6Mu{}6SyFjH)%~wOwy{WkB&nAPQ1p&)hx!0jEZ_) zTPuNsgVQ)Mq35_{!bBaQ+Q!$PnUy7H{`D^}U6drG41hy_+ONsFFE4)qu+r(jf8PS> zEjoGgvfu;RWpBy2cke=YI$$$*bB{4uHob{xa`W>EXlQ5vJ+vG2hEtCMo*c|P!)B~P z{iTd$HHF7S#15yoK24ZmZlF%lpms4gWI3cANi53X6y?p#*sQFrcWtNgwQlqe4i45x zd6kWh>X?|CGV$^f0}{QlvqSFno<;VHlXu{_>(e#^lN}??Ag2NVCG4tu zr4wN91_oD?&a2C{o;(REFE3YUiz&>{Z-y(A-fMCB@@4h){g~%ZRg?MVPR-E#s9pPF z2g3SyhDwCFxw&@$YlP|V+OAAb_e_r@*VKqZX-Oznz{SJU3QlNdX$c?D_u;Ya`w7db zG}SMiulR2ADn$$yJze2?Jdt5XJ;Ih;OzJMyd4rt56&zG|_xihi$ki@Dm5NPFObl3^ zOlI%+OCNv`VQBzpZ0zp(S5}JD7!6l_5ZljjGyBI@*2XFWWzu`Rd$jcGQT>Bh&%YUL zFFVCq_hy^1EK%x$|l< znhRH|cUQe&0G>3!GxRQgd$p$(j_~*RSBEOk!_vvCnt7UwP_Y6}Fq~CP($&><@zN#P zwkWMbmjBCED@ONkzZliBdnHyUSDYXd2^ z1{>JL+DxBUcsG;jr|JXUU zD5dHtSCU>wi(K(gq^Df3R(YR@ z7y>hMB+&j~b_z8{;}@Om?CcJXj({U5mbYfzzHzuFlDyOa*vADtJUsA!h#xUBGMb*A zPP;Oid-8*(pPaJ!?ay~bY7nbmzI+*(oNNq=OEs^W93B8JmAp_FoXg_3h)V;ygJwoR{&lqnE1sBLNO3+DNppCNAd_jw5SH^{;B%568}uef?Tp%`5>3%IKFqkEvhpXW85ex;kZ-)*GzjDF`k=NQ8}^@Hsyf>l~_h z-uqSZ{=2sKf-K_V;s?ja*O-}Q_A=c$!1UeRn3$QH(~7UOCXvKQP zTqGhI9IM`F^=qbYX?@vGNs)e#lj=Bld!rHiUvcqT|LKLkLj9i|-rV29iQQz6#wIMn zjM{19ST{E|?wFXcczSv5rp!=+m*M9pSz21c{n?@Pd+vPQM{K*$83ZJThKBzh3Bed_ zM$3D79-Eubj=D?*^MukG{+Mr|6I75>P|#VM`wDBJe!0HgIzRXAo1Cx0BoqHjsY5yQ z!(K&33cYA~BWl{F-d>W~wum9^_{x{ri=~T{aLOQ)A^gipLPEln2qxp4sr6h*DM(t6 zpXXV9*!DtqW?|ty$0f&@Xly+7cadp&&i3|=e)wc^@upGaq=^wUtbpW1MMe4eRB^@y zNjTu|ffDk;=(QP&h<3S{gVr>YI_ z`vpQmLLy;lJz*%son5?Gl24iOC#%;2;-6v9;?2K*{{oO4JD)2tn44bKyt^`lAX=1q zA6ks4g?oXJauTcHq^G~^{Tru0Dk|e{8GdUQXVdGv#E<_Qk@F`NlvJ{#$p|VVJIUDY zY`0L2&I3LK&hhoShaev+Q7M{5PF{YdTk5>O*nv)hn#oD5A=*;N^bx+AcE9I4zU;Un z4nQ;)j&@!<_Hn<468>d>f*CwtcETeNqsMUDto*1=N-o|k4h#SY-7gOV7V~|nsHiv#Aj6 zdf?$nNOJDWb`WF47(T{#1A1*h_{lF3F~aUH)f;v#1iA?l+BJph90-#(?lhzaONWHr zOw8pkeYN9K#+yrMY-V;AT}=H`JvlKn zE9j!;)XqMi?YL;6VI@xtpf`;-KGnY2pY&K?r%+*)0bv|Zl4Z5>uvT*^ITh8l+qWmu zdM$@DDwlX9w}QInFyNR#CFj@B(6IQc5DVRd6A~KQ3umA8@uSsHUUT#1lt_&1va-c3 zE`l1BCv3{m<@=6=OCkS`(sgAPJx(8Z>l^3A(1qOkwbYxNn=ox!sNA4Rd3KqOuE2pf zTUD1H>m3(~-UoSu$M`Gi4<91#bned|+Hnfs_zJqm2_Hh{ayk0PcPjbqr|$sm7NK|j z9w3G?i1IK)KJ-jKT|Et5Ih8Sx`yk_P47YP!i5g(@N_X!XXXcni=jYo?3R5DUTyFvL zD8Q|+ii$pQbbACjvj5?zmir+ig zZa+ORPIfAI{Ha*PgGh)G-SZ96k)53$uiRvV-v}!~2d2**gk!)?4vvc+cXxE~SKSCm zO-=pz46DCT$G?+|eYwyBIKaX8FXMTj-x?tFgKPYyvrGN9Jc*>~g5^z$^C4+=g_^P7b&ZvFT^eAj4|(=ahC zIdbF!J9#lTPwSzyuli9Nqu2^iimE?82;GI_fYQe-I5?P^&)`*8q!vLO0fMz0mw;mz zyyYC1_L1-7gE*Zcg*$gJ#>XKyw-L}VW!3oGz$h>7vRs%d`hpqoeapBRUJ*c-><=~! zQM5M^@NLwIKvB{0VDD*F1pW1DiJ0pg97v8?qoN)&^4qT&1_4vQSnRc*+;v29O~C++ z>XDD-6CUaSZWD9>A+?}cSl20uzluLmqHvSfqI&PV(4`uc$m=7&{F*@-Q6*|cPT;cI zqwNEL`D`1bk6%no%p$1`F1qAGkzNfY8QZ-ce7S6$@M%IiuC3^$@M2PW_s}0rqsT^Z z%P&ivz0MFLd*iY`iO~D(?xFc&OwxjY)pK720(K!0QQUSfAfx1Kji#5*nz*oeOOvImkDXg)Y@Q{0ZXu^k9`! zntA*H>s>;EWe_<^GfdTTi7kBO z7JzlQ&P6FY>Lg&@DL7tN?5TEA|DLua3Oh$nBbZ(Sc@!2lw*Rwqj?pids1zdx6GHGY z0#Gz>fW|I)kO0+1)7il+F^W;LcG5LDzl6_mxR%E1HJX=#oc!vwYrcX+kyk-QKKkZ$$u;7mpY%c> z?N3c*?Ar5)m9o4{QLn0~qK;=*lat04RQ=Y{Vz~LcrSz+x^$K->KvZ0ue;}hbb;HEX zV0H`yvp)WlxB&=N*`#7@e!&Cmk^&rUuCHFf1)@F-dBl=ITZw-}E)-8aJYUR=;IsV2 z(rZUE;b+KkZD3&Fy|4}KJ03lq^;<*3PY|39&&MaGp_oNk$!$|U)^Rmx^fmUbZFey# zi}}m<>Qx4(?z#y#@r9gdliWN!W>-Enw1WOYu0d5353L^XsPM^aDt*`VNyaj-L}7~t zD~j7JyxTv^daus_6#_U{s}%eBAu(G2=?~K#RTO4Xm{w@CQO?-Q;qC72f2l~4Xx)wmwzeEl$letN|M-xFgll0d^Hz^Ml6*5lqwe0`)wdHyVAB4O0W~^Yk%I2sw=4+`} zefko2+tnBOr(~^sMrKBTv4dJVY+e`yAS(5_;6pUUwBq^@H@j;}!)WjEya9WN_ z8nq`|oxAI=IpQ|lm#^b^ZeYgZ{=k`p=%S1jK!{yh>R9n*Wr#T0dG!_9)8ObhzBa#R z)3%7&t#82`ca?JYbpff`MDz$ZZ))zO$6!(iQ2uU@x`x2Q+MkXuVB}t7?y;R$sT%k8 z@Rb^~xKZPWWAOVMu21o<)Om0kZv)P-PHbU&IOd;~y)~PlB#laxYc#%J!e1#@7Td8Ht#`{eg#O!FUBCv$Ne#KIv6e)IKMhfoem6P#Rf0 zDw3JczH7fU#rVB}XB|Z1*TCIAF4q0()LAWLT|6cD{LW$C!&fX#Lc))BX2=r4H1hnQ zVge8UKwp1y@gy;tB|SIyB2>Gej)OlOCubg6XS0kP;}vU1adC+VaEP1f@8L4`@L&?U#rOc)&6XETbmyuKK_VaOqW-&wQLs z7RUZni{-fD-0798mUO2j=Kiu5H+VfU@AKQ^f4?e%s5?PE(|F@uJa(!NQK3HJO?!V< zV6&mH`gRBVXffIo#vBI{@U3~O7bP&rMdw*Em?&DDZ!MT>W7dzS7C9fBwbBvp1zH6G z#hc!6f`#ZU0`Hj!i;)ow4Prg5#OjP6vNpigm7=4U^c1Z>JF*s~>vFiDSPN!r_2{DK*N(bj%rUvW5H*4|=B4Vkmn z-aGDg_d`=9!ts#YmExMe&CnLnQxsLe$KnH2E~_$W{psFQ#0=tjp9wFT$J*Ljhrw_qEC;*k8Wja1Vg}~hjxiKm6dP+ zX>116I%xnzGD%7@9EYFy)-@G7S&QQ0i42Q;zl`W8Mm}Q`U_P9=oFLnAcG~_yoJt;4 zBnP#1b#>Bxa&GSKMuUagh{R#!Lw|F-wV0DtEI=Bbo)Yf+e~Uye>gnntsU-9*e3^8B z0HjJJgTXObL-lyLI=1i*jHpC&i*x~(=sDlTwY4>V@O}W$;vM+YtNTl#<}OsPz*W$icO^A}FvuP{CAgl|)7OiH z3t16-&?<<8R*{S;TISh-!HkXL^%u5{7)f-Zm;I-Fp4pLsu_5j{=YDD{7#)F+kI&BE z2|7GthHuLQ1-+1gD?CGVeb5bp2h6VJh7F1|G+x=%7H)h{L_}NOQ^&9d?R)o7;HN{z z=%|A~mY0(w9F50%UEu2K^FHz%^)&ET8*^2mCYV-t@1|>);XbC~Rgk|%XpE>?ZaMt9 z=}!3`nnd7V1|y}W?!2jD;^MQ}@%qgGqg!p|Wwbj&TydKJPJs{-Pdv6UMk_(n>7O8I zPV1No(A3qdR~yqXLkZLE?eDbNj7>Z5Ph?C9WZ-IYpCTHDjXT9JeW z^b3mv1@TSCYAPztu)ie^3u<*^0e*f4XD5f$x82JZEB$DJk4^aXOQRMt|0g8|uO}N{ zLV+sSE#X@^>3*q5=ZgSHQ@xLiI?k!r#wU80;-5|T*VUBlhbslLq+4_|%mE<>Dbkg! zrzk~eiP&t!CPtncWTn733XncC4-XL)uYP~c+H{leXOj-napy6cj49S?e6+ekh3!NL zL@G?+3xCzvJUmZRh+qh}Yd9U8`yvhi!|Js3o-|8(zJo*h;qOaG7!Pq+u>G2>Q`B1Z zVwTVJJ2@y6Mp!=SfeQG-CCcf#{YhDEZSBml$pdC_m%qAI4rbKa5LfPitn~NnETNUx zVA5S~fM@~zvUJ@eAX9f)dRa%D@!C93?&DRh$C-<5j8P5F+yszaYCd?;Y2)STxv^T- z{X4-tg@4Rs0C;F%2?L}nk-NS5C(-UV#IQFS9eZKSz_0$Yxaqq6Qv}iKtRz4CNiH(e zaI}0w0&*3z+ibu>%LW!bO}8*15@mjfl$M7pgG}cQ=ki}t6bwQ0<2vI{wE>A*p$7@*M!&L@7yEJ| zBHisyin`zPW4l#a)H?V=3P1eWc=POd9o^QZs8m-GW;omox+~x74?j}x*zmKl;og16 z)$|Gz`@?DVP_dqGL?N>h&mE~f+8F6dM@jyb~XkY4wg0ZP${*bw;ml$GyCp@Tu zm0V)GpAzmKMitPilFO{S1o*wO9r* z9OdqLSqzBW)B+|K8N3e+5otx~SJWsunEF4gKTL4B4iW7J`J5k5fkuSkpE}7l4*2LE z4Oso|hR$A8k`eH#fzN!D4P9_!`zs82HL$oR-PKQwIQbF#{l_`&4`)APv*M8XES}--h7~Q*|=;lar^-^)SJ{IG#3uI^|;uc1QC5LfgPpaD`iXqnS6 z2pl_LT;l?Mu_~<1XtYz;UPfl-&zFbFnY{);{}2DG1rI-5!MMMB%+0kCn0wLv5rT}3_;v|5~c%TJp1@9UF-ad2Rc1N05yz`r=i%peF^6u&G_aMG#!;nqB8QbF{O$TV42w77NV8Hw1ow`k9lv zsN)!gLiGncd;Xm3&6_uJj2~A|!RZEthN9DD19xocXlR04LaAm!3PPe+fuOb>!^NZk z?tBAl!v-uL)U>gndY}g{jD-K!*m(BSjhMjx2;1eTu+n2=BN9Q_5YL!Li#l|KW3$UOiR-~c7a$7=W(>R77Fcw8P6KL0n?`R8m7#vsGx+J z9etO#&Mzs6zHTgnnjZ@(N*bP111*j7)iJI_3q&lUo(FtTA%FCO2A9O==dc-~2S?3F zzr)#?{l$w_XB*qT$(16jb+rcTYlPywyu%$O@|l|2!B*k8LO!=ak1lTtT@Gbs58_hZ zLWjdj=dD}rZCf`3H1g!2*8*gtJ!R(ich<(JZoBVH%T=&FQdfTk>Ln^hsD3!@_*n-C zlhlV@5vEtloBqX%7q~!W4HaJjpfwQcz$=Joa`7f1M?iR6oM~DM@M2=%T@sGFBKMC=kn9k zb~|mz&dx2$y@Rxkgb?(TQ=DG$q$RWp4gGoMEq?13(#{g}_N@Z!Dl+*x50^xXwd+*S z|M_0PlmJlDbeE^z3^Z>IhcA8w^rtm0HufsC#vr#?60rCeOTSp_rgE3W;H2f`BrgyyrxA>~=ASFerTqqMErciWUK&uc2zUH)o&dmO!1>>Xg`xh%sNP=y zCRF|D(;L+$SHOV+x!Ny@@RN|Q0;9!z^(qFm7H#bB2ZC>;bJV6W)~QT1X2t-j`y@2m zG4ASN6-kJ{zdr{Lj{;OufXO}prnR(G0O^7T(feo**Nq!}mal<{49m~wtg5Q&Pt0%Y z?S0_E*flY6)ZaYB>Tw&>&uq?MZ^nbwCi7a+{ zLN+XUvLOqa&u5M!r3TxEu9*ejkbD>jMTN?`Uaj9CK?^_%kYB+;%lyuAg%Ff~Lz?FM z>C>mhU;d>_eBvvPH)^duve-y2 zOcMaYHbqE|ybtH8>c&bf1xU0@p}!ZwW}sQdy^@fSum#b;0{|MzQ`0h{NnKiiLo$*N zsp#*+h151+ns;r*P1-L*eMt_qOGc+Hzc5LFNPyr6NiVTt{knV?59he6%+@A9?4yZ@ zh+O|9m_Ij@F)SdKHP(3ypaaRs8zhQBrU7xWZqnxT^b}Fg6r`#TTcIQMSc8ISD?RpY zzqPf^Lez4vTpK8$`e&0MggK6w89EtStOlU~ag(bkyiP{It6Oy$q8Wu=wX(L>KXZN1 z2cF0XsKX~;UuIAC#@XKEI04OH)1(U9uZj@9d-v{rLsEHwq0J1NRekqu|>gep;3L)Xe-|<_xOeDSB zJF6pPNQ2X(Px^$6x5@O`Sy^Z5kN=_p+aRoFW;dsFGixsnveab#(~OgvC!Hbf#j&ZxAXXAfHawG}2-JFQ*a|5`q-D_H9>O%9U*7 z1Kyep+S8R(R0LL@4~bN(K)6wZZx}%K z&cq!N)xkc%EzNKlduNgLL|Z!;8qI)?qCmpRdSBr0>GS*xZbRgK{k)GsuDm+(kIi zN+b6ixTp+4AQA9$zo2?P$yb#)Jq-C zk=2=}zzGOcn`NRAO>V!3JptU*$xp{^b&oh}NudaU}m-o zf#y?7D0`wo6?2@hK0SRE8ugUHL0Q{B>%P68)I_rbSRjp7PLtu22M?H>mis%VWnxvb zcc9y9Wz`sFA~wNK>w$1}{}?SRP{LfmC3dBC`+!nq3G&Z4*uQcqpXL7RzO;`g$NSQ< zvc63pp@`4{)f$T)h-b|&Dl&!p%^c059PPY5$?XJ%Lz)Cq*E?=ZODjjSECG5XAm$4? zKx=zFT*9_@|NH$1s#70#5f3m7-wy%b%|JKN^$X996|4!8N>^ByyMuhXx|Z5C7m~yL;C*M+Aj5vw%MPeRfY!&_$#n23i#&v?p<36%F7X5KsV*h@S7T z9>8BBM#(TJa)s-Y>#wM$N>0p4Lso6g1(YC2Kus=_l8jWrPzK=L5mpQ;Z`JB`&>w+= zT}fWc?d5|LyXJkg#2!PDt;xv`AQUX&8f5i*dwXoREtr{0??LY`aQ1D7+Y9_2d(Q!+ zh*>*^>S4I*h4JhspMuUTkw3veq9%0^=u|wXNpbr_6SEc(6jazp_001%J{jBl=ROeb z8v%w%dsIN@dG^jc0A$s|mY1;_0Z|HqFdFkW20nk+$w`QkTgT*=7ckLC(O&6U-^BN$ zqoWC2nei4j;<^FxHrYBQC5hTSfNB#F;Ki)FOxC&qB@dD_T;3R{`e|-FD&*^wuCA%M zcI(#2oQQI8Q2W;W+}u^regI5sHgZ1K5%byeaLavVC7C8g(rYwLl13F#RKXNK*qqDB znS6!s=P;~*FQkC;`wbaLVP*Yf!p!acabHHCliotS0!_{rU_F3 zrz5~eGhMru+45{3z63-@U!UPsL$f`2L^GSZzW4H>lwS^Lu9J{-puzWj25-akDfwRr z90mqC*jFrMm_LNQ0+;~u3x?=((Zh?Gnbi1TV+Q(1YD$J6cWHwNhJOsEKw|ZU7V;z2 z(WtUAhZsCft%3o2h=0&X24@xw9j*i(vOz()rr^9l`9cTTc8~Tm(D(pJ;kzG`nrgRE z0R=fUulMu7MKeCySvv5sOH&+4rt4mXFYcjnlz-y*(9X=!#qgymhKq{}kEN}(gR!B# ZDUaPt^TfXrwD2JeWqCEZVi|)s{{xltQNREI literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/3.png b/waveorder/visuals/assets/gellman/3.png new file mode 100644 index 0000000000000000000000000000000000000000..e74c80702d07bd5da2df59d0fe25c5f2bbb885e7 GIT binary patch literal 21411 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 SIq2Xaxr6S$Zq=?)Xa57>=W`PP literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/4.png b/waveorder/visuals/assets/gellman/4.png new file mode 100644 index 0000000000000000000000000000000000000000..519b0e04ea87ff4fea8ede023e00d894c55c88bb GIT binary patch literal 19764 zcmdSBWmr~Q)HO^9C=H@CNGT%HAuS=LAR?l4cS|=&cZmqnAtl{iQX<_A0s_+A@Xhkvul9w+M(m5RRT0=szXG22zr;UWf{}l;|*gCQHjUfC0UH7Gw z1kw%S-_(Ze7&vmrT3W>x2`TY8;tz5+11g;NSR1hV*b~m_@^;~*5+6T)#3vyM{Kji#Iy*Kzd^ad4Xm@{~P=hDx z-9kJ~RM78aA+_uWw~%6G{qTfOpWAgyDEk=sOFYCUCJvwyavAw2I)$&MruOsa&s5iw z?ev~UrA~sJ^c>#fTAmt34{mw=`XyU`eZG7WvVWc(tS+vtF`iooGT(=(2{m5%6)T4+h9oU7 z|F=F%Lb3zqlkQEf(rs*PU)e0v{l>(kBmx+i!`4)gFf(n)PsGpT`E0+7BIYwBgx&V< ztHA&X{StV9`nQN#)SpLC2`Q+km=wD+5n)mYxqLqOLP&~?xYWYYxE8N5K8hE;%vWJ| z{=B?A32LgSb{!Hz(g{3~OD|LmToQx%l9H0sfB8ic)U;9Uiw26cs7$P^QdO3-k)J>7 zmyhr6?TLzsp$G~JN>H;zwIk*e`Kemv?;O$FNRyM3UwN&P*Tfl084(i}xw&a>w>?AZ z?CdP*`4lmu9UbXvRhH>^-@0fKPwKg0Z-PPegIkupYUh8ySp^I0(}uLb{h>Z{Kj(5e z*|ykEP8PX7rK56QCKwEOt$$47FM-Zxx26EMHL_epj*stku-f0&(jt1O_FR7qCOjvo z(qj4{{cYrhp{$pQRiC?%U|L2bjQSE=jyA@*r-(3L_=r;%YBhxiPU+~Vm`J|>k z2;-5Gl6uC+H(JMvfq`*)b{6D89{8KAEmGPmG!(nsa<*3aBKK`J{{8z8YXxcZ+j=r3 zP#B4V`KQn79!HVD8bF4T?YFYg6ME@L)3d?>Qkd1C7C4aF2scFN1Ba1VV+gyk;G9VQ zG*R_zur0Jn{>KOOec{aLh?Dlo$^Lg`WS9nNO&AXfTw)ov5A2FqfTc(Eb zh$1Haf9U`%ZGI=>)NoV(Tc`ZL#`*vIYy89ff0r^HZ14Zp^likN{@;H1w-#sq&y$WO zNGAP1yT~_ACTJP4iEhI}6Gp5+#Nz&6Iv^pT9Dz7B#K`}xQ~uxMl-AIZ{%^nNBi7h| z8wU|x&macefxr=s2fOD1Eb9O9fy8g0|C=F>RtP+ZE&FMgEzQlF}_CX=&-_ zs;UVcjSUSA>~?Dd37rlO4%YVeelan36B82`78j9;iHRZfw#?2_T3T9KJ30o$#l7z( zHmalimMC*J>~1FMXy1_$CG|$Pn!X~4 z@cDHT>uYO)$6M1GdY$3iTFA)A+@6j$HUt{9QQh$S>7V1`mj1Md03s51a}#7yul71U zb^6)anNeN+!Oqz*3$C^{loitPe!Ng)aee)9Ef=ki9!d1%ubv)pb8}|-6k&~nRX;3Z zL}DHpuP_3l91o9?f-=qSR{aW>9(de0e%ppvLR#?@!CjjIE=igFqIF-Pca~jpi?ipJ#?dM-wLU+hYz6 z4xV0}%oC84hb%Aa`)p|~E+AbvY-w+7U|sN7Xit|iY)+MX&u)oWxV!6Yte}ym=`N0sw=lN^bqxf7FOiWDc%?Wl20f!&o3k-U` zkg#bZq2p2nBBP?GzAw@&uBzglym*-jo3mRC7F;+5pW=c2+5U>;mXwa3?UQ9Ou>}%B zl+Fe1XAbKq0Snp_TxJi?*>;%5p6TQ~$;wzNIANsE?BoAcq%OMdhs7x5NtLy?$0+28 zYp}PM^Q(W{&dwrNOC`i=i6}GIdu0m9D;faU3x;mTFt~NF~4|bbBj<~TgQXcxrTO}o5_lskl`9^mnBx-7E z-+%x*T3Qr3sc_%Ax;g-jPat4%B<$~hq~N!^EpoYwdb~ZWW>y*N�lX$f4aVvN+z< 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 RNC6cwc)I$ztaD0e0s!Fh_E!J^ literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/5.png b/waveorder/visuals/assets/gellman/5.png new file mode 100644 index 0000000000000000000000000000000000000000..77481bbb7f165ce2591ece0b8cc9df2ad9359da3 GIT binary patch literal 25169 zcmdSBbx@t#w=D=E!2$#e?gY2s?gR<$PH=Y%F2Nl_65QP#g1b8e_W;3NgZA3zckX@n zRdsdsAFsNqlS-Xau)npJ%r)njV~+WSDac76BjO=KK|vu)Ns1{!K|yzee~Iw0;3op( zVf)}eBu0`-vQSVSlu%H9flyF);75MDP*5)Kp`iBlp`dsYp`ftsQ=1g|z%O78r6t6m zo*{oTTMOdBPY~=SHJzZKQiUM@pa&^X(4e3UGNr_XRooVjRzAC_m@EpOp45JmI{8i# z$ObR_ahFgyA+uZ~uH(4G4^c`XmW;9Ud;Sv%w6HM!ONHc8bWF0Wj|g9!W%3F>PsI<8 zJUoR4rj8h!ejOQM-G0(Zn7dEkAsAaW!#W?exap1`-gZP4NsRrNmni5fFk7bG--IbL zf|ua}9hg|dpj}VG!GR_Cc!_bK1kY^6$HV$AFzWo=@g*!QnV=xCZnJCeXbPu+iAir0 zy~qf98ZLU*fCRC3;KPF#RL9X$0~$X5aML@H5iH1WgiK8-JuY|s@9ysMnYFdGbMx{# zmzGjo+yfH{Ap`sWz1VU3f(k`TM_1)`W;mONX82=wBo>vQ*XMz&FA_J$nJ}imyU}R} z6$`7cNk(J@F^H7Z{oV0^s4ShYkWK_+w|6Y}Ib7-x62m3M!CpY4ei3k&O~!zS#D7cVpt=)XT{A8&I0cTgiLxzvu`$(*Ifoj1e+kq8)+l>$g2GI(GqJn!NM z-`lSXJ^bCtI9ipEl!PzWYekHc?76dtXV!F1`i4q)cXv?1ix~(N`}OM=ruK)gS6Qfr zQ;rs}*tmc|QNjHQ)TUA?@&ke}f514gUojj6X=U?KrH7%@03dX3B(l9zb*$zv&; zF;`h&Oll;A$!b=!G5zPKN1lb7v(5gq%^1OI0WlF76kSRRu?S-pT*zsh{r-^V9h{Lt ztfZtwf)$}j%t%TZFa{pZwrhV>D*)$mbF$XISZAe4OiM}`1o_R^p`nk!z$}jtlvH~R znZbkWJ3IY%l?coT&Zd%~KT5UgJ~Dv^XJ%$Js*Mp2L@9fWCz41>wNBSNVP3+)U!O-@w5C>sR!ahx1{?15Ufu z$a3B0;*}U762I0~ekiubPa=}{ll-D0IKXZ~3JMCkZnuXMCJW?=XSdM|T_HA!1PnB9 z^Rk(S)LH=t-B1W@ra?egEGn!PFSrMl!UmG?Fp%@^7J>X9W^f=579tog^ZmP?O7K7z z6tI@yAjkvYNoIQRf%ifNlA#7!@81PNgHIy81|L9z46Okf+7Ep4e;*179((}u-Ca;u zlobCC3Xbjn(D45~)BmxwLPGri%o#?+EcicbBp(_>q4{^|;*gaPKvn|Q9J&FP8i)p2 z2^^ScBxEIAkd*{NRsz``a>%BjKsE)exyK%|63CK*A-nkBL!pX{5J4V?7{~wqoBzLk z$^V)!s^RrN8v!oL|LN`e|Mp9I{2+%03ppR^|F(Maf2{uh^#T8{8UH`p$}j(5)NWE^ zT`1BmW+N`vZ^PTvbaXIKDyphkZEZwQB+4XSo}RN5=u@T;8#F9Tkg~G0j7?9E zuB~MQzLcDwpKxJefts3HNKp|D@%3xc?;o`vFT4bZqobqkU0uZs%P@kjwXpZ!AMFW@KapWrj3mQBsmIGo$T{ zrHRVQM(M>g7Z$?9p%8TV!@xnY1#dcy^M_#Txj`HCMfwK?1@+y3l$7uI6y$5LF%jg! z4{PWFF(R=~1{&(>A)+BD8{j!f85uD-Ik}Jv@JtavuCZM`-Ar_ou^l3k?P2GJKA9({= z`YsC+1v(HmrPzBer1K! z+2z&vRP~G?lbyL=Z7CA+hYyAA-uN!KxKYn}1vz(JuOy#+ZpD+Tt68iUYY{$qoxSRd zCX4{Mz;G&8YW)JA*lWCOj0N_pY@2|BSFS2R1tM`cwJqCw*(`)H1S#273~Txe~^mD zQicC!iJDW>D&6tFqo5$Ap+NwL-7i5WD3WhQsg|ncAAX0(;B_BpuL#ULw7#pdsR_^X z`mpl{gEj=3)Gb}u*ckA7upLs#1+D}lR9@c9tqEWJtwcl)QZ=X6F&+J)L;GPn48!fG zuOQLWiEr%IAKHhwc4J?Ip(+}`nm=pgq&6QQ9*_{Z`~K#HoRJY_&FeyT+S9iT&E2Ex ztQULZ^x>ismj|ZLn7s5sw@&x{e z#5>t+PS_pF&)x%_Pb)Hp50FjHEiofp;@i{iH)l(B?R8C26Dx#ctfn2? zqnri{W%vhQmrBpan2iae#cevTIGz_kz&0#@*10@hNx3pWB^QS*V}A_EZnY&01YM)G zHt(`@1g*RKZkJ=7pChRVZFTGiqLwvHUuCw4biY)$U)OsQZ*Onou$iN_J)BF+WC)~P zk)o0Rv%zIS8v$@{D$OU!4j1cQvn0vvWz>OfJ${YVLCQz4j=#3W$|Cg^am*|HVY-Ag z!S$4-fS|CftTz&uHTK7k;J0*Y6BzlqqyJdm!kfdbE#rT|$zDbkAwQlTvG>aqgKmw4 zH33KNkL5NJwo@XKv=_B)_o^joS+%v8_?))Z9*P;LvJgiZTK&Goe&frOjSVdFm;SF5 zm2cjd>AE(nnmzj;b}JSReVpC*aVJCDa(hrYO8Ddx20*&?Qa!Q=>0>B)a#lM{;QQ>s zJidcA?~SXo{z~6JxU<{!DY<0$jZzVcjy7-;=j`y@Y(AE??9ohZp+nLIB!feic=u2D zC!}$b;4}m$B5}mXoeBL>VTnd@=kc;i=)4S#(A+k@Ou}ZBX2HHPn(F3!F|eSMHrRGD z&>`VhoL=e{6_Xgk|R(zfb)v~fu&&sqL41sx-wxb!o zgbWxzHKinz!hxJ45u-*Fm?PeKcI$B4)5|UwXB=qh$V9r?9QX5Pvn2MLph9caEYYp? za@@c)#!57isyRxV^PZCRT3gzcx+oF>@K+wOiHV`<{GNvW(S+f2{Cs|OR{kyrB)iK= zE=5O_R)rrr8|w<2e3LbiMLvIa=68lClONA#Zq$k9Ty@Kt35kb?hRmoaIT?>$s}_Eo z?*gg1rlyhZuh)7IwjZ(YOYs;}UCWTOAhq3xaYFdpIT{VmYZJAQUJUt;=Y};BdwX-d z5|A@|qfrK7q3h0y3^p7<8bWT*YkH5X{UCO$S?U%(yOjo}L!T#HjV8>>Dg)hOwS`+` z(TS0WQ%_s`K#t?4XCVstZLO=rura}h4hu>9=O^!xRPOkzIZ-4`utqJ)_`yoUUc^WO zZuF$2q*NEaEYx&jpU{qr9a8hN$`GlIvyElm7o<*8>jKWCZZC8>kkoxHCZF~SHX5C2 z%ntjBeS3B?e8=vVNMS?4>=3am9&fd`LHen(oBd5Ax={9FCoo${z?2?~~k4Gkk}z9**-;916|)Jj%l(43!tnd>KZ z>f^9o4uyL|RO=Me^9^{ADOi>yB#_zk4KuYz78lcw@hqW{qdK3FSqB48lHWRCc(ou1=%g8W*w;P+%HtD%#o+Mal(+SNk)F^4?9r zuRfDYkX)YbPM9U`Pv?$G4b>>v|5466Xz42r91nDXPj*3Y)$Oly7~ekwt1i>%#EpoF z=?Bz-rY}PvGi1ovgoF?@a&i5+a@|yi^gIe4o0V|66wUy%tYPe7S&6Z@8`5|cnn-QW z>ma8!UudY(WseTuMKou3cR_lB=E7A2{F7E5zGpt`U?dBzA0P8H>MY+LR0O>T5g`#5opDhm;{3*_rNK^*-P3!Uf)idOI35DW2z;^9DP^BQJtxPl+0|jxw+p?z1mmuaTcd z8Hajz@#w@Xd7e1@Xs}-R67q()`YHLJyokEWc7+Gx57EN{S&y*@qZr}d;2mu>mk#XH z#d==CVfqaYujeWUW?5QPCfy=RdC0KS1)U zTwLJaD5QU^uY6wH=35xVfM0KRAorRro8G$*$iHK=SS@-RtMZ37GUCt+cnsmC&+}|_ z;u&B(}kcKcFo6dg0lVs__}4MRD@Y$Hm>GY9cyM0HrDLg`8}DpLE3xI29vU% zR(FU03FGv`(a8dBrQ;Xbf~=#b3lRSO?5vh*y^a>yb=tg=7CwKN2a|dk#hTTAIVC+` zZSvYo2jF*bvAN`ux|+mtzNg(XZcXt7IpD8#`U;|UqpZzhp^WqQ$re zdZLJWyw;w<%6NGy1h(9O*D+U4V2(hqtgyui>CLo{n&YY0txTlN&7**gW4b_EO^efx z+^@O08peoS+#phBD|UAFL_T*GpQogIn5Aqh^_QvKFd)8aCYaZ5Inq?4NYLr-EPL|w z2*7A2z(ZByv%+He_{a|T_V#i)ZowZ^ho%FIs!`R|jRL0+;JjmBna8D2$KG@*74lW@ zH=<0Mwp$9z*X4AP7Yv8%?5-bpuM5e&1XA7=6nDI4{15wn_6+HUh z;kOt8uWKZ$?c-Mbsh|g@2q!0axV2?-ctSxp_r2}LueY`V*_sIjLcq>hI#e5m5t#hfqut4MO#uO`R@WoEp`js-gb$a%A1Cqw%hs$hH2^>{)!|^Z zb@D3VQ%6IM)lRyLa!J)!>4P{YF?TEWgr^#V?bUdfFxs=SGVYpK@V%+rPQfmkV)Eb! z22)tgP}-jEti}>mZ;np|9RnGoEEPBYa{YQ+Ko|MCGSeIZ;*Ub9oxy&l66Uj_|$ZrP9AbkKm6}!$F2= zsEa-c;(%dwJ-G3Si8y14K^epkF+}X4jqtL27E{YP(j;Zdsjnx7*k6`En$zi zS20YMn?ws6OYJ|Ch!NIUP?LMtAp6~j;fPG5^Xcn^bN=3C6kxW=Q!{dV8Ebn7o z+$pKo+;Jn^j=tMk|*Y?GJE6p0!&gyNp+U{+vC&VENQWdk0LcLAY z<(e3A(<_U;rLW9XH+{CYMaJX)+6I_3A)rykcP3i`H8nM3;N1C~SF2!t`vdQ1R&9Ls zc7VTLWsm13>!iCYi}e7#E`K{|z- z&iq`?8%fHfyzA${D)zA`cCArNu=MVBFaLE5-+z6f31DjHTijXAMhwo4-F$9I52j?a z{u53>Q2!^KaDY2Rn^nW=hJ__N?C$O!>?@ey4Cq2;Jix{zvRh%9jQ|qD=j<#sAvY>1 z>#1W@&I{YoBCYFZ)QLXVm0OUD`(pp5@b2+yErc%tq1IJDq-P?jPaf6q9PsI9Lv$7P z#EDfkqQI6(?cc9rw?qkVry02=Uf29aBq9dz5}Leuxr8RJfXXb1@q04F`JMs}TF>{< zWx08-^a?v8W{lUhquBHF>~<1cqfU5f_vFVp?wfR!s+c{2%6FHi>-5-|n3arSyQpB^ zZW&(Z2tJQj>SKvJaKs&8;j?)zq0D?j%3d3IN?esTP2F%UBJ(|xsL*?Rj__4=Q zWC4x`mVuiam$=@ItIu^y(v4obuZb<5s*K^o>frXrIrr#WC^GE|j^oLn@LFuPg{7qs zVta*wR?Osnb=z84u8*RK#gGVu3{w9j=;*or?&ufKzawQ#45J$LRQMNPlpC32e+jRaKSuMl2H0aLm-e0lFR4&2gPz zAg(M=xbMg{$rr*oTJSqYQ`?pOneyc*UG}VIRJ3ywAvM#a{a*d=c2mE`X6ylAmD?z- z-d5EOdJz-ncgfqJ!Ky+!IdvRt4em}S6(sO=CB4rA!Hx(iyMWbX#sSI0?Zd@5w^6xW ztI6dF23|yP@r$kJHl;nD@yTIQ%x3US6>gZprhv%D3GC_;8A)UgxPa*gp}@`e9N}Q< zGJN~Z@_$^qSJm>rF8x7k-hA>zjWWKfJoF!1fVXSC!Ya5S2cC2%jW^1*&%;PL{xfc` zdYfv~nAvZh(_6AxihbfVT*$F>Edyfb6clP4`&tB^5-Im*asX)c(>WXICpBj|e4wgi zj6p^etpOZ-;t;5MEjK!G8`YDGcX&T}&E_RVP2o2yxSxFM;*!^gJ~_*%W{kmgw_z|_ zn3}r8M^0n~U+|0fq$MCbyPu)`){vRU`>}3tM7}Vbluwq{s!-T=iVT3yu5A51^noTZ zQ{$PLRFf(VNHiDCf5r&Gq0dW@P_GC!X|OGoX2pM1FqiqX!5`$16~)UZ?$y!6mv79@ zo@xSWF_6m-(4-9J+qa~&wBf9#tj^TStL`B#7BYJmZQpYFUcWLERBjrxI{XMvKk7MU z>>lyHw!YrL}mMlkI*OC!I}3$q5e&jpOpd$EjW)4eyQc0QF38_CW7 zPA3KX#&I_&dU*^;mCn^xPi`YUI`yupwG0;v(;V6Zf{%8GN?@N2={s_Wk*l#9!don9bWGtbsG%VlgHTf05}XHA{2(cHWh!^E!eVb?Phn zST$f&559(?09@6o1(GeZXp!`wS3KT-DRhZua=y~7W%2BM>Eilij$GkT0(|OwyzmI_ z;qvl7Gtpzq6Y?b4oRAxFcXhyRL==J532`NPNywVsuN-3-fF*eE-dEc1jx>n{OrDQS zEG#6sNQs3660W@>jUjCJBq;h&REf6GG24Z8{o~rc($TN31MfNf3HzN0!UU8MiU#b< z4sTGBrNGD36_ic>yl*5)>-4to!*!cIaZ@ItCz~$AIFhc8jt21NfDthN9#QxD&jSw} z3q@J$`}31$D4`c@p|9hb^59;oMXyI#lT=o|TI<4kVO3x{%z>)G?f-o9Jl;?VVF_Nm zZtfp8eM0oh-o3#M&brZ7H~C_#d<`ory>Kd=kMqA zX7IzIigJ;rrdZ0n7}vUv9o)2dI=pWUxq@3d?hf8lFNB?lFqSFfG@KD46%bWaGLB~s zP@qfF(I)>OBp@KcoQjcF7QaCXn`OYSL-OJmOlB&n@XKYvcA-d$l`7sq?fOuZ23j;-mjpdJP zU!vCD&xY73gfn^hFMc@yLH9MUTh!k~7kAjAr%sqLM}P?b>p`(tX;3{pxQP@s-a)RY zqSGMu2r<96Q2HkaU{r4!bLvqFwcb0=VEI}~KUQ5)!31nA8rWJIH4~G>pSdKz(#`v+ zZzqCnnN-r+1N=|KY%16w-R5!$TFNYRJ)GoIz)2=X6wSsMPGFKS&?2>070lSdyn`fp zH3HTFO4{GP{EkTZUVf2OIZJ(9QY~_WGz$3t$)=GJ8Ly|mJ5jHxpe0@I-9@Yt`l!gF zBFl+02L<@vI3|f1EY}a?aJ}RozB@(MS}D^`H4zjP{5QI5H2@(1NV+%~zd?U#j^=sf zI|(}=sdX^(o`>BwSR60>`MvP{*e1fdRbV^UuC}hJ>GbpX=GnSDYjGkUn5`-Z@Q5{Y z_k8!y&aP5pkT1INO1mBS)&d*7#uDe=eV&2HE z8H4g_nA&KDV8#W^ifa!%SNo~mPrkptTF>-tl1S~R6}v%aScvgPit4H5?-@bDx-(z` zP?FmfE^g|!pC1%h1V4a}S~YfS0w7)Za|RUycQP&j1LYQ}f!H>oRg$s8?)sKKwW%8H z;FN-@-@}?ZNP)RMxC0H7gC)~=fF}kNL9GZ`oc`cfid5zT^-n==9Y9?&F#DSEarM{Y z&e;b;;6#S*Dv{;JCVOLN`BiWl9FhI4o)^GG5E;5FL9OOr;9UC~Fh64AHN<3m($a3; zBS*Hs1+1BG$&QviM4<9;a{oc8>3b{jV;xnw!PNK{l5fWJ#noOYfEoJwp)##$}Cy=$-pUHr8N zk~UIXP@0-xd;L1>R69{59GsajWs%{d%X>wSO*oZR-n{o?7#ba(4+UVIc-7&Ezt&op zYORfo0?Kvw+G@GMwi7Dp=|#&6Za-DJWC#+!prXoKdfi)YPk35?{W@+~;b8|W*Jtwe z+t1O}Pg;w z``^M@Fq1_;(Wv@pomJ|#UaJShbsz+hS~P&7R!?uQLm%G{dgwJdmkES2T|Ch;WO{;! z^^0Exr%5;=ez;Tyd+51_R{zut_VX3^WFCnRDx?bDZEI3<-6u`CfNVr*^PHo&bu4Ya z*i7D1L7$NtNvgR_vj*10#6)%&S~GXLynp0H+crpYcsc;v%j z;M7t1-a1i!q;+yIyAgEwd?IaL$Y}iNLNn1foy#qOMcpcId9;Ad=>61thZ=7@z0re^}WD>t1l=9z`0HVw-M!T%~ zxu~`t^9{;a*U<@;u+kG$@W;)6W3kd*9(PdNi={Dg4dU?0qE;Wm>izxv?aWLPE*_q0 zkiS9Br3O?Oyg+F!l{qF)y0gYwo7L7o4`vyz{oYMza3KpMVRZ~WZeD21A&g=ZrGli_ z2+EB%?tM&>E5B;a=oN=+h7Od^zS}9LISCu2g7we+oB?+GaR&S6+Kyj)GZh--PAyt@ z2O|S_Y^R0gvj1`?G}QHI*hYaZ(2z6l;INq=xgcGceKp~`_M4YaYnslq0%~H3 zaN}Lsy7!yf^IhHpo*(;;)_M1a>rZ0)L!{f2;C&A84x1uXz3Fd!B^DFB73@usw;Tz_ zg-SoT7YGFBvZx#A7@xep$$G7kU{+iFyza`3$7N3f;tG)>&R34VAca&=QTbI%e|hTX zgn#n3ev8LGx@)Tim}f^<)?-bCHkYgab1gZfXz_A(tjv}QAVj$-GGiZBq`DyAy*+K~ zU+D!>2OsUS{s#(}JYY^l)HI>kS5iv;!N9<#OJq>;)fXdX^iZAM5&$+xe=RgOc~v<7-jv2QD`ht_pI$n?K^m zFWsU9E61q+v!1bx5S_c*eW45iZ~cWn&-I?4AX-Lw-yby$jCy;y%dKy{OlVl08fw#p z8}lC+!v}oDL;9oE_I-d2MfH_PLc+~v81_xAhcCMMK60^N1sLz zV043x#=?mVd`M_Py&RVzSU+-AUm+HopKrTHWfOQl^1Xo>kY$5-B!#I zhd3Bq`PKBXj-EpTE_1c5ZFviFMrL~cn+ZuP>A261jZ-e^9T@_p$9hFgEL_JE< zz(W9r;=xp&G}ppII${i*Iy|;o6>atN#;k}kjF0poKx8E9w16vPKOVt?~8F3F^$5VFFawWQGY8h>E5(w8+6xu!L z#s%;;r{6c9eOAO5$a=JV{Va0UJNi#LTT=asa1tqq6x98~@VL3T=UY>jkQN_TJ})(L zc3=HGyLkmFe5#E$3!gmWnu_2)miJk;M*nCyt-)9e0{QLyKcM6#f61P$6rJ(+?}a>` z^%hC}n+CV18-T2XOzS(Ol5=<0f3l)^bM#sp!)I!2v4-iz8sGEh&6C-aPaG<5*PrC4 z9qvQyC5LL9m4*RARs63+SpCC=V|L>Z1{I0j^-Z&UhJd}3(?JJOSQSX0%8Egy5lB9{ zjq=^p=9QE;Pm+8^4s)6))ZOodb8Qi4JT~nbYl*leRsn!C>CmopERZs*%i@c>AMWc3 z!vN76QXME;Y_KCnL_|#f@gY6k-fNFy)?_d0yC?OFCvRB5e5lrQd;YrRby}kS_1)OK z#aC569t$x2He~+=nA-8;6(`A%GCcNa0AK>*EvXU?o5dbYSNDriSI#XCHNG}fNMX(A zAbIgKGTlkq)W^+}|4yYVCW(vErozqhpT-D(neaVr_l}K4^Nc3~4lMf@$k`qKj3nio zqzg@K1TXiEbQRGx$x4e9NTrZgTDKlV%P2j9J=`c{_-N5h#rMO>nK{?x&D*{*9ntkE zU#x?QF5_>2ZPp}WHASKyIZWU`2l!sGcv~~;F;}W`*ZR-J)|p$>wCJc*5MZcjIm?E4 zLTZ^j8XC_UTIgQ^vJ9l4|EAz;Uj_@!(Sk83W%PB?4MG>I&|bZL`}XwgO!&ix57UXy z8{L&T#8?C-zDFNsne0lucP18duz6&k@C;9$QqMp&f4R*&rTJ{{877>G=CGS|SJT;j zn`$=1ngwLhKR5OfHDzU|=RswBqRvVkNN2@?-jNzoyrhC>cziVA^x-?GMv8{`j)!Ym zuAAh1$L8b*tASgOQ*c>IwAbF>S5A>^A<(Y_#%wmkN{JR7gIjpM#!MbjgcXQGFug8D zIa5u5$9r*msr~^Mq~s6jF%olgC?duuOZ%>lnlOmt6%vcstqSfpTz*)9WZd^lIr#ph z*&sY=x!4<5?{PaVpSlgo4{hGOWRwAn@#xWj2_|D>tF=fnGw>(U)I^<%7_gK&AR^lt zxTk))po1!Y(mY$AI+2}!qZiEk=E=u};!2uCw(2N(qK}Z&X_~asS>M}@Qgn5~FC{(Q zoSM-Vui5R445YyQO>AALk4^GiZI9R5W7XiWFaz<>HFz>XZE}}m^Cf*6jEeQb{Ox^b zy|7RxcP1tAfXeFPsOKRqq6k9?pZiygKp!6;xAQ?N&4dp)fH#xH!^5it^`EiC;Yi%l zVHTeHGzs%xx6N6}BNsjxQ%kqnliguKAg@Gpe9MfqF_%EwX#Z6}oU*pmHiI}?UFW_glQ&Njr z%}544mNiy-dtZ@@Pu$2*BMq*Fhlihn1i-#8bq~ErjnDSM8_nxr_U!!a6kD&Vp1q0>|yoK~1-GY!h zXrO#Q>&ItUca%YIDz=!sCW)_NTrwWO$5W`cBQ`tyuC8aGi^YtJ8T=9oA|tW?k*4RH z<|0Lz*U~~zR8%xDI2zJ2T+)%q?{~0f7B+X=!3>oGMF3|H3v^GvJ4-bsiP_*m7FRmV ziSS(_COd0^o<2Q2ovno50!%PAB_(3IKwgk&{bGa51o5C+M#RR3!FHu7XsN-@X{?3x z`c<$Og7Hh}FyD;F8E7tq%!Quo8Ei%pqg@5QME2J}T4wXtd`e{RcqMfk5Indq{4>)$ z$@75@LI37>CH5n+Jhw5g;Y%?A0dVM+pmYfwJdmwy*4A&XpZRlg2HZSY74tp}7*nrS zzoPN?r;dyWXvy-w+Q21r*CiyXcMHD+mbKhyONhWz^n0JdBKuMg7^>Sj0z%#>La+d1d+XQ#0&+7>4SxvYKvG%l2>_ z7G8LDtEZ+f1#FS?z3Z6wEU9QP2{m=7i)K&9MCn^odMYZowzf7|vtYhB_^iKw0Uch& zIJ&N?qC&#Ui(iMA8W+}UczbszLXXvOP&0&q9{UzI?5_FqjI9-?ErHkX-}*DHyayEn z`nj$r(Tm@{+AYlyu4Q17k_7G zPb0&GCB?1m5(1xwgsMx)t_%VY zKm($>y1JWCF(E`Z$HKpgFb<}WYipl&fRG6OW<*U zd}z`{i50Y9tZ#1zeKD1d2q@R_cR5)tl{PBR&3y?jzU8N6I4B7T2{%vAZjsS@zdI%G zGqUQPv6;fE2A8$AQ{8qVj_%+pMr|sHN>seYsHV17Oic|-XOB>83h|D85J|VmISeC^ zjEPC(W*>CS76mJo`-Ypx<&I0q!wGTBhA)rX zF46Z-*35L9X5V_By)yfom!OwT@{U#!Cpilul=^X$?t}M!dR7HoPtZ^y0lA(fC91af zg`P4%h-__bt&vbpkg+!3OUZz28yB~Z5L9MImylz9vYOAxac+7O8>K0#h(`2uOKx^p z*05>_T$VkkP3$&c$)+D=c!ax1dE9`pK7K?5wV+?N6&5!*qNMiDf$};8U&=p)q$NLl zo2aS|Xtxm*@q1=JBz)3HST&&dKOS7dM+&X!v}5^ee#vL*-ECiQCs0|;nlvQo zkad1~dWz+9n_#;J?$vO%RI^l?95#9kX;cPui-7dN53po42whE0JODN|%dQwTxg3~Y z3d7fyMacf`43I=XU>;)RF!8)`{4y~)iKbj2X95=d0xX!4h9<1W8d6XG>U^wtllbeG z$~R8W6PYB56(bQ5k=j}QK84d>L$|&jlHERJuD09kA)D>1t0-M-5s}E^2-%G$m(*>} zH7|_%`g(xh#r+uLlaq+}J=QDQpDeXdxV-T=L_?|>rOki{AvP^-lpEhR3bc)Qd3z)8 zBPi+?1E1ikf@zuZ=xrW#G!S>H*}j^Y!BfuXf|xX_SBxDd_S5A6i${?aAsZT0w#j3N zaNGl#u>R2?RXb zp4 zuz>;TFWtw_Kqn8v>c-xl1!l#^-QN5^*Z-NKFh&BZEi|;)t!?I#MpD5~2F3X|tDZ5M{5@QgFl?sT zKY!vJA0Nj)J}$AY4IyUbk`ENa&XuKevwG}$L`~l=h*#8&ib@`%8!760Ede|vX(=#i-Aw|iD4Hzyw!rnv72rLG0 z({03kjYfJvhaC4XkaScs7U_bE1c{WXsRVLA>5QzbB0vnjy}cD8L9Jw5G1t7=3-DX> zm6(Hp)8E)Ba9nD-bYAxPgS(9KM&dDzm#3o4z8hA~$Pb*>#*F$WlgIC90VhO1l#SL zB-0NpD3;$YT|BDwLtERaR@I9!G&HTUto54l*cyG|>1xoJcf8zagno1p`ZOtz#2TBF zG>9M)7ZaoS(m+ZI0o->51%*rw_@kpEh~Jl&2N?^0B@(>QTmT$CQ(CdbF2mqZ*W=!F z9JeWwrQPzx(9W=>C8l1rFOX%@z)Lkx$|JL8+PB+JkjzpPsQ@`vWF%4)QxcPhtEWGc@cxe%*Lf-IRHhut7&95m-wTJ8LLafTM zC;*$mU^D4$(c@db<_Ht}0`y7d|GACApwmJ5LNsv~YrN|PBsNV}0-pgQ_1TStfeyjW z2Y1;)S~|tkL8{a$V4rVLP*6(S5e<6)@!2s`qRu@$`*>f&7`6#6HbBp*!;NLk_27Vl z2KwQ^RoZoZ__-Loirm`^{83kz0ua5Y2o$_$L_rP;k_ENG^3d>bL+{(30f@CrL`#+-$wMFUR?!g(3|RVC_=KTuXb(1$8f}jc88;zoqw<9HXip1P(9oV zmsRdgNuTx?-%gpHB$NYM`K0|>0JPzWM|Vy10E*dx=O&`1WrCb4#6=U}AyHdfM&Rv! z+0p?xw7qTOc=0+J#zG3={0GxqVMJ8NBpRe(X|vj*);z!UxFa}lf8v|+gP)%t0;29- zg*oE@u;Ku!o1667%;_=5VC@zb7M200z&!&1geXEzVee{tXn4_2Y>Ce=IXnb0PbJSQ zkq?I1U3bC1fA`%-j?(M3Bz}oCe-)G!);#%J(e)smKWXM1L^M6U^u=RlS6A0x{C$BM zf`8w=kjn^OtG7XQ_b6VjXJkNbpp>&dG<3SLopyT~`VK)j44?OZg{6~FXaR@x8);(% z#49YIE1D|w9LWiDeSCZ@@{BCnI9n$`^N$@F;c%%Mj0h9f)xrzsu-z&JLM$5knZp27 zSD$(xZWQjKcH0 z=rtTDE=l%>XN_r-y69?4nGyeCwa0V_e z?4To-eV@q4#wH3>v-r+>Ffap6M+Eo?IyvniSp3w+X2uqj+w6g)?HgyaucV{|z$2C5 z{)|Fc(NR!1u5)nyxc?p-AVa*}FeIg7T9C&>G)I9X={)YPC`>vpv=J`UBgyAZm zM-R{*K`tPmcSPYP!X|8B-gSu<9MjZq&NEpa5%Jn3hO`X*?;VG06}Kyr8ox*3G6yFo zHpu?|QT9ITiY$hAO$6@oAkop%LSXXYp*6V0$VfPCyTP$f;BJ>Z zl87JAY%I02-eze%D?jT7xD95*9{9kJkWFwFrEkJ;pgqPX#H8|+G_U3rU?RMfD|v)a}->2jaCnK zz#bOVm&9q%_kDTwx)Ky?_ccTz06iPTWhgFRa08oIDWGGhPmLt20sy+%Lf4%7QAW}KJ)+oEMaX{ZE}ig?hqnfC z7Dy*mas=^-h#&-(;X<{EC4LJP1x429lU3kfe>Y5yZgPp|AB5N>Lk zzo&>1SitwSI?=;8H~j+F>b+ z3kySQ=$&0u=fx@GvobS;uZa+{-HFGMiP(su7&9!(FBw*^JlQ5~fqxKRF*48A&87=F zn9cfgzMXGE6cfS(kZrfTPc$JfRsz!}ack?+MXFT!G@hU~5ctT*L^iOc42dB7=itC> zrU|;l)*WteWGNSIR{4V|7b%()e4ho*1@1ywulw-r^e=ZOwHDeX--)QGV1nKSW~Vs#5h7vvlu<`RJ*UMM0Xxd~-jql!>nVBVi@@-Tx)a9OUFM&$s=8sKBTJb;eqhunP-@3k7LB87(a$XmVH48Qg^S zeOh3XD%PM62Ktd6OU5+xY#V?qB$1Inh}Ptzfj8Qs6TxkdB7ZbCzG=za$M$d|VPlGMS?GzJS!<7}U2P?0 zptHim=XQJtu8ApXf`n?E!MnNL`a{&D_*JUJ~$cD+z z&aS7we-qSec2}AkhHN4tBGAB{0dDd4;X+NRG!~&!oUt-k?{frpKe9B>p^S?jf(08(JehklWvUzm7%4)KS_Ia8t@q2%2O=EECaYEGmDN`@)YY-C`aXNpgItWh z4PR8;EEvhe*jOKQMdJwiv@DWbo}EEZf&Ig+L!B_Z-!#$3)d?s~S>%Im3R599<$z}8 zeETm7^jJoqQb-Nz3zn|pQoz|Ujv}g2Xq}Rkog0U0M1|a{^WMP%4m0~feBy? z%s=lanm!m5QYA3zcAai?vnR9jzJHI}{&YtN=)}cG!)g(nVhi#Qk`lk3uCR#!!)r#* z#)g^U`@{`M(JzoT`hOzxY{jP-kas}Z$XSviUH+Q-cPaD(Rt)v++aSob0zL6yC+g~Y zt4)UOLBBcQSUaK_P=;;xejwT0HR=$5nMODMNvZaW#FL^-}bTC(;zK|0Oio}bx76T(AVlpyF z^z`&R4}U>vB-I3geV?+4Bz|M{pdyEnJb<;uVww^_>zeX0{U|(kM9Aua6P@9>Yy;IY z$XffplZ~yCacxTh$5IiTC;$s=$#Wy16jfDK zC2KQR8wGTZi`^N%;!ojkQ>y^{)<0UT1A1TDn5Ov$(EJVFeGociGdwf|3GJX0_jdy* zg6K9$WxfCP1+>+Im;wvlI4}=*y&;H~M+mmUSA6EhG21BATQ{Jmw#18O8*YEn{YeviwJ z><f@894B5t-9Xn8&!LW;IX6p<$rf*q3%{KNfMNrlxiaUf8p7 z1V|3PoW9L7R5UbyNNj@uL*FVcw^Nd)&nMB4*grd4&A3*mh*LkE^YkU2O1>|R_p^%a zQjszhq*WUv0UylG3-$I7mK#~k8lSFhOxNBr6FP>y{0v&GfD1@H1GyRj3@nnqZu+44 zYOm?uNy#DB4YNfOjZ$@6VJ#IsxmHt3rGKudc2;9?Sl1 zYZw_>A!MeKBs)S`q3lPYp&~M(&~2oUSrSq_BocRaLMStth1{VK8us2Rqw+hh=Xu}v z_xZg4(&u(v*ZsZD^Ei*=IFGEv&2y>1B>6{2N1q#ys<&ndt<^uUG_NVVkokhfP}F+q z8*9IZ?Zhmma+p?)6dO*_kKJPV`F0n-&vYlcrUGfuPzdpd;Db>eFC?GjzkOw zHvY=WO7r|nVO@gpvi59OuzHW1EvFS~R`_umhmF7EK%}bc`kU|Lf~h;#`y)8~XEM^B zQ~~Fk$6T5tY9e^D=jX~#J8?Y*-bjW=0WezqdcJO_rX0Nn)7dglRsvc8;YH>QsYT({ z!?v6-F3eDQcJrqa%-f2M`wuv=CTG0#Ub|lk>$JFt$mUXy>EQb>_vU4%y^fKVZU9d?R~>rGns4xwy?XtPh!P*Z12 zGQA(3mvZWoeU_RE9M$mDM|UedeV=&8+0wca5i9Gl)>h{|%JDMPvC+{SKC7d9C^nwS zz7bChB8b{WS|!?-=gb8_oTs_D_uWQmLaK6uh-ZZD`8(m^rZ>vH{|CSJ_4oHqO;Jin z)ZVLDw(2-rT!lZNDWa%YB>--^Om8tHX3Uk_JkqNh zwIXL8eWsY4c5K}p?-IBYbF%is^78ULhc>aoZ^NguGBf=H0yI9I_dCBCg||7ao}a8Bi7swWbd+&XD0Cq6P}Wo5Clvy)qt z4q>iG-z%_BwlIY@!NS7QTp*s6L69Vbs`jv>qkk%DFTEO|xg)*$rj80<-*}Z9oLKcS zxluTV33NL&5#s11K9M~*EvcuYqY7d?(}0XT@p(FN`CjYPb`jhQV3$zC=0MPus$=wY8Onn>%AkF?h(r@w2|dm4*+}j5+7Z z??&#_wwfELcUW6>8*gX&FKC3oafv9Dw#8!;r{G-C*cH=X`Qedh_v;85WrxqA6JPQ) zY`sY#SKuSWwHT21e8ppoLy>E>;$*vKY3f_5tEXjpx)rGdd#v=K?WX2HS)uK0um#L9(YCr9+G zsVp^+^nke86NO5gcfmQcY;{1OM(+8UnYS%u?H0leGswy}`}5~Jv=Sh$XRnex@Nopr z!0-Fxwj7+2ES7{;^1nE5rTdhxOeZdq#LYK`WN4Y*GBI%W8vk^TH3pU+WyZgdgqR+alRH~YSptT?%lg~ z$(7TlQ{G4JB)G1Hv9?gR&O>{mk3(1RLx)Mt>$%)dWndwk^Vx*nogd z#2AnBkK3R@x?7%euR3wECU+TQwu4z?y%N@Py&;h*5elP>v~=8)TYqr{LPY!W%855z z;K&tD2&^0Zs0jRNJJIX63D5oBfX|GdI!jk)XK!RfEu{Io8Qi@+OrT|yv_kw^H+PPpSCajbr&eNEAg ztZ?0B`=PRO12&*OUyY)&n<&1CLL%DbdZ)ETtmMO#fuTMumc{Rd^{rYH1 z%x4-)$fLw6ors7?tQ(AlF_l1(^PZ{_+VxA5cvxP7F*?o%gw=%*24peHFJz2aV~dN6 zOOfYnGGH-43%*K*+gtfs&x8S(e@>SwK7>CMj~M_Ok28^f_gx1>Y$cks+$vKoB+lvb zpZtG|pW2AIIHAqjkq7|5xKwW6DJ@7PWN&}#k%hGUo|j_|U%h9KMy;>zv;7LWL_2(k z)Z?`wO(kGeNi=A1!G>|NTYv>SM$2MAB7(J0#i&SmqSBqzeIAL5%FaDhK5LT)&lbB< z1CX@USIW!EUTFM#Kgh}|jE^U2gpj9LVE+k{3emFZt2qB)P1SYxhU~`os?nT2WZ~dr zkmb(wzuUJt349>5H#Ie#F7bcyH4-5nb?GKS+t7^QS0Q3@Fhh+!djk2NU}Hei)L|Q4 zsUXKaksIMx3lp8}_P&N&24GuQSRkP&I!vk>#w++?1j1&q#pEks3mBkzM}(;4E>Yq` zV6(`y??i48;yNB-)4EZlvy*6M&~SS0(Na?jNl6(yMI2ZETn_|HA{9VE^7i)5L#_$> zrQPH_*2GvriGls1QbkM_v@-gA`?B08bH3RIKbCiLKsqCML}2H8rdy@=?PI{ivR%ci zpbHD^E^25>Cf#z$UAPg&#uO#@r%9b_w`-Q$&Q*lpLwbk*jB$V$y?Qbe!C2zB%Eh(p z!AF3Neitis(V1NPqjBkUp4CxE(Qkpuaxt#DPv={)7NJrrV0$m=9Bh|>u(%?QiUjKR;i= z|L4!2JE5Tgdb=;r4`Zx19M!A=62%cZRT`euWE%{+j2OK|*p6j&|H9TJ{ekPRcVleE z^(fq2&h2tiavKXI9#2+BLX)wD1>X~-hf#E2`uy;w9p;QN3URrwoAUm5&!%n9Pmyya zq@~%$+q20b*F(-FC|_s8OwJ>>bY*;=zvA~2?{2-=<}oh>U4k_!T-lH(zs_(SujC0D zp=K|KgJ!0t!ACV*1- z9R5Zi2R^0h<0i?1B3=hw2Xv5C!41F?0MOImwEk<}kdg1uHXJgfLYfsj;Y~%UQ;F`@ zNojSrNOJsC@c=!fw}Ugne&03hQ`gkhiCOH4X~eKi44cH%v*;l4Q6H~7*f^Y8z0yQB z;k(~`)||}GoH@9^BQ-U7pmZ9!23=2=hV-GMr_YQ9!dZZFL3HRmiO5)~!-)|ny2xD> zl9v8!@Wu$L20?8+X5f`~Y#!MV-~TS69|Vn4rdfaEGq~Nn*OuBwOaiT~Zyn&wmk3Lroq4zpg42t@?f(+uhdQF~P`D|0p<5|qryb>E?j;?eb=-MSwMd&n_`UMRa-21OgCfloM6 zhde2FSmxJQR*vjebzIKSgNotfjg5`fcrHkTO_irhh>u6}4MjOLVHx{U6~v%89dz8) z)o5X57)4y{eac@066fwIeN;(f3DgK?tOT?cr3+CT12y2Y{@Vjo7;VDm1x*88-D*%} ztl&X0Ky0K!nND_LIo$OQF&9a*OFRkOP|gG;C3BlB>IGDamlr$Elgnz4eEl2Fqf~>0 zyYSUBLfCft{p6TZ_UXdR(HKrD76k=C_`U=k+_elD14Irm{{%P2p&cRi{4sXV@!lI+ z#N07X&&;e!OZ2%5xhBi|O-)x956;Rm?zrpMK*qy|?9$TGJ$-%2?q|ZPoGbmQmnZT+ zl9p;2w@Xst%Tom%PB|PPYGz^*07}`~#zrno(cqE)o~0=p3m*H_xn-BHj}LLcdPgZr zN!bNdc?%vD5&TuCWp%LH zAisgdj(9foC@J= zkEs=I)tu$UiAra$Jt22 z_$VqS=D?E=o5b7Be3nf6EX}Sop(6sF^Fv7sw*|Jf!xKro%br+w8U21pikDC*5prDc z<~%bqGm$O){rfq~gjShqi~%E16o_b`lil4>x&7ngp&+p+%->7yF%pK8_i^70MNSLG zkRknLBqbdWkQ5hZBseJ0M;dB6AGA;jiSzf+dw6)b|C8a7lO+a1jQKBL=3>VyfzJ+( zGpqwV%m4i_{Ko2NTHb}qjTH06Acmr!tK+EoSfknkQuqB1IeG|-+8X)!&Q!U%xo?rz zOz>7vLh>Y{S<1VLTrNH?NhoYvU-MmO#;jaO_Gkh8U5f4|AiL= zYOhM&$cSC`;X}FwAHnHCVb#KysnE~Nyu&VDF^S1yph>@}EvsmLA|KXm9ebA5<>d=F z-UJT@$VLDRd<+$V4l$6yx$Ihe_8h7fe7-3MIozD^)p!DzV*~n`6AZ7CP-}xiQ_ zlsG>>zXEdXw8D65xnoYA=%Fwi=&5{sRUO8eap}buV;{J`UY}BYMw$+PFxegb#z&eo zVni?;N*+@ul=Aip4OzEkp8Q}?$L|fx_hC<*!-_$~d@_AHpdrq=6W($BSZ`b~&fd-rkQ#$Qa&@+?E`(9)sCpCNSlb>qvg&V7GFf!leVA zlT%ZJ?fH=7y?GjC&-IZW4Sxi!w)Q*s=@0ew8Q*-{TUsIyIc`rvmitB6ERU}XE0wQ` zl|jin8cKWhUV_LT+Fd_x2J~g0G0y3G-Pzo1baSCa+jFXTcw|~$$jJzCDKMmmy^5~4 z_WTl@WsI!N;{vU$berRvNQOF>VFC{3mbujP^1-F=$_xxelqXDBuoRN*qhWB(*v3)u zJGgKE86%&WrwtzB$Xtt{X5hTg5Vg0*uT5934Od;6Fb}vqx*PmdTZW0~`l1;=i8dte(js2ZoU%E3 zl&{-HUWI`yWbE%19G8gcClV5A1#aG2SXeN9&5l^s-j(6rnJR`{K1DX=B%}4(2J3Wv z{iC&!JJS_zZjat$sGeFG_6tw%L!2A~oT6U0r}EkV3BPXHf;+@dc!)6iHUrw-Bp1J? znciG(c}DmERlnv4Al0TTXYU?bxtIgw)nC_7Bpittn;(|*;i!S-hj90N_v~k4DGtzkBk1Djczkl*XL54e2E4?Sm+y}}R*Z{Gq zbjPZXBVt}s#Oqpb6*aLvyF8kfXmt8?L&=U5I5mmnF+v(4)UxN<*`Vj>S~lV8JYc#+ zXRL-wS7FL%!7YKb?m5t<_q-;{{I2dJzQ1* literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/6.png b/waveorder/visuals/assets/gellman/6.png new file mode 100644 index 0000000000000000000000000000000000000000..2bdfee3079e4f9a19dbf5d0439ee13da694f4c0a GIT binary patch literal 28108 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 Ok*S3;aK&{FEF3 literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/gellman/8.png b/waveorder/visuals/assets/gellman/8.png new file mode 100644 index 0000000000000000000000000000000000000000..53089ed49b5e87696d839719fe0387754f6c0e54 GIT binary patch literal 20552 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 QEiR(4L!*f1kks@40ckrt%m4rY literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/stokes/0.png b/waveorder/visuals/assets/stokes/0.png new file mode 100644 index 0000000000000000000000000000000000000000..335b0764450a659b844c7a24ba63414dc12e8c40 GIT binary patch literal 20999 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%0DXC^Iou0rQnu zcx*XOLS$FPb~7}j`lyi}3S3-vWo44fbY~|P=zo9vq2csUk+Ey;Sq)OyR|IXe z$TC#fv@kx>1|3Wr6Kn@9V?)E8O9EmLkl_?$pC^!<_qaIJ`2V{sR9iS${7wq7c;R?w zA;SVxtH&U{CkFZf7=D>sNVKG)7V3;Gf#VM!Zo96H5rFWjSer{{YXuJ_oANlHpm-FWuw*+i3{_~qr9gMiJ5%Icb%t1GX=j0DgvH#RpnlVg%oQY

_U30jaKA^|C8p>x|VbX`IcJjiqQ_V(J| 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 B^929^ literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/stokes/1.png b/waveorder/visuals/assets/stokes/1.png new file mode 100644 index 0000000000000000000000000000000000000000..2d85c5935f11bfc7aef99111bbbf14175917c689 GIT binary patch literal 16008 zcmaJ|Wmr^e)SaQbJEakjM!E-3Qjn4yKxrhUV?;UxqfgV0sV;)EN9R#K8ie@Y#k7 zg1_$BJkryGK>Rr&5JVURas@s@>_8x|z<0ml5QtO;1VZhW^I2C0e1T=Hp$3E8q5eg- zmnMTxaNQm~^MpWh6i|Pl0~|!85Qyx|W0<18@7(^PkFmbmJl0Ww¨in=wg5v-&t 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 literal 0 HcmV?d00001 diff --git a/waveorder/visuals/assets/stokes/2.png b/waveorder/visuals/assets/stokes/2.png new file mode 100644 index 0000000000000000000000000000000000000000..b4deae09078e8e2497f3439f5a5a980b22ad85bb GIT binary patch literal 17892 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 Date: Mon, 7 Oct 2024 17:59:01 -0700 Subject: [PATCH 49/72] plot all stokes --- examples/models/plot-vector-tf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 61f8b06..57fb98f 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -142,7 +142,7 @@ H_re /= torch.amax(torch.abs(H_re)) - s_labels = [0, 1, 2] + s_labels = [0, 1, 2, 3] s = util.pauli()[s_labels] # select s0, s1, and s2 (drop s3) Y = util.gellmann()[[0, 4, 8]] # select phase f00 and transverse linear isotropic terms 2-2, and f22 From 90aced944fe18345b45b7385696e1f3a2eec37b7 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 17:59:16 -0700 Subject: [PATCH 50/72] brighter plot for greens --- examples/models/plot-vector-tf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 57fb98f..7a166ca 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -147,7 +147,9 @@ Y = util.gellmann()[[0, 4, 8]] # select phase f00 and transverse linear isotropic terms 2-2, and f22 - sfZYX_transfer_function = torch.einsum("sik,ikpjzyx,lpj->slzyx", s, H_re, Y) + sfZYX_transfer_function = torch.einsum( + "sik,ikpjzyx,lpj->slzyx", s, H_re, Y + ) # Make plots plot_transfer_function( @@ -159,7 +161,7 @@ f_labels=["Z", "Y", "X"], rose_path=None, inches_per_column=1, - saturate_clim_fraction=0.1, + saturate_clim_fraction=0.05, trim_edges=0, ) From 86e9fa4fd2ce369394eb068e9db304ddca4d0042 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 22:16:06 -0700 Subject: [PATCH 51/72] visual improvements --- examples/models/plot-vector-tf.py | 4 ++-- waveorder/visuals/matplotlib_visuals.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 7a166ca..53c9b1d 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -156,12 +156,12 @@ G_3D, filename=os.path.join(output_folder, f"G_{file_suffix}.pdf"), zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), - z_slice=-20, + z_slice=0, s_labels=["Z", "Y", "X"], f_labels=["Z", "Y", "X"], rose_path=None, inches_per_column=1, - saturate_clim_fraction=0.05, + saturate_clim_fraction=1.0, trim_edges=0, ) diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py index 391a3c9..122db79 100644 --- a/waveorder/visuals/matplotlib_visuals.py +++ b/waveorder/visuals/matplotlib_visuals.py @@ -292,5 +292,5 @@ def plot_transfer_function( color='black' ) - + print(f"Saving {filename}") fig.savefig(filename, dpi=300, format="pdf", bbox_inches="tight") From 3c5db575112cb4ce3ace220098b136c773a187d3 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 7 Oct 2024 22:17:41 -0700 Subject: [PATCH 52/72] revised rotation-symmetric Green's tensor --- examples/models/plot-vector-tf.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 53c9b1d..0e8dc14 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -120,6 +120,19 @@ ## CANDIDATE FOR REMOVAL + # Quick Green's tensor, directly in Fourier space + G_3D[0, 0] = (1/wavelength) - z_broadcast**2 + G_3D[0, 1] = -z_broadcast * y_broadcast + G_3D[0, 2] = -z_broadcast * x_broadcast + G_3D[1, 0] = -y_broadcast * z_broadcast + G_3D[1, 1] = (1/wavelength) - y_broadcast**2 + G_3D[1, 2] = -y_broadcast * x_broadcast + G_3D[2, 0] = -x_broadcast * z_broadcast + G_3D[2, 1] = -x_broadcast * y_broadcast + G_3D[2, 2] = (1/wavelength) - x_broadcast**2 + G_3D *= 1j * mask + G_3D /= torch.amax(torch.abs(G_3D)) + # Main transfer function calculation PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) From 35c326105f606ee4d432822aa130a3d0c315699a Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 8 Oct 2024 10:35:45 -0700 Subject: [PATCH 53/72] ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d410e3d..29fece4 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,4 @@ recOrder/_version.py *.npz *.png *.tif[f] +*.pdf From 75e23b04512e7e04260181865f65eae0b7d581af Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 8 Oct 2024 10:56:30 -0700 Subject: [PATCH 54/72] fix green's tensor units --- examples/models/plot-vector-tf.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 0e8dc14..e0290d0 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -109,8 +109,8 @@ nu_rr = torch.sqrt(z_broadcast**2 + y_broadcast**2 + x_broadcast**2) wavelength = wavelength_illumination / index_of_refraction_media - nu_max = (17 / 16) / (wavelength) - nu_min = (15 / 16) / (wavelength) + nu_max = (33 / 32) / (wavelength) + nu_min = (31 / 32) / (wavelength) mask = torch.logical_and(nu_rr < nu_max, nu_rr > nu_min) @@ -121,18 +121,33 @@ ## CANDIDATE FOR REMOVAL # Quick Green's tensor, directly in Fourier space - G_3D[0, 0] = (1/wavelength) - z_broadcast**2 + G_3D[0, 0] = (1 / wavelength)**2 - (z_broadcast)**2 G_3D[0, 1] = -z_broadcast * y_broadcast G_3D[0, 2] = -z_broadcast * x_broadcast G_3D[1, 0] = -y_broadcast * z_broadcast - G_3D[1, 1] = (1/wavelength) - y_broadcast**2 + G_3D[1, 1] = (1 / wavelength)**2 - (y_broadcast)**2 G_3D[1, 2] = -y_broadcast * x_broadcast G_3D[2, 0] = -x_broadcast * z_broadcast G_3D[2, 1] = -x_broadcast * y_broadcast - G_3D[2, 2] = (1/wavelength) - x_broadcast**2 + G_3D[2, 2] = (1 / wavelength)**2 - (x_broadcast)**2 G_3D *= 1j * mask G_3D /= torch.amax(torch.abs(G_3D)) + # Use this to visualize Green's tensor + # from waveorder.visuals.napari_visuals import ( + # add_transfer_function_to_viewer, + # ) + # import napari + + # v = napari.Viewer() + # add_transfer_function_to_viewer( + # v, + # G_3D, + # zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + # complex_rgb=True, + # ) + # import pdb; pdb.set_trace() + # Main transfer function calculation PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) From d0d6f7ed68fef283c509f7ff543004b7bada69c3 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 8 Oct 2024 14:17:08 -0700 Subject: [PATCH 55/72] modify circular anisotropy gellman matrices so that all transfer function are hermitian --- examples/models/plot-vector-tf.py | 10 +++++----- waveorder/util.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index e0290d0..b6a6faf 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -21,7 +21,7 @@ for i, numerical_aperture_illumination in enumerate([0.01, 0.5]): file_suffix = str(i) - input_jones = torch.tensor([0.0 - 1.0j, 1.0 + 0j]) # circular + input_jones = torch.tensor([-1j, 1.0 + 0j]) # circular # Calculate frequencies y_frequencies, x_frequencies = util.generate_frequencies( @@ -166,15 +166,15 @@ ) H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components - # H_im = 1j * (H1 - H2) # ignore absorptive terms + # H_im = 1j * (H1[1:, 1:] - H2[1:,1:]) # ignore absorptive terms H_re /= torch.amax(torch.abs(H_re)) s_labels = [0, 1, 2, 3] + f_labels = [0, 1, 2, 3, 4, 5, 6, 7, 8] #[[0, 4, 8]] s = util.pauli()[s_labels] # select s0, s1, and s2 (drop s3) - Y = util.gellmann()[[0, 4, 8]] + Y = util.gellmann()[f_labels] # select phase f00 and transverse linear isotropic terms 2-2, and f22 - sfZYX_transfer_function = torch.einsum( "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) @@ -212,7 +212,7 @@ zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), z_slice=-10, s_labels=s_labels, - f_labels=[0, 4, 8], + f_labels=f_labels, rose_path=None, inches_per_column=1, saturate_clim_fraction=0.2, diff --git a/waveorder/util.py b/waveorder/util.py index c797863..f3cf7be 100644 --- a/waveorder/util.py +++ b/waveorder/util.py @@ -2262,6 +2262,8 @@ def pauli(): ] ) return sigma + + # torch.allclose( # torch.abs(torch.einsum("kij,lji->kl", s, s) - torch.eye(4)), # torch.zeros((4, 4)), @@ -2280,25 +2282,26 @@ def gellmann(): # zyx # zyx a = 3**-0.5 - b = -1j * 2**-0.5 c = 2**-0.5 d = -(6**-0.5) e = 2 * (6**-0.5) return torch.tensor( [ [[a, 0, 0], [0, a, 0], [0, 0, a]], - [[0, 0, -b], [0, 0, 0], [b, 0, 0]], - [[0, 0, 0], [0, 0, -b], [0, b, 0]], - [[0, -b, 0], [b, 0, 0], [0, 0, 0]], + [[0, 0, -c], [0, 0, 0], [c, 0, 0]], + [[0, 0, 0], [0, 0, -c], [0, c, 0]], + [[0, -c, 0], [c, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, c], [0, c, 0]], # [[0, c, 0], [c, 0, 0], [0, 0, 0]], [[e, 0, 0], [0, d, 0], [0, 0, d]], [[0, 0, c], [0, 0, 0], [c, 0, 0]], [[0, 0, 0], [0, -c, 0], [0, 0, c]], # - ] + ], dtype=torch.complex64 ) # torch.allclose( + + # torch.abs(torch.einsum("kij,lji->kl", Y, Y) - torch.eye(9)), # torch.zeros((9, 9)), # atol=1e-5, From 92bb6d646ca091027cb9824324d57c3a7925294d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 14 Oct 2024 22:20:10 -0700 Subject: [PATCH 56/72] improved matplotlib visuals --- waveorder/visuals/matplotlib_visuals.py | 258 ++++++++++++++---------- 1 file changed, 149 insertions(+), 109 deletions(-) diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py index 122db79..56bad0c 100644 --- a/waveorder/visuals/matplotlib_visuals.py +++ b/waveorder/visuals/matplotlib_visuals.py @@ -8,54 +8,63 @@ import torch -def plot_transfer_function( - sfZYX_data, +def plot_5d_ortho( + rczyx_data, filename, zyx_scale, z_slice, - s_labels, - f_labels, + row_labels, + column_labels, rose_path=None, inches_per_column=1, saturate_clim_fraction=1.0, trim_edges=0, + fourier_space=True, ): - S, F, Z, Y, X = sfZYX_data.shape - voxel_scale = [ + R, C, Z, Y, X = rczyx_data.shape + + axis_extent = [ Z * zyx_scale[0], Y * zyx_scale[1], X * zyx_scale[2], ] + if fourier_space: + axis_extent = [1 / x for x in axis_extent] - sfZYX_data = nd_fourier_central_cuboid( - sfZYX_data, (S, F, Z - trim_edges, Y - trim_edges, X - trim_edges) + rczyx_data = nd_fourier_central_cuboid( + rczyx_data, (R, C, Z - trim_edges, Y - trim_edges, X - trim_edges) ) - S, F, Z, Y, X = sfZYX_data.shape - sfZYX_data = torch.fft.ifftshift(sfZYX_data, dim=(-3, -2, -1)) - sfZYX_rgb = complex_tensor_to_rgb(sfZYX_data, saturate_clim_fraction) - - assert S == len(s_labels) - assert F == len(f_labels) + R, C, Z, Y, X = rczyx_data.shape # after cropping - X_size = 1 * inches_per_column - Y_size = (zyx_scale[2] / zyx_scale[1]) * inches_per_column - Z_size = (zyx_scale[2] / zyx_scale[0]) * inches_per_column + rczyx_data = torch.fft.ifftshift(rczyx_data, dim=(-3, -2, -1)) + sfZYX_rgb = complex_tensor_to_rgb(rczyx_data, saturate_clim_fraction) + assert R == len(row_labels) + assert C == len(column_labels) - n_rows = S + 1 - n_cols = (F * 3) + 1 - - height = n_rows - width = n_cols + n_rows = (R * 2) + 1 + n_cols = (C * 2) + 1 fig, axes = plt.subplots( n_rows, n_cols, - figsize=(width, height), + figsize=(n_cols, n_rows), gridspec_kw={ "wspace": 0.1, # Adjust this value to reduce space between columns "hspace": 0.1, # Adjust this value to reduce space between rows + "width_ratios": [1] + + C + * [ + 1, + Z / X, + ], + "height_ratios": [1] + + R + * [ + 1, + Z / Y, + ], }, ) @@ -67,14 +76,14 @@ def plot_transfer_function( for i in range(n_rows): for j in range(n_cols): - if (i == 0 and (j - 1) % 3 == 1) or (j == 0 and i > 0): + if (i == 0 and (j - 1) % 2 == 0) or (j == 0 and (i - 1) % 2 == 0): if i == 0: folder = "gellman" - index = f_labels[int((j - 1) / 3)] + index = column_labels[int((j - 1) / 2)] else: folder = "stokes" - index = s_labels[i - 1] - + index = row_labels[int((i - 1) / 2)] + if isinstance(index, int): image_path = os.path.join( os.path.dirname(__file__), @@ -94,21 +103,31 @@ def plot_transfer_function( ) if i > 0 and j > 0: - if (j - 1) % 3 == 0: + # XY + if (i - 1) % 2 == 0 and (j - 1) % 2 == 0: axes[i, j].imshow( - sfZYX_rgb[i - 1, int((j - 1) / 3), (Z // 2) + z_slice], - aspect=voxel_scale[1] / voxel_scale[2], - #interpolation=None + sfZYX_rgb[ + int((i - 1) / 2), + int((j - 1) / 2), + (Z // 2) + z_slice, + ], + aspect=axis_extent[1] / axis_extent[2], ) - elif (j - 1) % 3 == 1: + # YZ + elif (i - 1) % 2 == 0 and (j - 1) % 2 == 1: axes[i, j].imshow( - sfZYX_rgb[i - 1, int((j - 1) / 3), :, :, X // 2, :], - aspect=voxel_scale[2] / voxel_scale[0], + sfZYX_rgb[ + int((i - 1) / 2), int((j - 1) / 2), :, :, X // 2, : + ].transpose(1, 0, 2), + aspect=axis_extent[1] / axis_extent[0], ) - elif (j - 1) % 3 == 2: + # XZ + elif (i - 1) % 2 == 1 and (j - 1) % 2 == 0: axes[i, j].imshow( - sfZYX_rgb[i - 1, int((j - 1) / 3), :, Y // 2], - aspect=voxel_scale[1] / voxel_scale[0], + sfZYX_rgb[ + int((i - 1) / 2), int((j - 1) / 2), :, Y // 2 + ], + aspect=axis_extent[0] / axis_extent[1], ) axes[i, j].tick_params( @@ -120,8 +139,11 @@ def plot_transfer_function( bottom = axes[-1, -1].get_position().y0 left = axes[0, 0].get_position().x0 right = axes[-1, -1].get_position().x1 - if i == 0 and (j - 1) % 3 == 0: - left_edge = (axes[0, j].get_position().x0 + axes[0, j - 1].get_position().x1)/2 + if i == 0 and (j - 1) % 2 == 0: + left_edge = ( + axes[0, j].get_position().x0 + + axes[0, j - 1].get_position().x1 + ) / 2 fig.add_artist( plt.Line2D( [left_edge, left_edge], @@ -131,8 +153,11 @@ def plot_transfer_function( lw=0.5, ) ) - if j == 0 and i > 0: - top_edge = (axes[i, 0].get_position().y1 + axes[i - 1, 0].get_position().y0)/2 + if j == 0 and (i - 1) % 2 == 0: + top_edge = ( + axes[i, 0].get_position().y1 + + axes[i - 1, 0].get_position().y0 + ) / 2 fig.add_artist( plt.Line2D( [left, right], @@ -148,22 +173,20 @@ def plot_transfer_function( axes[i, j].spines["bottom"].set_visible(False) axes[i, j].spines["left"].set_visible(False) - # Draw ortho view lines + # Draw ortho view lines fig.add_artist( plt.Line2D( - #[(X/2) - 0.5*X/np.sqrt(2) , (X/2) + 0.5*X/np.sqrt(2)], - #[(Y/2) - 0.5*Y/np.sqrt(2) , (Y/2) + 0.5*Y/np.sqrt(2)], - [Y//2, Y//2], + [Y // 2, Y // 2], [0, X], transform=axes[1, 1].transData, # use axis coordinates color="red", lw=0.5, ) ) - fig.add_artist( + fig.add_artist( plt.Line2D( [0, X], - [Y//2, Y//2], # from bottom to top in axis coordinates + [Y // 2, Y // 2], # from bottom to top in axis coordinates transform=axes[1, 1].transData, # use axis coordinates color="blue", lw=0.5, @@ -175,41 +198,48 @@ def plot_transfer_function( X, # width Y, # height linewidth=0.5, - edgecolor='green', - facecolor='none', - transform=axes[1, 1].transData # use axis coordinates + edgecolor="green", + facecolor="none", + transform=axes[1, 1].transData, # use axis coordinates ) ) axes[1, 1].text( - 0.1, 0.975, 'x', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 1].transAxes, - fontsize=5, - color='black' + 0.1, + 0.975, + "x", + horizontalalignment="left", + verticalalignment="top", + transform=axes[1, 1].transAxes, + fontsize=5, + color="black", ) axes[1, 1].text( - 0.025, 0.9, 'y', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 1].transAxes, - fontsize=5, - color='black' + 0.025, + 0.9, + "y", + horizontalalignment="left", + verticalalignment="top", + transform=axes[1, 1].transAxes, + fontsize=5, + color="black", ) - - fig.add_artist( + + fig.add_artist( plt.Line2D( - [0, X], - [Z//2 + z_slice, Z//2 + z_slice], # from bottom to top in axis coordinates + [ + Z // 2 + z_slice, + Z // 2 + z_slice, + ], + [0, Y], transform=axes[1, 2].transData, # use axis coordinates color="green", lw=0.5, ) ) - fig.add_artist( + fig.add_artist( plt.Line2D( - [X//2, X//2], [0, Z], # from bottom to top in axis coordinates + [Y // 2, Y // 2], transform=axes[1, 2].transData, # use axis coordinates color="blue", lw=0.5, @@ -218,47 +248,53 @@ def plot_transfer_function( rect = plt.Rectangle( (0, 0), # lower-left corner - X, # width - Z, # height + Z, # width + Y, # height linewidth=0.5, - edgecolor='red', - facecolor='none', - transform=axes[1, 2].transData # use axis coordinates + edgecolor="red", + facecolor="none", + transform=axes[1, 2].transData, # use axis coordinates ) fig.add_artist(rect) axes[1, 2].text( - 0.1, 0.975, 'y', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 2].transAxes, - fontsize=5, - color='black' + 0.1, + 0.975, + "z", + horizontalalignment="left", + verticalalignment="top", + transform=axes[1, 2].transAxes, + fontsize=5, + color="black", ) axes[1, 2].text( - 0.025, 0.9, 'z', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 2].transAxes, - fontsize=5, - color='black' + 0.025, + 0.9, + "y", + horizontalalignment="left", + verticalalignment="top", + transform=axes[1, 2].transAxes, + fontsize=5, + color="black", ) - - fig.add_artist( + fig.add_artist( plt.Line2D( [0, X], - [Z//2 + z_slice, Z//2 + z_slice], # from bottom to top in axis coordinates - transform=axes[1, 3].transData, # use axis coordinates + [ + Z // 2 + z_slice, + Z // 2 + z_slice, + ], # from bottom to top in axis coordinates + transform=axes[2, 1].transData, # use axis coordinates color="green", lw=0.5, ) ) - fig.add_artist( + fig.add_artist( plt.Line2D( - [X//2, X//2], + [X // 2, X // 2], [0, Z], # from bottom to top in axis coordinates - transform=axes[1, 3].transData, # use axis coordinates + transform=axes[2, 1].transData, # use axis coordinates color="red", lw=0.5, ) @@ -269,27 +305,31 @@ def plot_transfer_function( X, # width Z, # height linewidth=0.5, - edgecolor='blue', - facecolor='none', - transform=axes[1, 3].transData # use axis coordinates + edgecolor="blue", + facecolor="none", + transform=axes[2, 1].transData, # use axis coordinates ) fig.add_artist(rect) - axes[1, 3].text( - 0.1, 0.975, 'x', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 3].transAxes, - fontsize=5, - color='black' + axes[2, 1].text( + 0.1, + 0.975, + "x", + horizontalalignment="left", + verticalalignment="top", + transform=axes[2, 1].transAxes, + fontsize=5, + color="black", ) - axes[1, 3].text( - 0.025, 0.9, 'z', - horizontalalignment='left', - verticalalignment='top', - transform=axes[1, 3].transAxes, - fontsize=5, - color='black' + axes[2, 1].text( + 0.025, + 0.9, + "z", + horizontalalignment="left", + verticalalignment="top", + transform=axes[2, 1].transAxes, + fontsize=5, + color="black", ) print(f"Saving {filename}") From 7cece20f30a426761d70692015dde387edc4b8e2 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 14 Oct 2024 22:22:48 -0700 Subject: [PATCH 57/72] clean up plotting script --- examples/models/plot-vector-tf.py | 170 ++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 57 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index b6a6faf..5c376c1 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -1,9 +1,10 @@ import torch import os +import numpy as np from waveorder import util, optics -from waveorder.visuals.matplotlib_visuals import plot_transfer_function +from waveorder.visuals.matplotlib_visuals import plot_5d_ortho -output_folder = "2024-10-07" +output_folder = "2024-10-14" os.makedirs(output_folder, exist_ok=True) # Parameters @@ -16,12 +17,16 @@ wavelength_illumination = 0.532 z_padding = 0 index_of_refraction_media = 1.3 -numerical_aperture_detection = 1.2 +numerical_aperture_detection = 1.0 -for i, numerical_aperture_illumination in enumerate([0.01, 0.5]): +for i, numerical_aperture_illumination in enumerate([0.01, 0.75]): file_suffix = str(i) - input_jones = torch.tensor([-1j, 1.0 + 0j]) # circular + input_jones = torch.tensor([-1j, 1.0 + 0j]) / torch.sqrt( + torch.tensor([2]) + ) # circular + + # input_jones = torch.tensor([0, 1.0 + 0j]) # Calculate frequencies y_frequencies, x_frequencies = util.generate_frequencies( @@ -87,16 +92,8 @@ z_position_list, ) - G = optics.generate_defocus_greens_tensor( - x_frequencies, - y_frequencies, - greens_functions_z, - pupil, - lambda_in=wavelength_illumination / index_of_refraction_media, - ) P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) - G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) S_3D = torch.fft.ifft(S, dim=-3) ## CANDIDATE FOR REMOVAL @@ -115,38 +112,44 @@ mask = torch.logical_and(nu_rr < nu_max, nu_rr > nu_min) P_3D *= mask - G_3D *= mask S_3D *= mask ## CANDIDATE FOR REMOVAL - # Quick Green's tensor, directly in Fourier space - G_3D[0, 0] = (1 / wavelength)**2 - (z_broadcast)**2 - G_3D[0, 1] = -z_broadcast * y_broadcast - G_3D[0, 2] = -z_broadcast * x_broadcast - G_3D[1, 0] = -y_broadcast * z_broadcast - G_3D[1, 1] = (1 / wavelength)**2 - (y_broadcast)**2 - G_3D[1, 2] = -y_broadcast * x_broadcast - G_3D[2, 0] = -x_broadcast * z_broadcast - G_3D[2, 1] = -x_broadcast * y_broadcast - G_3D[2, 2] = (1 / wavelength)**2 - (x_broadcast)**2 - G_3D *= 1j * mask - G_3D /= torch.amax(torch.abs(G_3D)) + # TODO REFACTOR: Direct green's tensor + Z = z_total + Y = zyx_shape[1] + X = zyx_shape[2] - # Use this to visualize Green's tensor - # from waveorder.visuals.napari_visuals import ( - # add_transfer_function_to_viewer, - # ) - # import napari + z_step = torch.fft.ifftshift( + (torch.arange(z_total) - z_total // 2) * z_pixel_size + ) + y_step = torch.fft.ifftshift((torch.arange(Y) - Y // 2) * yx_pixel_size) + x_step = torch.fft.ifftshift((torch.arange(X) - X // 2) * yx_pixel_size) - # v = napari.Viewer() - # add_transfer_function_to_viewer( - # v, - # G_3D, - # zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), - # complex_rgb=True, - # ) - # import pdb; pdb.set_trace() + zz = torch.broadcast_to(z_step[:, None, None], (Z, Y, X)) + yy = torch.broadcast_to(y_step[None, :, None], (Z, Y, X)) + xx = torch.broadcast_to(x_step[None, None, :], (Z, Y, X)) + + rr = torch.sqrt(xx**2 + yy**2 + zz**2) + rhat = torch.stack([zz, yy, xx], dim=0) / rr + + scalar_g = torch.exp(1j * 2 * torch.pi * rr / wavelength) / ( + 4 * torch.pi * rr + ) + + eye = torch.zeros((3, 3, Z, Y, X)) + eye[0, 0] = 1 + eye[1, 1] = 1 + eye[2, 2] = 1 + + Q = eye - torch.einsum("izyx,jzyx->ijzyx", rhat, rhat) + g_3d = Q * scalar_g + g_3d = torch.nan_to_num(g_3d) + + G_3D = torch.fft.fftn(g_3d, dim=(-3, -2, -1)) + G_3D = torch.imag(G_3D) * 1j + G_3D /= torch.amax(torch.abs(G_3D)) # Main transfer function calculation PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) @@ -164,14 +167,20 @@ torch.einsum("ikzyx,jpzyx->ijpkzyx", ps, torch.conj(pg)), dim=(-3, -2, -1), ) + + # Not necessary yet + illum_intensity = torch.sum(S_3D * torch.conj(S_3D), dim=0) + direct_intensity = torch.real( + torch.sum(illum_intensity * P_3D) * torch.sum(P_3D) + ) H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components # H_im = 1j * (H1[1:, 1:] - H2[1:,1:]) # ignore absorptive terms - H_re /= torch.amax(torch.abs(H_re)) + # H_re /= torch.amax(torch.abs(H_re)) s_labels = [0, 1, 2, 3] - f_labels = [0, 1, 2, 3, 4, 5, 6, 7, 8] #[[0, 4, 8]] + f_labels = [0, 4, 8] s = util.pauli()[s_labels] # select s0, s1, and s2 (drop s3) Y = util.gellmann()[f_labels] # select phase f00 and transverse linear isotropic terms 2-2, and f22 @@ -179,42 +188,89 @@ "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) - # Make plots - plot_transfer_function( + sfzyx_point_response_function = torch.fft.fftn( + sfZYX_transfer_function, dim=(-3, -2, -1) + ) + + # # Use this to visualize Green's tensor + # from waveorder.visuals.napari_visuals import ( + # add_transfer_function_to_viewer, + # ) + # import napari + + # v = napari.Viewer() + # add_transfer_function_to_viewer( + # v, + # sfZYX_transfer_function, + # zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + # complex_rgb=True, + # ) + # import pdb + # pdb.set_trace() + + plot_5d_ortho( + g_3d, + filename=os.path.join(output_folder, f"greens_{file_suffix}.pdf"), + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + z_slice=0, + row_labels=["Z", "Y", "X"], + column_labels=["Z", "Y", "X"], + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=1.0, + trim_edges=50, + fourier_space=False, + ) + + plot_5d_ortho( G_3D, filename=os.path.join(output_folder, f"G_{file_suffix}.pdf"), zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), z_slice=0, - s_labels=["Z", "Y", "X"], - f_labels=["Z", "Y", "X"], + row_labels=["Z", "Y", "X"], + column_labels=["Z", "Y", "X"], rose_path=None, inches_per_column=1, - saturate_clim_fraction=1.0, + saturate_clim_fraction=0.7, trim_edges=0, ) - plot_transfer_function( - S_3D[None], - filename=os.path.join(output_folder, f"S_{file_suffix}.pdf"), + plot_5d_ortho( + S_3D[1:][None], + filename=os.path.join(output_folder, f"source_{file_suffix}.pdf"), zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), z_slice=-35, - s_labels=[""], - f_labels=["Z", "Y", "X"], + row_labels=[""], + column_labels=["Y", "X"], rose_path=None, inches_per_column=1, - saturate_clim_fraction=0.5, + saturate_clim_fraction=0.75, trim_edges=0, + fourier_space=True, ) - plot_transfer_function( + plot_5d_ortho( sfZYX_transfer_function, - filename=os.path.join(output_folder, f"H_{file_suffix}.pdf"), + filename=os.path.join(output_folder, f"tf_{file_suffix}.pdf"), zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), - z_slice=-10, - s_labels=s_labels, - f_labels=f_labels, + z_slice=-5, + row_labels=s_labels, + column_labels=f_labels, rose_path=None, inches_per_column=1, saturate_clim_fraction=0.2, trim_edges=40, ) + + plot_5d_ortho( + sfzyx_point_response_function, + filename=os.path.join(output_folder, f"psf_{file_suffix}.pdf"), + zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), + z_slice=-10, + row_labels=s_labels, + column_labels=f_labels, + rose_path=None, + inches_per_column=1, + saturate_clim_fraction=0.2, + trim_edges=60, + ) From 74c8bfc8535ed15d73ec863f366f4b4f981b6109 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 14 Oct 2024 22:23:15 -0700 Subject: [PATCH 58/72] fix bug with 3x3 hardcoded shape --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 5853c49..ddc79d7 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -89,8 +89,8 @@ def calculate_transfer_function( # Crop to original size sfzyx_out_shape = ( - 3, - 3, + pooled_sfZYX_transfer_function.shape[0], + pooled_sfZYX_transfer_function.shape[1], zyx_shape[0] + 2 * z_padding, ) + zyx_shape[1:] return ( From ee6b7440fb6864f49adbdedda280898c305c9199 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 14 Oct 2024 22:24:13 -0700 Subject: [PATCH 59/72] update tf components --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index ddc79d7..29ef8bb 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -239,7 +239,7 @@ def _calculate_wrap_unsafe_transfer_function( H_re /= torch.amax(torch.abs(H_re)) - s = util.pauli()[[0, 1, 2]] # select s0, s1, and s2 (drop s3) + s = util.pauli()[[0, 1, 2, 3]] # select s0, s1, and s2 Y = util.gellmann()[[0, 4, 8]] # select phase f00 and transverse linear isotropic terms 2-2, and f22 From 3ffbf4f56d4126769c862043b990fa18c144061e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Mon, 14 Oct 2024 22:24:39 -0700 Subject: [PATCH 60/72] minor reconstruction updates --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 29ef8bb..2bd3b1c 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -258,7 +258,7 @@ def calculate_singular_system(sfZYX_transfer_function): print("Computing SVD") ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) - S /= torch.max(S) + S # /= torch.max(S) singular_system = (U, S, Vh) return singular_system @@ -302,9 +302,9 @@ def apply_inverse_transfer_function( # Key computation print("Computing inverse filter") U, S, Vh = singular_system - S_reg = S / (S**2 + regularization_strength**2) + S_reg = S / (S**2 + regularization_strength) ZYXsf_inverse_filter = torch.einsum( - "zyxij,zyxj,zyxjk->zyxki", U, S_reg, Vh + "zyxij,zyxj,zyxjk->zyxik", U, S_reg, Vh ) # Apply inverse filter From 5036883a76fcf55aa18812068d79dfabe81078d0 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 15 Oct 2024 14:07:16 -0700 Subject: [PATCH 61/72] refactor greens tensor spectrum --- waveorder/optics.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/waveorder/optics.py b/waveorder/optics.py index 389191b..fa7bd37 100644 --- a/waveorder/optics.py +++ b/waveorder/optics.py @@ -717,6 +717,60 @@ def gen_dyadic_Greens_tensor(G_real, ps, psz, lambda_in, space="real"): ) +def generate_greens_tensor_spectrum( + zyx_shape, + zyx_pixel_size, + wavelength, + ): + """ + Parameters + ---------- + zyx_shape : tuple + zyx_pixel_size : tuple + wavelength : float + wavelength in medium + + Returns + ------- + torch.tensor + Green's tensor spectrum + """ + Z, Y, X = zyx_shape + dZ, dY, dX = zyx_pixel_size + + z_step = torch.fft.ifftshift( + (torch.arange(Z) - Z // 2) * dZ + ) + y_step = torch.fft.ifftshift((torch.arange(Y) - Y // 2) * dY) + x_step = torch.fft.ifftshift((torch.arange(X) - X // 2) * dX) + + zz = torch.broadcast_to(z_step[:, None, None], (Z, Y, X)) + yy = torch.broadcast_to(y_step[None, :, None], (Z, Y, X)) + xx = torch.broadcast_to(x_step[None, None, :], (Z, Y, X)) + + rr = torch.sqrt(xx**2 + yy**2 + zz**2) + rhat = torch.stack([zz, yy, xx], dim=0) / rr + + scalar_g = torch.exp(1j * 2 * torch.pi * rr / wavelength) / ( + 4 * torch.pi * rr + ) + + eye = torch.zeros((3, 3, Z, Y, X)) + eye[0, 0] = 1 + eye[1, 1] = 1 + eye[2, 2] = 1 + + Q = eye - torch.einsum("izyx,jzyx->ijzyx", rhat, rhat) + g_3d = Q * scalar_g + g_3d = torch.nan_to_num(g_3d) + + G_3D = torch.fft.fftn(g_3d, dim=(-3, -2, -1)) + G_3D = torch.imag(G_3D) * 1j + G_3D /= torch.amax(torch.abs(G_3D)) + + return G_3D + + def compute_weak_object_transfer_function_2d( illumination_pupil, detection_pupil ): From 17d8283931ccc080e992b93143583bf1eb8bd3d7 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 15 Oct 2024 14:07:48 -0700 Subject: [PATCH 62/72] clean test script --- examples/models/plot-vector-tf.py | 55 +++++-------------------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/examples/models/plot-vector-tf.py b/examples/models/plot-vector-tf.py index 5c376c1..92d7418 100644 --- a/examples/models/plot-vector-tf.py +++ b/examples/models/plot-vector-tf.py @@ -4,7 +4,7 @@ from waveorder import util, optics from waveorder.visuals.matplotlib_visuals import plot_5d_ortho -output_folder = "2024-10-14" +output_folder = "2024-10-15" os.makedirs(output_folder, exist_ok=True) # Parameters @@ -65,14 +65,6 @@ z_position_list, ) - greens_functions_z = optics.generate_greens_function_z( - radial_frequencies, - pupil, - wavelength_illumination / index_of_refraction_media, - z_position_list, - axially_even=True, - ) - # Calculate vector defocus pupils S = optics.generate_vector_source_defocus_pupil( x_frequencies, @@ -95,6 +87,11 @@ P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) S_3D = torch.fft.ifft(S, dim=-3) + G_3D = optics.generate_greens_tensor_spectrum( + zyx_shape=(z_total, zyx_shape[1], zyx_shape[2]), + zyx_pixel_size=(z_pixel_size, yx_pixel_size, yx_pixel_size), + wavelength=wavelength_illumination / index_of_refraction_media + ) ## CANDIDATE FOR REMOVAL # cleanup some ringing @@ -114,42 +111,7 @@ P_3D *= mask S_3D *= mask - ## CANDIDATE FOR REMOVAL - - # TODO REFACTOR: Direct green's tensor - Z = z_total - Y = zyx_shape[1] - X = zyx_shape[2] - - z_step = torch.fft.ifftshift( - (torch.arange(z_total) - z_total // 2) * z_pixel_size - ) - y_step = torch.fft.ifftshift((torch.arange(Y) - Y // 2) * yx_pixel_size) - x_step = torch.fft.ifftshift((torch.arange(X) - X // 2) * yx_pixel_size) - - zz = torch.broadcast_to(z_step[:, None, None], (Z, Y, X)) - yy = torch.broadcast_to(y_step[None, :, None], (Z, Y, X)) - xx = torch.broadcast_to(x_step[None, None, :], (Z, Y, X)) - - rr = torch.sqrt(xx**2 + yy**2 + zz**2) - rhat = torch.stack([zz, yy, xx], dim=0) / rr - - scalar_g = torch.exp(1j * 2 * torch.pi * rr / wavelength) / ( - 4 * torch.pi * rr - ) - - eye = torch.zeros((3, 3, Z, Y, X)) - eye[0, 0] = 1 - eye[1, 1] = 1 - eye[2, 2] = 1 - - Q = eye - torch.einsum("izyx,jzyx->ijzyx", rhat, rhat) - g_3d = Q * scalar_g - g_3d = torch.nan_to_num(g_3d) - - G_3D = torch.fft.fftn(g_3d, dim=(-3, -2, -1)) - G_3D = torch.imag(G_3D) * 1j - G_3D /= torch.amax(torch.abs(G_3D)) + # CANDIDATE FOR REMOVAL # Main transfer function calculation PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) @@ -208,6 +170,7 @@ # import pdb # pdb.set_trace() + g_3d = torch.fft.ifftn(G_3D, dim=(-3, -2, -1)) plot_5d_ortho( g_3d, filename=os.path.join(output_folder, f"greens_{file_suffix}.pdf"), @@ -244,7 +207,7 @@ column_labels=["Y", "X"], rose_path=None, inches_per_column=1, - saturate_clim_fraction=0.75, + saturate_clim_fraction=1.0, trim_edges=0, fourier_space=True, ) From b19c0c97a51e9e74b083000fe8300d64d1211bf7 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 15 Oct 2024 14:09:18 -0700 Subject: [PATCH 63/72] clean models --- .../inplane_oriented_thick_pol3d_vector.py | 49 +++++-------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 2bd3b1c..e4b30a6 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -17,8 +17,8 @@ def generate_test_phantom(zyx_shape): margin=50, ) c00 = yx_star - c2_2 = -torch.sin(2 * yx_theta) * yx_star #torch.zeros_like(c00) - c22 = -torch.cos(2 * yx_theta) * yx_star #torch.zeros_like(c00) # + c2_2 = -torch.sin(2 * yx_theta) * yx_star # torch.zeros_like(c00) + c22 = -torch.cos(2 * yx_theta) * yx_star # torch.zeros_like(c00) # # Put in a center slices of a 3D object center_slice_object = torch.stack((c00, c2_2, c22), dim=0) @@ -160,13 +160,6 @@ def _calculate_wrap_unsafe_transfer_function( z_position_list, ) - greens_functions_z = optics.generate_greens_function_z( - radial_frequencies, - pupil, - wavelength_illumination / index_of_refraction_media, - z_position_list, - ) - # Calculate vector defocus pupils S = optics.generate_vector_source_defocus_pupil( x_frequencies, @@ -186,41 +179,24 @@ def _calculate_wrap_unsafe_transfer_function( z_position_list, ) - G = optics.generate_defocus_greens_tensor( - x_frequencies, - y_frequencies, - greens_functions_z, - pupil, - lambda_in=wavelength_illumination / index_of_refraction_media, - ) - P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) - G_3D = torch.abs(torch.fft.ifft(G, dim=-3)) * (-1j) S_3D = torch.fft.ifft(S, dim=-3) + G_3D = optics.generate_greens_tensor_spectrum( + zyx_shape=(z_total, zyx_shape[1], zyx_shape[2]), + zyx_pixel_size=(z_pixel_size, yx_pixel_size, yx_pixel_size), + wavelength=wavelength_illumination / index_of_refraction_media, + ) + import pdb; pdb.set_trace() - # cleanup - freq_shape = z_position_list.shape + x_frequencies.shape - - z_broadcast = torch.broadcast_to(z_frequencies[:, None, None], freq_shape) - y_broadcast = torch.broadcast_to(y_frequencies[None, :, :], freq_shape) - x_broadcast = torch.broadcast_to(x_frequencies[None, :, :], freq_shape) - - nu_rr = torch.sqrt(z_broadcast**2 + y_broadcast**2 + x_broadcast**2) - wavelength = wavelength_illumination / index_of_refraction_media - nu_max = (17 / 16) / (wavelength) - nu_min = (15 / 16) / (wavelength) - - mask = torch.logical_and(nu_rr < nu_max, nu_rr > nu_min) - P_3D *= mask - G_3D *= mask - S_3D *= mask + # P_3D *= mask + # G_3D *= mask + # S_3D *= mask # Main part PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) - pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) @@ -246,7 +222,6 @@ def _calculate_wrap_unsafe_transfer_function( sfZYX_transfer_function = torch.einsum( "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) - return ( sfZYX_transfer_function, intensity_to_stokes_matrix, @@ -258,7 +233,6 @@ def calculate_singular_system(sfZYX_transfer_function): print("Computing SVD") ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) - S # /= torch.max(S) singular_system = (U, S, Vh) return singular_system @@ -303,6 +277,7 @@ def apply_inverse_transfer_function( print("Computing inverse filter") U, S, Vh = singular_system S_reg = S / (S**2 + regularization_strength) + ZYXsf_inverse_filter = torch.einsum( "zyxij,zyxj,zyxjk->zyxik", U, S_reg, Vh ) From ab745e98ffc3ee4414c81537bff71def203c71f4 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 15 Oct 2024 16:19:24 -0700 Subject: [PATCH 64/72] simple memory reduction --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index e4b30a6..27aa2a3 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -197,9 +197,13 @@ def _calculate_wrap_unsafe_transfer_function( PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) PS_3D = torch.einsum("zyx,jzyx,kzyx->jkzyx", P_3D, S_3D, torch.conj(S_3D)) + del P_3D, G_3D, S_3D + pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) + del PG_3D, PS_3D + H1 = torch.fft.ifftn( torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), dim=(-3, -2, -1), @@ -213,6 +217,8 @@ def _calculate_wrap_unsafe_transfer_function( H_re = H1[1:, 1:] + H2[1:, 1:] # drop data-side z components # H_im = 1j * (H1 - H2) # ignore absorptive terms + del H1, H2 + H_re /= torch.amax(torch.abs(H_re)) s = util.pauli()[[0, 1, 2, 3]] # select s0, s1, and s2 From f2f0b03ea1f3ac45f61d4d23d9eecc7f609dfcda Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 16 Oct 2024 09:48:27 -0700 Subject: [PATCH 65/72] clean debug statements --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 27aa2a3..576d211 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -186,12 +186,6 @@ def _calculate_wrap_unsafe_transfer_function( zyx_pixel_size=(z_pixel_size, yx_pixel_size, yx_pixel_size), wavelength=wavelength_illumination / index_of_refraction_media, ) - import pdb; pdb.set_trace() - - - # P_3D *= mask - # G_3D *= mask - # S_3D *= mask # Main part PG_3D = torch.einsum("zyx,ipzyx->ipzyx", P_3D, G_3D) From ef8ac7753fba1ed82de07b7f256ca29cbd9fba6e Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 16 Oct 2024 09:48:45 -0700 Subject: [PATCH 66/72] reorder svd for clean i/o --- .../models/inplane_oriented_thick_pol3d_vector.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 576d211..54b2bf4 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -231,9 +231,12 @@ def _calculate_wrap_unsafe_transfer_function( def calculate_singular_system(sfZYX_transfer_function): # Compute regularized inverse filter print("Computing SVD") - ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) - U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) - singular_system = (U, S, Vh) + U, S, Vh = torch.linalg.svd( + sfZYX_transfer_function.permute(2, 3, 4, 0, 1), full_matrices=False + ) + singular_system = (U.permute(3, 4, 0, 1, 2), + S.permute(3, 0, 1, 2), + Vh.permute(3, 4, 0, 1, 2)) return singular_system @@ -279,12 +282,12 @@ def apply_inverse_transfer_function( S_reg = S / (S**2 + regularization_strength) ZYXsf_inverse_filter = torch.einsum( - "zyxij,zyxj,zyxjk->zyxik", U, S_reg, Vh + "sjzyx,jzyx,jfzyx->sfzyx", U, S_reg, Vh ) # Apply inverse filter fZYX_reconstructed = torch.einsum( - "szyx,zyxsf->fzyx", sZYX_data, ZYXsf_inverse_filter + "szyx,sfzyx->fzyx", sZYX_data, ZYXsf_inverse_filter ) return torch.real(torch.fft.ifftn(fZYX_reconstructed, dim=(1, 2, 3))) From 4fccfcc63494b19dc23363b13d1d42051c261ea5 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 16 Oct 2024 10:11:08 -0700 Subject: [PATCH 67/72] invert phase contrast --- waveorder/models/inplane_oriented_thick_pol3d_vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 54b2bf4..1117f2d 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -131,7 +131,7 @@ def _calculate_wrap_unsafe_transfer_function( z_position_list = torch.fft.ifftshift( (torch.arange(z_total) - z_total // 2) * z_pixel_size ) - if invert_phase_contrast: + if not invert_phase_contrast: # opposite sign of direct phase reconstruction z_position_list = torch.flip(z_position_list, dims=(0,)) z_frequencies = torch.fft.fftfreq(z_total, d=z_pixel_size) From 0d7ad4bf29488f1988b2172fc798f184f5637243 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 16 Oct 2024 14:51:29 -0700 Subject: [PATCH 68/72] formatting --- .../inplane_oriented_thick_pol3d_vector.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 1117f2d..e2ae4df 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -114,6 +114,7 @@ def _calculate_wrap_unsafe_transfer_function( numerical_aperture_detection, invert_phase_contrast=False, ): + print("Computing transfer function") intensity_to_stokes_matrix = stokes.calculate_intensity_to_stokes_matrix( swing, scheme=scheme ) @@ -131,7 +132,9 @@ def _calculate_wrap_unsafe_transfer_function( z_position_list = torch.fft.ifftshift( (torch.arange(z_total) - z_total // 2) * z_pixel_size ) - if not invert_phase_contrast: # opposite sign of direct phase reconstruction + if ( + not invert_phase_contrast + ): # opposite sign of direct phase reconstruction z_position_list = torch.flip(z_position_list, dims=(0,)) z_frequencies = torch.fft.fftfreq(z_total, d=z_pixel_size) @@ -231,12 +234,15 @@ def _calculate_wrap_unsafe_transfer_function( def calculate_singular_system(sfZYX_transfer_function): # Compute regularized inverse filter print("Computing SVD") + ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) U, S, Vh = torch.linalg.svd( - sfZYX_transfer_function.permute(2, 3, 4, 0, 1), full_matrices=False + ZYXsf_transfer_function, full_matrices=False + ) + singular_system = ( + U.permute(3, 4, 0, 1, 2), + S.permute(3, 0, 1, 2), + Vh.permute(3, 4, 0, 1, 2), ) - singular_system = (U.permute(3, 4, 0, 1, 2), - S.permute(3, 0, 1, 2), - Vh.permute(3, 4, 0, 1, 2)) return singular_system From 7242f66aa6a54c29731e05caa95c34957db507f7 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Thu, 17 Oct 2024 11:35:31 -0700 Subject: [PATCH 69/72] padding warning --- .../models/inplane_oriented_thick_pol3d_vector.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index e2ae4df..1df364a 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -41,6 +41,9 @@ def calculate_transfer_function( invert_phase_contrast=False, fourier_oversample_factor=1, ): + if z_padding != 0: + raise NotImplementedError("Padding not implemented for this model") + transverse_nyquist = sampling.transverse_nyquist( wavelength_illumination, numerical_aperture_illumination, @@ -196,11 +199,13 @@ def _calculate_wrap_unsafe_transfer_function( del P_3D, G_3D, S_3D + print("\tComputing pg and ps...") pg = torch.fft.fftn(PG_3D, dim=(-3, -2, -1)) ps = torch.fft.fftn(PS_3D, dim=(-3, -2, -1)) del PG_3D, PS_3D + print("\tComputing H1 and H2...") H1 = torch.fft.ifftn( torch.einsum("ipzyx,jkzyx->ijpkzyx", pg, torch.conj(ps)), dim=(-3, -2, -1), @@ -222,6 +227,7 @@ def _calculate_wrap_unsafe_transfer_function( Y = util.gellmann()[[0, 4, 8]] # select phase f00 and transverse linear isotropic terms 2-2, and f22 + print("\tComputing final transfer function...") sfZYX_transfer_function = torch.einsum( "sik,ikpjzyx,lpj->slzyx", s, H_re, Y ) @@ -235,9 +241,7 @@ def calculate_singular_system(sfZYX_transfer_function): # Compute regularized inverse filter print("Computing SVD") ZYXsf_transfer_function = sfZYX_transfer_function.permute(2, 3, 4, 0, 1) - U, S, Vh = torch.linalg.svd( - ZYXsf_transfer_function, full_matrices=False - ) + U, S, Vh = torch.linalg.svd(ZYXsf_transfer_function, full_matrices=False) singular_system = ( U.permute(3, 4, 0, 1, 2), S.permute(3, 0, 1, 2), From 726a28bf8a60dd27d0406f1780198540e63afe65 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Thu, 17 Oct 2024 11:54:06 -0700 Subject: [PATCH 70/72] revise visuals --- waveorder/visuals/matplotlib_visuals.py | 373 ++++++++++-------------- waveorder/visuals/utils.py | 8 +- 2 files changed, 165 insertions(+), 216 deletions(-) diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py index 56bad0c..b15f85e 100644 --- a/waveorder/visuals/matplotlib_visuals.py +++ b/waveorder/visuals/matplotlib_visuals.py @@ -9,73 +9,69 @@ def plot_5d_ortho( - rczyx_data, + rcCzyx_data, filename, - zyx_scale, - z_slice, - row_labels, - column_labels, + voxel_size, + zyx_slice, + color_func, + row_labels=None, + column_labels=None, rose_path=None, - inches_per_column=1, - saturate_clim_fraction=1.0, - trim_edges=0, - fourier_space=True, + inches_per_column=1.5, + label_size=1, + ortho_line_width=0.5, + row_column_line_width=0.5, + **kwargs, ): - R, C, Z, Y, X = rczyx_data.shape - - axis_extent = [ - Z * zyx_scale[0], - Y * zyx_scale[1], - X * zyx_scale[2], - ] - if fourier_space: - axis_extent = [1 / x for x in axis_extent] - - rczyx_data = nd_fourier_central_cuboid( - rczyx_data, (R, C, Z - trim_edges, Y - trim_edges, X - trim_edges) - ) + R, C, Ch, Z, Y, X = rcCzyx_data.shape + + # Extent + dZ, dY, dX = Z * voxel_size[0], Y * voxel_size[1], X * voxel_size[2] + + # if fourier_space: + # axis_extent = [1 / x for x in axis_extent] - R, C, Z, Y, X = rczyx_data.shape # after cropping + # rczyx_data = nd_fourier_central_cuboid( + # rczyx_data, (R, C, Z - trim_edges, Y - trim_edges, X - trim_edges) + # ) - rczyx_data = torch.fft.ifftshift(rczyx_data, dim=(-3, -2, -1)) - sfZYX_rgb = complex_tensor_to_rgb(rczyx_data, saturate_clim_fraction) + # R, C, Z, Y, X = rczyx_data.shape # after cropping + + # rczyx_data = torch.fft.ifftshift(rczyx_data, dim=(-3, -2, -1)) + # sfZYX_rgb = complex_tensor_to_rgb(rczyx_data, saturate_clim_fraction) assert R == len(row_labels) assert C == len(column_labels) + assert zyx_slice[0] < Z and zyx_slice[1] < Y and zyx_slice[2] < X + assert zyx_slice[0] >= 0 and zyx_slice[1] >= 0 and zyx_slice[2] >= 0 + + n_rows = 1 + (2 * R) + n_cols = 1 + (2 * C) - n_rows = (R * 2) + 1 - n_cols = (C * 2) + 1 + width_ratios = [label_size] + C * [1, dZ / dX] + height_ratios = [label_size] + R * [dY / dX, dZ / dX] + + fig_width = np.array(width_ratios).sum() * inches_per_column + fig_height = np.array(height_ratios).sum() * inches_per_column fig, axes = plt.subplots( n_rows, n_cols, - figsize=(n_cols, n_rows), + figsize=(fig_width, fig_height), gridspec_kw={ - "wspace": 0.1, # Adjust this value to reduce space between columns - "hspace": 0.1, # Adjust this value to reduce space between rows - "width_ratios": [1] - + C - * [ - 1, - Z / X, - ], - "height_ratios": [1] - + R - * [ - 1, - Z / Y, - ], + "wspace": 0.05, + "hspace": 0.05, + "width_ratios": width_ratios, + "height_ratios": height_ratios, }, ) - rose_path = os.path.join( - os.path.dirname(__file__), - f"./assets/rose.png", - ) - axes[0, 0].imshow(plt.imread(rose_path)) + if rose_path is not None: + axes[0, 0].imshow(plt.imread(rose_path)) for i in range(n_rows): for j in range(n_cols): + # Add labels if (i == 0 and (j - 1) % 2 == 0) or (j == 0 and (i - 1) % 2 == 0): if i == 0: folder = "gellman" @@ -98,42 +94,41 @@ def plot_5d_ortho( index, horizontalalignment="center", verticalalignment="center", - fontsize=10, + fontsize=10 * label_size, color="black", ) + # Add data if i > 0 and j > 0: - # XY + Cyx_data = rcCzyx_data[ + int((i - 1) / 2), int((j - 1) / 2), :, zyx_slice[0] + ] + Cyz_data = rcCzyx_data[ + int((i - 1) / 2), int((j - 1) / 2), :, :, :, zyx_slice[2] + ].transpose(0, 2, 1) + Czx_data = rcCzyx_data[ + int((i - 1) / 2), int((j - 1) / 2), :, :, zyx_slice[1] + ] + + # YX if (i - 1) % 2 == 0 and (j - 1) % 2 == 0: axes[i, j].imshow( - sfZYX_rgb[ - int((i - 1) / 2), - int((j - 1) / 2), - (Z // 2) + z_slice, - ], - aspect=axis_extent[1] / axis_extent[2], + color_func(*Cyx_data, **kwargs), + aspect=voxel_size[1] / voxel_size[2], ) # YZ elif (i - 1) % 2 == 0 and (j - 1) % 2 == 1: axes[i, j].imshow( - sfZYX_rgb[ - int((i - 1) / 2), int((j - 1) / 2), :, :, X // 2, : - ].transpose(1, 0, 2), - aspect=axis_extent[1] / axis_extent[0], + color_func(*Cyz_data, **kwargs), + aspect=voxel_size[1] / voxel_size[0], ) # XZ elif (i - 1) % 2 == 1 and (j - 1) % 2 == 0: axes[i, j].imshow( - sfZYX_rgb[ - int((i - 1) / 2), int((j - 1) / 2), :, Y // 2 - ], - aspect=axis_extent[0] / axis_extent[1], + color_func(*Czx_data, **kwargs), + aspect=voxel_size[0] / voxel_size[2], ) - axes[i, j].tick_params( - left=False, bottom=False, labelleft=False, labelbottom=False - ) - # Draw lines between rows and cols top = axes[0, 0].get_position().y1 bottom = axes[-1, -1].get_position().y0 @@ -150,7 +145,7 @@ def plot_5d_ortho( [bottom, top], transform=fig.transFigure, color="black", - lw=0.5, + lw=row_column_line_width, ) ) if j == 0 and (i - 1) % 2 == 0: @@ -164,173 +159,129 @@ def plot_5d_ortho( [top_edge, top_edge], transform=fig.transFigure, color="black", - lw=0.5, + lw=row_column_line_width, ) ) + # Remove ticks and spines + axes[i, j].tick_params( + left=False, bottom=False, labelleft=False, labelbottom=False + ) axes[i, j].spines["top"].set_visible(False) axes[i, j].spines["right"].set_visible(False) axes[i, j].spines["bottom"].set_visible(False) axes[i, j].spines["left"].set_visible(False) - # Draw ortho view lines - fig.add_artist( - plt.Line2D( - [Y // 2, Y // 2], - [0, X], - transform=axes[1, 1].transData, # use axis coordinates - color="red", - lw=0.5, - ) - ) - fig.add_artist( - plt.Line2D( - [0, X], - [Y // 2, Y // 2], # from bottom to top in axis coordinates - transform=axes[1, 1].transData, # use axis coordinates - color="blue", - lw=0.5, - ) - ) - fig.add_artist( - plt.Rectangle( - (0, 0), # lower-left corner - X, # width - Y, # height - linewidth=0.5, - edgecolor="green", - facecolor="none", - transform=axes[1, 1].transData, # use axis coordinates - ) - ) - axes[1, 1].text( - 0.1, - 0.975, - "x", - horizontalalignment="left", - verticalalignment="top", - transform=axes[1, 1].transAxes, - fontsize=5, - color="black", - ) - axes[1, 1].text( - 0.025, - 0.9, - "y", - horizontalalignment="left", - verticalalignment="top", - transform=axes[1, 1].transAxes, - fontsize=5, - color="black", - ) + yx_slice_color = "green" + yz_slice_color = "red" + zx_slice_color = "blue" - fig.add_artist( - plt.Line2D( - [ - Z // 2 + z_slice, - Z // 2 + z_slice, - ], - [0, Y], - transform=axes[1, 2].transData, # use axis coordinates - color="green", - lw=0.5, - ) - ) - fig.add_artist( - plt.Line2D( - [0, Z], # from bottom to top in axis coordinates - [Y // 2, Y // 2], - transform=axes[1, 2].transData, # use axis coordinates - color="blue", - lw=0.5, - ) - ) + # Label orthogonal slices + add_ortho_lines_to_axis( + axes[1, 1], + (zyx_slice[1], zyx_slice[2]), + ("y", "x"), + yx_slice_color, + yz_slice_color, + zx_slice_color, + ortho_line_width, + ) # YX axis - rect = plt.Rectangle( - (0, 0), # lower-left corner - Z, # width - Y, # height - linewidth=0.5, - edgecolor="red", - facecolor="none", - transform=axes[1, 2].transData, # use axis coordinates - ) - fig.add_artist(rect) + add_ortho_lines_to_axis( + axes[2, 1], + (zyx_slice[0], zyx_slice[2]), + ("z", "x"), + zx_slice_color, + yz_slice_color, + yx_slice_color, + ortho_line_width, + ) # ZX axis + + add_ortho_lines_to_axis( + axes[1, 2], + (zyx_slice[1], zyx_slice[0]), + ("y", "z"), + yz_slice_color, + yx_slice_color, + zx_slice_color, + ortho_line_width, + ) # YZ axis - axes[1, 2].text( - 0.1, - 0.975, - "z", + print(f"Saving {filename}") + fig.savefig(filename, dpi=400, format="pdf", bbox_inches="tight") + + +def add_ortho_lines_to_axis( + axis, + yx_slice, + axis_labels, + outer_color, + vertical_color, + horizontal_color, + line_width=0, + text_color="white", +): + + xmin, xmax = axis.get_xlim() + ymin, ymax = axis.get_ylim() + + # Axis labels + horizontal_axis_label_pos = (0.1, 0.975) + vertical_axis_label_pos = (0.025, 0.9) + axis.text( + horizontal_axis_label_pos[0], + horizontal_axis_label_pos[1], + axis_labels[1], horizontalalignment="left", verticalalignment="top", - transform=axes[1, 2].transAxes, + transform=axis.transAxes, fontsize=5, - color="black", + color=text_color, ) - axes[1, 2].text( - 0.025, - 0.9, - "y", + + axis.text( + vertical_axis_label_pos[0], + vertical_axis_label_pos[1], + axis_labels[0], horizontalalignment="left", verticalalignment="top", - transform=axes[1, 2].transAxes, + transform=axis.transAxes, fontsize=5, - color="black", + color=text_color, ) - fig.add_artist( - plt.Line2D( - [0, X], - [ - Z // 2 + z_slice, - Z // 2 + z_slice, - ], # from bottom to top in axis coordinates - transform=axes[2, 1].transData, # use axis coordinates - color="green", - lw=0.5, + # Outer rectangle + axis.add_artist( + plt.Rectangle( + (xmin, ymin), + xmax - xmin, + ymax - ymin, + linewidth=line_width, + edgecolor=outer_color, + facecolor="none", + transform=axis.transData, + clip_on=False, ) ) - fig.add_artist( + + # Horizontal line + axis.add_artist( plt.Line2D( - [X // 2, X // 2], - [0, Z], # from bottom to top in axis coordinates - transform=axes[2, 1].transData, # use axis coordinates - color="red", - lw=0.5, + [xmin, xmax], + [yx_slice[0], yx_slice[0]], + transform=axis.transData, + color=horizontal_color, + lw=line_width, ) ) - rect = plt.Rectangle( - (0, 0), # lower-left corner - X, # width - Z, # height - linewidth=0.5, - edgecolor="blue", - facecolor="none", - transform=axes[2, 1].transData, # use axis coordinates - ) - fig.add_artist(rect) - - axes[2, 1].text( - 0.1, - 0.975, - "x", - horizontalalignment="left", - verticalalignment="top", - transform=axes[2, 1].transAxes, - fontsize=5, - color="black", - ) - axes[2, 1].text( - 0.025, - 0.9, - "z", - horizontalalignment="left", - verticalalignment="top", - transform=axes[2, 1].transAxes, - fontsize=5, - color="black", + # Vertical line + axis.add_artist( + plt.Line2D( + [yx_slice[1], yx_slice[1]], + [ymin, ymax], + transform=axis.transData, + color=vertical_color, + lw=line_width, + ) ) - - print(f"Saving {filename}") - fig.savefig(filename, dpi=300, format="pdf", bbox_inches="tight") diff --git a/waveorder/visuals/utils.py b/waveorder/visuals/utils.py index c612ddf..fc564ea 100644 --- a/waveorder/visuals/utils.py +++ b/waveorder/visuals/utils.py @@ -4,13 +4,11 @@ # Main function to convert a complex-valued torch tensor to RGB numpy array # with red at +1, green at +i, blue at -1, and purple at -i -def complex_tensor_to_rgb(tensor, saturate_clim_fraction=1.0): - # Convert the torch tensor to a numpy array - tensor_np = tensor.numpy() +def complex_tensor_to_rgb(array, saturate_clim_fraction=1.0): # Calculate magnitude and phase for the entire array - magnitude = np.abs(tensor_np) - phase = np.angle(tensor_np) + magnitude = np.abs(array) + phase = np.angle(array) # Normalize phase to [0, 1] hue = (phase + np.pi) / (2 * np.pi) From 9b3fcc489814c02b7efc5450f2c92d8c7d550148 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Fri, 8 Nov 2024 09:52:13 -0800 Subject: [PATCH 71/72] visual cleanup --- waveorder/visuals/matplotlib_visuals.py | 11 ++++++++++- waveorder/visuals/napari_visuals.py | 2 +- waveorder/visuals/utils.py | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/waveorder/visuals/matplotlib_visuals.py b/waveorder/visuals/matplotlib_visuals.py index b15f85e..1b438ed 100644 --- a/waveorder/visuals/matplotlib_visuals.py +++ b/waveorder/visuals/matplotlib_visuals.py @@ -13,7 +13,7 @@ def plot_5d_ortho( filename, voxel_size, zyx_slice, - color_func, + color_funcs, row_labels=None, column_labels=None, rose_path=None, @@ -45,6 +45,13 @@ def plot_5d_ortho( assert zyx_slice[0] < Z and zyx_slice[1] < Y and zyx_slice[2] < X assert zyx_slice[0] >= 0 and zyx_slice[1] >= 0 and zyx_slice[2] >= 0 + assert R == len(color_funcs) + for color_func_row in color_funcs: + if isinstance(color_func_row, list): + assert len(color_func_row) == C + else: + color_func_row = [color_func_row] * C + n_rows = 1 + (2 * R) n_cols = 1 + (2 * C) @@ -100,6 +107,8 @@ def plot_5d_ortho( # Add data if i > 0 and j > 0: + color_func = color_funcs[int((i - 1) / 2)][int((j - 1) / 2)] + Cyx_data = rcCzyx_data[ int((i - 1) / 2), int((j - 1) / 2), :, zyx_slice[0] ] diff --git a/waveorder/visuals/napari_visuals.py b/waveorder/visuals/napari_visuals.py index 0fbef9d..157063d 100644 --- a/waveorder/visuals/napari_visuals.py +++ b/waveorder/visuals/napari_visuals.py @@ -25,7 +25,7 @@ def add_transfer_function_to_viewer( if complex_rgb: rgb_transfer_function = complex_tensor_to_rgb( - torch.fft.ifftshift(transfer_function, dim=shift_dims), + np.array(torch.fft.ifftshift(transfer_function, dim=shift_dims)), saturate_clim_fraction=clim_factor, ) viewer.add_image( diff --git a/waveorder/visuals/utils.py b/waveorder/visuals/utils.py index fc564ea..dbde006 100644 --- a/waveorder/visuals/utils.py +++ b/waveorder/visuals/utils.py @@ -15,7 +15,10 @@ def complex_tensor_to_rgb(array, saturate_clim_fraction=1.0): hue = np.mod(hue + 0.5, 1) # Normalize magnitude to [0, 1] for saturation - max_abs_val = np.amax(magnitude) * saturate_clim_fraction + if saturate_clim_fraction is not None: + max_abs_val = np.amax(magnitude) * saturate_clim_fraction + else: + max_abs_val = 1.0 sat = magnitude / max_abs_val if max_abs_val != 0 else magnitude From 75be3e53bd8abee1a543f5c055a5434bb6463404 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Fri, 8 Nov 2024 09:53:08 -0800 Subject: [PATCH 72/72] manage large reconstructions --- .../inplane_oriented_thick_pol3d_vector.py | 36 +++++++---- .../inplane_oriented_thick_pol3d_vector.py | 60 ++++++++++++++++--- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/examples/models/inplane_oriented_thick_pol3d_vector.py b/examples/models/inplane_oriented_thick_pol3d_vector.py index 72f01d4..c173632 100644 --- a/examples/models/inplane_oriented_thick_pol3d_vector.py +++ b/examples/models/inplane_oriented_thick_pol3d_vector.py @@ -66,19 +66,19 @@ intensity_to_stokes_matrix, ) -# Reconstruct -fzyx_object_recon = ( - inplane_oriented_thick_pol3d_vector.apply_inverse_transfer_function( - szyx_data, - singular_system, - intensity_to_stokes_matrix, - regularization_strength=1e-1, - ) -) +# from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer + +# add_transfer_function_to_viewer( +# viewer, +# singular_system[1], +# zyx_scale=(z_pixel_size, yx_pixel_size, yx_pixel_size), +# layer_name="Singular Values", +# ) +# import pdb; pdb.set_trace() + # Display arrays = [ - (fzyx_object_recon, "Object - recon"), (szyx_data, "Data"), (fzyx_object, "Object"), ] @@ -86,6 +86,22 @@ for array in arrays: viewer.add_image(torch.real(array[0]).cpu().numpy(), name=array[1]) + +# Reconstruct +for reg_strength in [0.005, 0.008, 0.01, 0.05, 0.1]: + fzyx_object_recon = ( + inplane_oriented_thick_pol3d_vector.apply_inverse_transfer_function( + szyx_data, + singular_system, + intensity_to_stokes_matrix, + regularization_strength=reg_strength, + ) + ) + viewer.add_image( + torch.real(fzyx_object_recon).cpu().numpy(), + name=f"Object - recon, reg_strength={reg_strength}", + ) + viewer.grid.enabled = True viewer.grid.shape = (2, 5) import pdb diff --git a/waveorder/models/inplane_oriented_thick_pol3d_vector.py b/waveorder/models/inplane_oriented_thick_pol3d_vector.py index 1df364a..94e9938 100644 --- a/waveorder/models/inplane_oriented_thick_pol3d_vector.py +++ b/waveorder/models/inplane_oriented_thick_pol3d_vector.py @@ -1,9 +1,10 @@ import torch +import tqdm import numpy as np from torch import Tensor from typing import Literal -from torch.nn.functional import avg_pool3d +from torch.nn.functional import avg_pool3d, interpolate from waveorder import optics, sampling, stokes, util from waveorder.visuals.napari_visuals import add_transfer_function_to_viewer @@ -40,6 +41,7 @@ def calculate_transfer_function( numerical_aperture_detection, invert_phase_contrast=False, fourier_oversample_factor=1, + transverse_downsample_factor=1, ): if z_padding != 0: raise NotImplementedError("Padding not implemented for this model") @@ -58,15 +60,34 @@ def calculate_transfer_function( yx_factor = int(np.ceil(yx_pixel_size / transverse_nyquist)) z_factor = int(np.ceil(z_pixel_size / axial_nyquist)) + print("YX factor:", yx_factor) + print("Z factor:", z_factor) + + tf_calculation_shape = ( + zyx_shape[0] * z_factor * fourier_oversample_factor, + int( + np.ceil( + zyx_shape[1] + * yx_factor + * fourier_oversample_factor + / transverse_downsample_factor + ) + ), + int( + np.ceil( + zyx_shape[2] + * yx_factor + * fourier_oversample_factor + / transverse_downsample_factor + ) + ), + ) + sfZYX_transfer_function, intensity_to_stokes_matrix = ( _calculate_wrap_unsafe_transfer_function( swing, scheme, - ( - zyx_shape[0] * z_factor * fourier_oversample_factor, - zyx_shape[1] * yx_factor * fourier_oversample_factor, - zyx_shape[2] * yx_factor * fourier_oversample_factor, - ), + tf_calculation_shape, yx_pixel_size / yx_factor, z_pixel_size / z_factor, wavelength_illumination, @@ -96,11 +117,29 @@ def calculate_transfer_function( pooled_sfZYX_transfer_function.shape[1], zyx_shape[0] + 2 * z_padding, ) + zyx_shape[1:] + + cropped = sampling.nd_fourier_central_cuboid( + pooled_sfZYX_transfer_function, sfzyx_out_shape + ) + + # Compute singular system on cropped and downsampled + U, S, Vh = calculate_singular_system(cropped) + + # Interpolate to final size in YX + def complex_interpolate(tensor, zyx_shape): + interpolated_real = interpolate(tensor.real, size=zyx_shape) + interpolated_imag = interpolate(tensor.imag, size=zyx_shape) + return interpolated_real + 1j * interpolated_imag + + full_cropped = complex_interpolate(cropped, zyx_shape) + full_U = complex_interpolate(U, zyx_shape) + full_S = interpolate(S[None], size=zyx_shape)[0] # S is real + full_Vh = complex_interpolate(Vh, zyx_shape) + return ( - sampling.nd_fourier_central_cuboid( - pooled_sfZYX_transfer_function, sfzyx_out_shape - ), + full_cropped, intensity_to_stokes_matrix, + (full_U, full_S, full_Vh), ) @@ -142,6 +181,7 @@ def _calculate_wrap_unsafe_transfer_function( z_frequencies = torch.fft.fftfreq(z_total, d=z_pixel_size) # 2D pupils + print("\tCalculating pupils...") ill_pupil = optics.generate_pupil( radial_frequencies, numerical_aperture_illumination, @@ -187,6 +227,8 @@ def _calculate_wrap_unsafe_transfer_function( P_3D = torch.abs(torch.fft.ifft(P, dim=-3)).type(torch.complex64) S_3D = torch.fft.ifft(S, dim=-3) + + print("\tCalculating greens tensor spectrum...") G_3D = optics.generate_greens_tensor_spectrum( zyx_shape=(z_total, zyx_shape[1], zyx_shape[2]), zyx_pixel_size=(z_pixel_size, yx_pixel_size, yx_pixel_size),