Skip to content

Commit 3ecd7f5

Browse files
committed
feat: Add option for LPP CTF
1 parent b8de654 commit 3ecd7f5

2 files changed

Lines changed: 110 additions & 27 deletions

File tree

src/leopard_em/pydantic_models/data_structures/optics_group.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,48 @@
99
from leopard_em.pydantic_models.custom_types import BaseModel2DTM
1010

1111

12+
class LaserParams(BaseModel2DTM):
13+
"""Laser phase plate parameters for optics groups using a laser phase plate.
14+
15+
Default enabled is False (omit or set laser_params to null when not used).
16+
Include this block only when the optics group uses a laser phase plate.
17+
18+
Attributes
19+
----------
20+
NA : float
21+
Numerical aperture.
22+
laser_wavelength_angstrom : float
23+
Laser wavelength in Angstrom.
24+
focal_length_angstrom : float
25+
Focal length in Angstrom.
26+
laser_xy_angle_deg : float
27+
Laser angle in the XY plane in degrees.
28+
laser_xz_angle_deg : float
29+
Laser angle in the XZ plane in degrees.
30+
laser_long_offset_angstrom : float
31+
Longitudinal offset in Angstrom.
32+
laser_trans_offset_angstrom : float
33+
Transverse offset in Angstrom.
34+
laser_polarization_angle_deg : float
35+
Laser polarization angle in degrees.
36+
peak_phase_deg : float
37+
Peak phase in degrees.
38+
dual_laser : bool
39+
Whether a dual-laser setup is used. Default is False.
40+
"""
41+
42+
NA: float = 0.055
43+
laser_wavelength_angstrom: float = 10640
44+
focal_length_angstrom: float = 7.1e7
45+
laser_xy_angle_deg: float = 0.0
46+
laser_xz_angle_deg: float = 0.0
47+
laser_long_offset_angstrom: float = 0.0
48+
laser_trans_offset_angstrom: float = 0.0
49+
laser_polarization_angle_deg: float = 90.0
50+
peak_phase_deg: float = 45.0
51+
dual_laser: bool = True
52+
53+
1254
class OpticsGroup(BaseModel2DTM):
1355
"""Stores optics group parameters for the imaging system on a microscope.
1456
@@ -62,6 +104,10 @@ class OpticsGroup(BaseModel2DTM):
62104
Optional dict of even Zernike moments. Possible keys: "Z44c", "Z44s", "Z60".
63105
mag_matrix : Optional[list[float]]
64106
Optional list of floats of length 4 representing the magnification matrix.
107+
laser_params : Optional[LaserParams]
108+
Optional laser phase plate parameters. Omit or set to null when not using
109+
a laser phase plate. When present, the optics group uses a laser phase
110+
plate with the given parameters.
65111
66112
Methods
67113
-------
@@ -93,6 +139,7 @@ class OpticsGroup(BaseModel2DTM):
93139
mag_matrix: Optional[Annotated[list[float], Field(min_length=4, max_length=4)]] = (
94140
None
95141
)
142+
laser_params: Optional[LaserParams] = None
96143

97144
@property
98145
def mag_matrix_tensor(self) -> Optional[torch.Tensor]:

src/leopard_em/utils/ctf_utils.py

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import numpy as np
77
import torch
8-
from torch_ctf import calculate_ctf_2d
8+
from torch_ctf import calc_LPP_ctf_2D, calculate_ctf_2d
99
from torch_fourier_filter.envelopes import b_envelope
1010

1111
from leopard_em.utils.search_utils import get_cs_range
@@ -78,25 +78,56 @@ def calculate_ctf_filter_stack_full_args(
7878
# If it's neither a list nor a tensor, try to convert it
7979
mag_matrix = torch.tensor(mag_matrix, dtype=torch.float32)
8080

81+
# When laser phase plate params are provided, use LPP CTF; otherwise standard CTF
82+
laser_params = kwargs.pop("laser_params", None)
83+
8184
# Loop over spherical aberrations one at a time and collect results
8285
ctf_list = []
8386
for cs_val in cs_values:
84-
tmp = calculate_ctf_2d(
85-
defocus=defocus * 1e-4, # Convert to um from Angstrom
86-
astigmatism=astigmatism * 1e-4, # Convert to um from Angstrom
87-
astigmatism_angle=kwargs["astigmatism_angle"],
88-
voltage=kwargs["voltage"],
89-
spherical_aberration=cs_val,
90-
amplitude_contrast=kwargs["amplitude_contrast_ratio"],
91-
phase_shift=kwargs["phase_shift"],
92-
pixel_size=kwargs["pixel_size"],
93-
image_shape=template_shape,
94-
rfft=True,
95-
fftshift=False,
96-
even_zernike_coeffs=kwargs["even_zernikes"],
97-
odd_zernike_coeffs=kwargs["odd_zernikes"],
98-
transform_matrix=mag_matrix,
99-
)
87+
if laser_params is not None:
88+
tmp = calc_LPP_ctf_2D(
89+
defocus=defocus * 1e-4, # Convert to um from Angstrom
90+
astigmatism=astigmatism * 1e-4, # Convert to um from Angstrom
91+
astigmatism_angle=kwargs["astigmatism_angle"],
92+
voltage=kwargs["voltage"],
93+
spherical_aberration=cs_val,
94+
amplitude_contrast=kwargs["amplitude_contrast_ratio"],
95+
pixel_size=kwargs["pixel_size"],
96+
image_shape=template_shape,
97+
rfft=True,
98+
fftshift=False,
99+
NA=laser_params.NA,
100+
laser_wavelength_angstrom=laser_params.laser_wavelength_angstrom,
101+
focal_length_angstrom=laser_params.focal_length_angstrom,
102+
laser_xy_angle_deg=laser_params.laser_xy_angle_deg,
103+
laser_xz_angle_deg=laser_params.laser_xz_angle_deg,
104+
laser_long_offset_angstrom=laser_params.laser_long_offset_angstrom,
105+
laser_trans_offset_angstrom=laser_params.laser_trans_offset_angstrom,
106+
laser_polarization_angle_deg=laser_params.laser_polarization_angle_deg,
107+
peak_phase_deg=laser_params.peak_phase_deg,
108+
dual_laser=laser_params.dual_laser,
109+
beam_tilt_mrad=None,
110+
even_zernike_coeffs=kwargs.get("even_zernikes"),
111+
odd_zernike_coeffs=kwargs.get("odd_zernikes"),
112+
transform_matrix=mag_matrix,
113+
)
114+
else:
115+
tmp = calculate_ctf_2d(
116+
defocus=defocus * 1e-4, # Convert to um from Angstrom
117+
astigmatism=astigmatism * 1e-4, # Convert to um from Angstrom
118+
astigmatism_angle=kwargs["astigmatism_angle"],
119+
voltage=kwargs["voltage"],
120+
spherical_aberration=cs_val,
121+
amplitude_contrast=kwargs["amplitude_contrast_ratio"],
122+
phase_shift=kwargs["phase_shift"],
123+
pixel_size=kwargs["pixel_size"],
124+
image_shape=template_shape,
125+
rfft=True,
126+
fftshift=False,
127+
even_zernike_coeffs=kwargs["even_zernikes"],
128+
odd_zernike_coeffs=kwargs["odd_zernikes"],
129+
transform_matrix=mag_matrix,
130+
)
100131
# calc B-envelope and apply
101132
b_envelope_tmp = b_envelope(
102133
B=kwargs["ctf_B_factor"],
@@ -139,22 +170,27 @@ def calculate_ctf_filter_stack(
139170
Tensor of CTF filter values for the specified shape and optics group. Will have
140171
shape (num_pixel_sizes, num_defocus_offsets, h, w // 2 + 1)
141172
"""
173+
kwargs: dict[str, Any] = {
174+
"astigmatism_angle": optics_group.astigmatism_angle,
175+
"voltage": optics_group.voltage,
176+
"spherical_aberration": optics_group.spherical_aberration,
177+
"amplitude_contrast_ratio": optics_group.amplitude_contrast_ratio,
178+
"ctf_B_factor": optics_group.ctf_B_factor,
179+
"phase_shift": optics_group.phase_shift,
180+
"pixel_size": optics_group.pixel_size,
181+
"even_zernikes": optics_group.even_zernikes,
182+
"odd_zernikes": optics_group.odd_zernikes,
183+
"mag_matrix": optics_group.mag_matrix_tensor,
184+
}
185+
if optics_group.laser_params is not None:
186+
kwargs["laser_params"] = optics_group.laser_params
142187
return calculate_ctf_filter_stack_full_args(
143188
template_shape,
144189
optics_group.defocus_u,
145190
optics_group.defocus_v,
146191
defocus_offsets,
147192
pixel_size_offsets,
148-
astigmatism_angle=optics_group.astigmatism_angle,
149-
voltage=optics_group.voltage,
150-
spherical_aberration=optics_group.spherical_aberration,
151-
amplitude_contrast_ratio=optics_group.amplitude_contrast_ratio,
152-
ctf_B_factor=optics_group.ctf_B_factor,
153-
phase_shift=optics_group.phase_shift,
154-
pixel_size=optics_group.pixel_size,
155-
even_zernikes=optics_group.even_zernikes,
156-
odd_zernikes=optics_group.odd_zernikes,
157-
mag_matrix=optics_group.mag_matrix_tensor,
193+
**kwargs,
158194
)
159195

160196

0 commit comments

Comments
 (0)