diff --git a/docs/source/conf.py b/docs/source/conf.py index 7b7e635..9137e53 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -130,7 +130,7 @@ # Hide some classes that are not production ready yet -def skip(app, what, name, obj, do_skip, options): +def skip(_app, _what, name, _obj, do_skip, _options): if name in ("WFSController", "Gain"): return True return do_skip @@ -142,7 +142,7 @@ def visit_citation(self, node): self.add(f'') -def visit_label(self, node): +def visit_label(_self, _node): """Patch-in function for markdown builder to support citations.""" pass diff --git a/examples/micro_manager_microscope.py b/examples/micro_manager_microscope.py index 8db6edd..aecc93c 100644 --- a/examples/micro_manager_microscope.py +++ b/examples/micro_manager_microscope.py @@ -11,7 +11,7 @@ import astropy.units as u import numpy as np -from openwfs.simulation import Microscope, StaticSource +from openwfs.simulation import Microscope, StaticSource, Camera specimen_resolution = (1024, 1024) # height × width in pixels of the specimen image specimen_pixel_size = 60 * u.nm # resolution (pixel size) of the specimen image @@ -36,7 +36,8 @@ ) # simulate shot noise in an 8-bit camera with auto-exposure: -cam = mic.get_camera( +cam = Camera( + mic, shot_noise=True, digital_max=255, data_shape=camera_resolution, @@ -44,4 +45,4 @@ ) # construct dictionary of objects to expose to Micro-Manager -devices = {"camera": cam, "stage": mic.stage} +devices = {"camera": cam, "stage": mic.xy_stage} diff --git a/examples/sample_microscope.py b/examples/sample_microscope.py index 898604c..074e564 100644 --- a/examples/sample_microscope.py +++ b/examples/sample_microscope.py @@ -10,7 +10,7 @@ import set_path # noqa - needed for setting the module search path to find openwfs from openwfs.plot_utilities import grab_and_show, imshow -from openwfs.simulation import Microscope, StaticSource +from openwfs.simulation import Microscope, StaticSource, Camera specimen_resolution = (1024, 1024) # height × width in pixels of the specimen image specimen_pixel_size = 60 * u.nm # resolution (pixel size) of the specimen image @@ -36,7 +36,8 @@ ) # simulate shot noise in an 8-bit camera with auto-exposure: -cam = mic.get_camera( +cam = Camera( + mic, shot_noise=True, digital_max=255, data_shape=camera_resolution, diff --git a/examples/troubleshooter_demo.py b/examples/troubleshooter_demo.py index f836ccf..bf80e68 100644 --- a/examples/troubleshooter_demo.py +++ b/examples/troubleshooter_demo.py @@ -13,7 +13,7 @@ from openwfs.algorithms import StepwiseSequential, troubleshoot from openwfs.processors import SingleRoi -from openwfs.simulation import SLM, Microscope, Shutter +from openwfs.simulation import SLM, Microscope, Shutter, Camera from openwfs.utilities import set_pixel_size # === Define virtual devices for a WFS simulation === @@ -43,7 +43,7 @@ ) # Simulate a camera device with gaussian noise and shot noise -cam = sim.get_camera(analog_max=1e4, shot_noise=True, gaussian_noise_std=4.0) +cam = Camera(sim, analog_max=1e4, shot_noise=True, gaussian_noise_std=4.0) # Define feedback as circular region of interest in the center of the frame roi_detector = SingleRoi(cam, radius=0.1) diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index 19c788e..181e9da 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -197,6 +197,7 @@ def _compute_cobasis(self): if self.phase_patterns is None: raise "The phase_patterns must be set before computing the cobasis." + # TODO: simplify, integrate in calling function, fix warnings cobasis = [None, None] for side in range(2): p = np.prod(self._shape) # Number of SLM pixels diff --git a/openwfs/algorithms/troubleshoot.py b/openwfs/algorithms/troubleshoot.py index 4c8c5fc..10516e7 100644 --- a/openwfs/algorithms/troubleshoot.py +++ b/openwfs/algorithms/troubleshoot.py @@ -132,7 +132,8 @@ def pearson_correlation(a: np.ndarray, b: np.ndarray, noise_var: np.ndarray = 0. by subtracting the noise variance from the signal variance. Args: - a, b: Real valued arrays. + a: real-valued input array. + b: real-valued input array. noise_var: Variance of uncorrelated noise to compensate for. """ a_dev = a - a.mean() # Deviations from mean a @@ -396,7 +397,7 @@ class WFSTroubleshootResult: Attributes: fidelity_non_modulated: The estimated fidelity reduction factor due to the presence of non-modulated light. - phase_calibration_ratio: A ratio indicating the correctness of the SLM phase response. An incorrect phase + fidelity_phase_calibration: A ratio indicating the correctness of the SLM phase response. An incorrect phase response produces a value < 1. wfs_result (WFSResult): Object containing the analyzed result of running the WFS algorithm. feedback_before: Feedback from before running the WFS algorithm, with a flat wavefront. diff --git a/openwfs/devices/camera.py b/openwfs/devices/camera.py index bbc6c6b..66126f8 100644 --- a/openwfs/devices/camera.py +++ b/openwfs/devices/camera.py @@ -163,7 +163,7 @@ def duration(self) -> Quantity[u.ms]: """Returns the exposure time in milliseconds if software triggering is used. Returns ∞ if hardware triggering is used. TODO: implement hardware triggering.""" - return self.exposure_time.to(u.ms) + return self.exposure.to(u.ms) @property def exposure(self) -> u.Quantity[u.ms]: diff --git a/openwfs/simulation/microscope.py b/openwfs/simulation/microscope.py index 4b62746..49b3bf0 100644 --- a/openwfs/simulation/microscope.py +++ b/openwfs/simulation/microscope.py @@ -9,16 +9,8 @@ from ..core import Processor, Detector from ..plot_utilities import imshow # noqa - for debugging -from ..processors import TransformProcessor -from ..simulation.mockdevices import XYStage, Camera, StaticSource -from ..utilities import ( - project, - place, - Transform, - get_pixel_size, - patterns, - CoordinateType, -) +from ..simulation.mockdevices import XYStage, StaticSource +from ..utilities import project, place, Transform, get_pixel_size, patterns class Microscope(Processor): @@ -251,33 +243,3 @@ def pixel_size(self) -> Quantity: def data_shape(self): """Returns the shape of the image in the image plane""" return self._data_shape - - def get_camera( - self, - *, - transform: Optional[Transform] = None, - data_shape: Optional[tuple[int, int]] = None, - pixel_size: Optional[CoordinateType] = None, - **kwargs - ) -> Detector: - """ - Returns a simulated camera that observes the microscope image. - - The camera is a MockCamera object that simulates an AD-converter with optional noise. - shot noise and readout noise (see MockCamera for options). - In addition to the inputs accepted by the MockCamera constructor (data_shape, analog_max, shot_noise, etc.), - it is also possible to specify a transform, to mimic the (mis)alignment of the camera. - - Args: - transform (): - **kwargs (): - - Returns: - - """ - if transform is None and data_shape is None and pixel_size is None: - src = self - else: - src = TransformProcessor(self, data_shape=data_shape, pixel_size=pixel_size, transform=transform) - - return Camera(src, **kwargs) diff --git a/openwfs/simulation/mockdevices.py b/openwfs/simulation/mockdevices.py index 27a07bb..7e8aeaa 100644 --- a/openwfs/simulation/mockdevices.py +++ b/openwfs/simulation/mockdevices.py @@ -328,10 +328,6 @@ def data_shape(self, value): def exposure(self) -> Quantity[u.ms]: return self.duration - @exposure.setter - def exposure(self, value: Quantity[u.ms]): - self.duration = value.to(u.ms) - class XYStage(Actuator): """ diff --git a/openwfs/utilities/utilities.py b/openwfs/utilities/utilities.py index a81caf8..7959031 100644 --- a/openwfs/utilities/utilities.py +++ b/openwfs/utilities/utilities.py @@ -293,15 +293,20 @@ def project( The input image is scaled so that the pixel sizes match those of the output, and cropped/zero-padded so that the data shape matches that of the output. - Optionally, an additional transformation can be specified, e.g., to scale or translate the source image. - This transformation is specified as a 2x3 transformation matrix in homogeneous coordinates. + Optionally, an additional :class:`~Transform` can be specified, e.g., to scale or translate the source image. Args: - source (np.ndarray): input image. - Must have the pixel_size set (see set_pixel_size) - transform: transformation to appy to the source image before placing it in the output - out (np.ndarray): optional array where the output image is stored in. - If specified, `out_shape` is ignored. + source: input image. + source_extent: extent of the source image in some physical unit. + If not given (``None``), the extent metadata of the input image is used. + see :func:`~get_extent`. + transform: optional transformed (rotate, translate, etc.) + to appy to the source image before placing it in the output + out: optional array where the output image is stored in. + out_extent: extent of the output image in some physical unit. + If not given, the extent metadata of the out image is used. + out_shape: shape of the output image. + This value is ignored if `out` is specified. Returns: np.ndarray: the projected image (`out` if specified, otherwise a new array) diff --git a/tests/test_algorithms_troubleshoot.py b/tests/test_algorithms_troubleshoot.py index 515018d..130a20f 100644 --- a/tests/test_algorithms_troubleshoot.py +++ b/tests/test_algorithms_troubleshoot.py @@ -15,6 +15,7 @@ measure_modulated_light_dual_phase_stepping, ) from ..openwfs.processors import SingleRoi +from ..openwfs.simulation import Camera from ..openwfs.simulation import SimulatedWFS, StaticSource, SLM, Microscope @@ -214,7 +215,7 @@ def test_fidelity_phase_calibration_ssa_with_noise(n_y, n_x, phase_steps, gaussi aberrations=aberration, wavelength=800 * u.nm, ) - cam = sim.get_camera(analog_max=1e4, gaussian_noise_std=gaussian_noise_std) + cam = Camera(sim, analog_max=1e4, gaussian_noise_std=gaussian_noise_std) roi_detector = SingleRoi(cam, radius=0) # Only measure that specific point # Define and run WFS algorithm @@ -253,7 +254,7 @@ def test_measure_modulated_light_dual_phase_stepping_with_noise(num_blocks, phas # Aberration and image source img = np.zeros((64, 64), dtype=np.int16) img[32, 32] = 100 - src = StaticSource(img, 200 * u.nm) + src = StaticSource(img, pixel_size=200 * u.nm) # SLM, simulation, camera, ROI detector slm = SLM(shape=(100, 100)) @@ -264,7 +265,7 @@ def test_measure_modulated_light_dual_phase_stepping_with_noise(num_blocks, phas numerical_aperture=1.0, wavelength=800 * u.nm, ) - cam = sim.get_camera(analog_max=1e4, gaussian_noise_std=gaussian_noise_std) + cam = Camera(sim, analog_max=1e4, gaussian_noise_std=gaussian_noise_std) roi_detector = SingleRoi(cam, radius=0) # Only measure that specific point # Measure the amount of modulated light (no non-modulated light present) @@ -316,7 +317,7 @@ def test_measure_modulated_light_dual_phase_stepping_with_noise( non_modulated_field_fraction=non_modulated_field, ) sim = Microscope(source=src, incident_field=slm.field, wavelength=800 * u.nm) - cam = sim.get_camera(analog_max=1e3, gaussian_noise_std=gaussian_noise_std) + cam = Camera(sim, analog_max=1e3, gaussian_noise_std=gaussian_noise_std) roi_detector = SingleRoi(cam, radius=0) # Only measure that specific point # Measure the amount of modulated light (no non-modulated light present) diff --git a/tests/test_processors.py b/tests/test_processors.py index 6406fa2..0a79059 100644 --- a/tests/test_processors.py +++ b/tests/test_processors.py @@ -12,7 +12,7 @@ ) def test_croppers(): img = sk.data.camera() - src = StaticSource(img, 50 * u.nm) + src = StaticSource(img, pixel_size=50 * u.nm) roi = select_roi(src, "disk") assert roi.mask_type == "disk" diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 4fdfd4d..2760cec 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -38,8 +38,7 @@ def test_microscope_without_magnification(shape): # construct microscope sim = Microscope(source=src, magnification=1, numerical_aperture=1, wavelength=800 * u.nm) - - cam = sim.get_camera() + cam = Camera(sim) img = cam.read() assert img[256, 256] == 2**16 - 1 @@ -138,7 +137,7 @@ def test_slm_tilt(): new_location = signal_location + shift - cam = sim.get_camera() + cam = Camera(sim) img = cam.read(immediate=True) max_pos = np.unravel_index(np.argmax(img), img.shape) assert np.all(max_pos == new_location) @@ -172,7 +171,7 @@ def test_microscope_wavefront_shaping(caplog): wavelength=800 * u.nm, ) - cam = sim.get_camera(analog_max=100) + cam = Camera(sim, analog_max=100) roi_detector = SingleRoi(cam, pos=signal_location, radius=0) # Only measure that specific point alg = StepwiseSequential(feedback=roi_detector, slm=slm, phase_steps=3, n_x=3, n_y=3) diff --git a/tests/test_wfs.py b/tests/test_wfs.py index 2708da0..870010a 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -13,16 +13,16 @@ ) from ..openwfs.algorithms.troubleshoot import field_correlation from ..openwfs.algorithms.utilities import WFSController +from ..openwfs.plot_utilities import plot_field from ..openwfs.processors import SingleRoi -from ..openwfs.simulation.mockdevices import GaussianNoise from ..openwfs.simulation import SimulatedWFS, StaticSource, SLM, Microscope -from ..openwfs.plot_utilities import plot_field +from ..openwfs.simulation.mockdevices import GaussianNoise, Camera @pytest.mark.parametrize("shape", [(4, 7), (10, 7), (20, 31)]) @pytest.mark.parametrize("noise", [0.0, 0.1]) @pytest.mark.parametrize("algorithm", ["ssa", "fourier"]) -def test_multi_target_algorithms(shape, noise: float, algorithm: str): +def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm: str): """ Test the multi-target capable algorithms (SSA and Fourier dual ref). @@ -165,7 +165,7 @@ def test_fourier_microscope(): img[signal_location] = 100 slm_shape = (1000, 1000) - src = StaticSource(img, 400 * u.nm) + src = StaticSource(img, pixel_size=400 * u.nm) slm = SLM(shape=(1000, 1000)) sim = Microscope( source=src, @@ -175,7 +175,7 @@ def test_fourier_microscope(): aberrations=aberration, wavelength=800 * u.nm, ) - cam = sim.get_camera(analog_max=100) + cam = Camera(sim, analog_max=100) roi_detector = SingleRoi(cam, pos=(250, 250)) # Only measure that specific point alg = FourierDualReference(feedback=roi_detector, slm=slm, slm_shape=slm_shape, k_radius=1.5, phase_steps=3) controller = WFSController(alg) @@ -231,7 +231,6 @@ def test_phase_shift_correction(): # compute the phase pattern to optimize the intensity in target 0 optimised_wf = -np.angle(t) sim.slm.set_phases(0) - before = sim.read() optimised_wf -= 5 signals = [] @@ -376,7 +375,7 @@ def test_simple_genetic(population_size: int, elite_size: int): @pytest.mark.parametrize("basis_str", ("plane_wave", "hadamard")) @pytest.mark.parametrize("shape", ((8, 8), (16, 4))) -def test_dual_reference_ortho_split(basis_str: str, shape): +def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): """Test dual reference with an orthonormal phase-only basis. Two types of bases are tested: plane waves and Hadamard""" do_debug = False @@ -426,6 +425,9 @@ def test_dual_reference_ortho_split(basis_str: str, shape): result = alg.execute() if do_debug: + # Plot the modes + import matplotlib.pyplot as plt + plt.figure() for m in range(N): plt.subplot(*modes_shape[0:2], m + 1) @@ -512,6 +514,9 @@ def test_dual_reference_non_ortho_split(): t_field = np.exp(1j * np.angle(result.t)) if do_debug: + # Plot the modes + import matplotlib.pyplot as plt + plt.figure() for m in range(M): plt.subplot(N2, N1, m + 1)