Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.

Commit f10d58b

Browse files
authored
feat: Add option for second laser (#7)
1 parent 57de7e3 commit f10d58b

2 files changed

Lines changed: 83 additions & 13 deletions

File tree

src/torch_ctf/ctf_lpp.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def calc_LPP_ctf_2D(
454454
laser_trans_offset_angstrom: float,
455455
laser_polarization_angle_deg: float,
456456
peak_phase_deg: float,
457+
dual_laser: bool = False,
457458
beam_tilt_mrad: torch.Tensor | None = None,
458459
even_zernike_coeffs: dict | None = None,
459460
odd_zernike_coeffs: dict | None = None,
@@ -508,6 +509,10 @@ def calc_LPP_ctf_2D(
508509
Polarization angle of the laser in degrees.
509510
peak_phase_deg : float
510511
Desired peak phase in degrees.
512+
dual_laser : bool, optional
513+
If True, add a second laser with the same parameters but rotated 90° in the
514+
xy plane (perpendicular to the first). The two phase contributions are summed.
515+
Default is False.
511516
beam_tilt_mrad : torch.Tensor | None
512517
Beam tilt in milliradians. [bx, by] in mrad
513518
even_zernike_coeffs : dict | None
@@ -580,19 +585,48 @@ def calc_LPP_ctf_2D(
580585
)
581586

582587
# Calculate laser phase using the dedicated function
583-
laser_phase_radians = calc_LPP_phase(
584-
fft_freq_grid=fft_freq_grid,
585-
NA=NA,
586-
laser_wavelength_angstrom=laser_wavelength_angstrom,
587-
focal_length_angstrom=focal_length_angstrom,
588-
laser_xy_angle_deg=laser_xy_angle_deg,
589-
laser_xz_angle_deg=laser_xz_angle_deg,
590-
laser_long_offset_angstrom=laser_long_offset_angstrom,
591-
laser_trans_offset_angstrom=laser_trans_offset_angstrom,
592-
laser_polarization_angle_deg=laser_polarization_angle_deg,
593-
peak_phase_deg=peak_phase_deg,
594-
voltage=voltage,
595-
)
588+
if dual_laser:
589+
phase1 = calc_LPP_phase(
590+
fft_freq_grid=fft_freq_grid,
591+
NA=NA,
592+
laser_wavelength_angstrom=laser_wavelength_angstrom,
593+
focal_length_angstrom=focal_length_angstrom,
594+
laser_xy_angle_deg=laser_xy_angle_deg,
595+
laser_xz_angle_deg=laser_xz_angle_deg,
596+
laser_long_offset_angstrom=laser_long_offset_angstrom,
597+
laser_trans_offset_angstrom=laser_trans_offset_angstrom,
598+
laser_polarization_angle_deg=laser_polarization_angle_deg,
599+
peak_phase_deg=peak_phase_deg,
600+
voltage=voltage,
601+
)
602+
phase2 = calc_LPP_phase(
603+
fft_freq_grid=fft_freq_grid,
604+
NA=NA,
605+
laser_wavelength_angstrom=laser_wavelength_angstrom,
606+
focal_length_angstrom=focal_length_angstrom,
607+
laser_xy_angle_deg=laser_xy_angle_deg + 90,
608+
laser_xz_angle_deg=laser_xz_angle_deg,
609+
laser_long_offset_angstrom=laser_long_offset_angstrom,
610+
laser_trans_offset_angstrom=laser_trans_offset_angstrom,
611+
laser_polarization_angle_deg=laser_polarization_angle_deg,
612+
peak_phase_deg=peak_phase_deg,
613+
voltage=voltage,
614+
)
615+
laser_phase_radians = phase1 + phase2
616+
else:
617+
laser_phase_radians = calc_LPP_phase(
618+
fft_freq_grid=fft_freq_grid,
619+
NA=NA,
620+
laser_wavelength_angstrom=laser_wavelength_angstrom,
621+
focal_length_angstrom=focal_length_angstrom,
622+
laser_xy_angle_deg=laser_xy_angle_deg,
623+
laser_xz_angle_deg=laser_xz_angle_deg,
624+
laser_long_offset_angstrom=laser_long_offset_angstrom,
625+
laser_trans_offset_angstrom=laser_trans_offset_angstrom,
626+
laser_polarization_angle_deg=laser_polarization_angle_deg,
627+
peak_phase_deg=peak_phase_deg,
628+
voltage=voltage,
629+
)
596630

597631
# Convert laser phase from radians to degrees for compatibility
598632
laser_phase_degrees = torch.rad2deg(laser_phase_radians)

tests/test_torch_ctf.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,42 @@ def test_calc_LPP_ctf_2D():
15871587
assert not torch.is_complex(result)
15881588

15891589

1590+
def test_calc_LPP_ctf_2D_dual_laser():
1591+
"""Test LPP CTF with dual perpendicular laser option."""
1592+
common = {
1593+
"defocus": 1.5,
1594+
"astigmatism": 0,
1595+
"astigmatism_angle": 0,
1596+
"voltage": 300,
1597+
"spherical_aberration": 2.7,
1598+
"amplitude_contrast": 0.1,
1599+
"pixel_size": 8,
1600+
"image_shape": (10, 10),
1601+
"rfft": False,
1602+
"fftshift": False,
1603+
"NA": 0.1,
1604+
"laser_wavelength_angstrom": 5000.0,
1605+
"focal_length_angstrom": 1e6,
1606+
"laser_xy_angle_deg": 0.0,
1607+
"laser_xz_angle_deg": 0.0,
1608+
"laser_long_offset_angstrom": 0.0,
1609+
"laser_trans_offset_angstrom": 0.0,
1610+
"laser_polarization_angle_deg": 0.0,
1611+
"peak_phase_deg": 90.0,
1612+
}
1613+
result_single = calc_LPP_ctf_2D(**common, dual_laser=False)
1614+
result_dual = calc_LPP_ctf_2D(**common, dual_laser=True)
1615+
assert result_single.shape == (10, 10)
1616+
assert result_dual.shape == (10, 10)
1617+
assert torch.all(torch.isfinite(result_single))
1618+
assert torch.all(torch.isfinite(result_dual))
1619+
assert not torch.is_complex(result_single)
1620+
assert not torch.is_complex(result_dual)
1621+
assert not torch.allclose(result_single, result_dual), (
1622+
"dual_laser=True should differ from dual_laser=False"
1623+
)
1624+
1625+
15901626
def test_calc_LPP_ctf_2D_with_zernikes():
15911627
"""Test LPP CTF with Zernike coefficients."""
15921628
with pytest.warns(RuntimeWarning, match="Both beam tilt and Zernike"):

0 commit comments

Comments
 (0)