From c82ab692bcadd329a4d58261db88c3c8ff7452ee Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Thu, 3 Oct 2024 18:56:35 +0200 Subject: [PATCH 01/37] Adding algorithms to development instructions --- docs/source/development.rst | 5 +++++ docs/source/references.bib | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/source/development.rst b/docs/source/development.rst index 7b57fcb..b83e810 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -48,6 +48,11 @@ Reporting bugs and contributing -------------------------------------------------- Bugs can be reported through the GitHub issue tracking system. Better than reporting bugs, we encourage users to *contribute bug fixes, new algorithms, device drivers, and other improvements*. These contributions can be made in the form of a pull request :cite:`zandonellaMassiddaOpenScience2022`, which will be reviewed by the development team and integrated into the package when appropriate. Please contact the current development team through GitHub :cite:`openwfsgithub` to coordinate such contributions. +Implementing new algorithms +-------------------------------------------------- +To implement a new algorithm, the currently existing algorithms can be consulted for a few examples. +Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away. Using `slm.set_phases` and `feedback.trigger` the algorithm can be naive to specific hardware. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. + Implementing new devices -------------------------------------------------- diff --git a/docs/source/references.bib b/docs/source/references.bib index 998292c..d85ee54 100644 --- a/docs/source/references.bib +++ b/docs/source/references.bib @@ -5,6 +5,24 @@ @book{goodman2015statistical publisher = {John Wiley \& Sons} } +@article{Piestun2012, + abstract = {We introduce genetic algorithms (GA) for wavefront control to focus light through highly scattering media. We theoretically and experimentally compare GAs to existing phase control algorithms and show that GAs are particularly advantageous in low signal-to-noise environments.}, + author = {Rafael Piestun and Albert N. Brown and Antonio M. Caravaca-Aguirre and Donald B. Conkey}, + doi = {10.1364/OE.20.004840}, + issn = {1094-4087}, + issue = {5}, + journal = {Optics Express, Vol. 20, Issue 5, pp. 4840-4849}, + keywords = {Optical trapping,Phase conjugation,Phase shift,Scattering media,Spatial light modulators,Turbid media}, + month = {2}, + pages = {4840-4849}, + pmid = {22418290}, + publisher = {Optica Publishing Group}, + title = {Genetic algorithm optimization for focusing through turbid media in noisy environments}, + volume = {20}, + url = {https://opg.optica.org/viewmedia.cfm?uri=oe-20-5-4840&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=oe-20-5-4840 https://opg.optica.org/oe/abstract.cfm?uri=oe-20-5-4840}, + year = {2012}, +} + @misc{Mastiani2024PracticalConsiderations, title = {Practical Considerations for High-Fidelity Wavefront Shaping Experiments}, From c69b730393e8458a8e7e6271882729f630f92b8d Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Thu, 3 Oct 2024 19:22:37 +0200 Subject: [PATCH 02/37] move the mention of similar work --- docs/source/readme.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 833bfeb..2d144b1 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -17,7 +17,7 @@ Wavefront shaping (WFS) is a technique for controlling the propagation of light It stands out that an important driving force in WFS is the development of new algorithms, for example to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed for specific towards the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns, or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019`, and many potential applications have been developed and prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering, :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging such as optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. -With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. +With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. What is OpenWFS? ---------------------- @@ -47,6 +47,7 @@ OpenWFS is a Python package for performing and for simulating wavefront shaping The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. +A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Getting started ---------------------- From ea91c6256ef734f35d25491e508b288e33d9db60 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Thu, 3 Oct 2024 19:28:14 +0200 Subject: [PATCH 03/37] further editing --- docs/source/readme.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 2d144b1..833bfeb 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -17,7 +17,7 @@ Wavefront shaping (WFS) is a technique for controlling the propagation of light It stands out that an important driving force in WFS is the development of new algorithms, for example to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed for specific towards the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns, or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019`, and many potential applications have been developed and prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering, :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging such as optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. -With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. +With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. What is OpenWFS? ---------------------- @@ -47,7 +47,6 @@ OpenWFS is a Python package for performing and for simulating wavefront shaping The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. -A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Getting started ---------------------- From 296a30a9176520f0084faf4c546efd9858f87ae5 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Thu, 3 Oct 2024 20:01:56 +0200 Subject: [PATCH 04/37] added lists of available devices and algorithms --- docs/source/core.rst | 73 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/source/core.rst b/docs/source/core.rst index 3e585c2..e67e4c1 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -71,7 +71,6 @@ Processors ------------ A `Processor` is a `Detector` that takes input from one or more other detectors, and combines/processes this data. We already encountered an example in :numref:`Getting started`, where the `SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. Since a processor, itself, is a `Detector`, multiple processors can be chained together to combine their functionality. The OpenWFS further includes various processors, such as a `CropProcessor` to crop data to a rectangular region of interest, and a `TransformProcessor` to perform affine image transformations to image produced by a source. - Actuators --------- Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators and derive from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. @@ -140,4 +139,74 @@ This synchronization is performed automatically. If desired, it is possible to e Finally, devices have a `timeout` attribute, which is the maximum time to wait for a device to become ready. This timeout is used in the state-switching mechanism, and when explicitly waiting for results using :meth:`~.Device.wait()` or :meth:`~.Device.read()`. - +Currently available devices +---------------------------- + +The following devices are currently implemented in OpenWFS: + +.. list-table:: + :header-rows: 1 + + * - Device Name + - Device Type + - Description + * - Camera + - Detector + - Adapter for GenICam/GenTL cameras + * - ScanningMicroscope + - Detector + - Laser scanning microscope using galvo mirrors and NI DAQ + * - StaticSource + - Detector + - Returns pre-set data, simulating a static source + * - NoiseSource + - Detector + - Generates uniform or Gaussian noise as a source + * - SingleRoi + - Processor (Detector) + - Averages signal over a single ROI + * - MultipleRoi + - Processor (Detector) + - Averages signals over multiple regions of interest (ROIs) + * - CropProcessor + - Processor (Detector) + - Crops data from the source to a region of interest + * - TransformProcessor + - Processor (Detector) + - Performs affine transformations on the source data + * - ADCProcessor + - Processor (Detector) + - Simulates an analog-digital converter + * - SimulatedWFS + - Processor + - Simulates wavefront shaping experiment using Fourier transform-based intensity computation at the focal plane + * - Gain + - Actuator + - Controls PMT gain voltage using NI data acquisition card + * - PhaseSLM + - Actuator + - Simulates a phase-only spatial light modulator + * - SLM + - Actuator + - Controls and renders patterns on a Spatial Light Modulator (SLM) using OpenGL + +Available Algorithms +--------------------- + +The following algorithms are available in OpenWFS for wavefront shaping: + +.. list-table:: + :header-rows: 1 + + * - Algorithm Name + - Description + * - FourierDualReference + - A Fourier dual reference algorithm that uses plane waves from a disk in k-space for wavefront shaping :cite:`Mastiani2022`. + * - IterativeDualReference + - A generic iterative dual reference algorithm with the ability to use custom basis functions for non-linear feedback applications. + * - DualReference + - A generic dual reference algorithm with the option for optimized reference, suitable for multi-target optimization and iterative feedback. + * - SimpleGenetic + - A simple genetic algorithm that optimizes wavefronts by selecting elite individuals and introducing mutations for focusing through scattering media :cite:`Piestun2012`. + * - StepwiseSequential + - A stepwise sequential algorithm which systematically modifies the phase pattern of each SLM element :cite:`Vellekoop2007`. \ No newline at end of file From e2d6adfd229711a39f2011f9152a6b1b07515bd4 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 14:49:11 +0200 Subject: [PATCH 05/37] pydevice chapter --- docs/source/index.rst | 1 + docs/source/index_latex.rst | 1 + docs/source/pydevice.rst | 6 ++++++ 3 files changed, 8 insertions(+) create mode 100644 docs/source/pydevice.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 0907aa4..c6da15e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,5 +10,6 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments slms simulations development + pydevice api auto_examples/index diff --git a/docs/source/index_latex.rst b/docs/source/index_latex.rst index 198c2b6..5945f93 100644 --- a/docs/source/index_latex.rst +++ b/docs/source/index_latex.rst @@ -9,6 +9,7 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments core slms simulations + pydevice development conclusion diff --git a/docs/source/pydevice.rst b/docs/source/pydevice.rst new file mode 100644 index 0000000..2551f06 --- /dev/null +++ b/docs/source/pydevice.rst @@ -0,0 +1,6 @@ +.. _section-pydevice: + +OpenWFS in PyDevice +============================================== + +To smoothly enable end-user interaction with wavefront shaping algorithms, the Micro-Manager device adapter PyDevice was developed :cite:`MMpydevice`. A more detailed description can be found in the mmCoreAndDevices source tree :cite:`mmCoreAndDevices`. In essence, PyDevice is a c++ based adapter that imports objects from a Python script and integrates them in Micro-Manager as devices, e.g. a camera or stage. OpenWFS was written in compliance with the templates required for PyDevice, which means OpenWFS cameras, scanners and algorithms can be loaded into Micro-Manager as devices. Examples of this are found in the example gallery :cite:`ExampleGallery`. \ No newline at end of file From 620683cba8e6b4195f274dc5bc3c3ed6a8720065 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 16:27:07 +0200 Subject: [PATCH 06/37] correct stage name in micromanager example --- examples/micro_manager_microscope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/micro_manager_microscope.py b/examples/micro_manager_microscope.py index d038939..0c80dfc 100644 --- a/examples/micro_manager_microscope.py +++ b/examples/micro_manager_microscope.py @@ -44,4 +44,4 @@ ) # construct dictionary of objects to expose to Micro-Manager -devices = {"camera": cam, "stage": mic.stage} +devices = {"camera": cam, "stage": mic.xy_stage} From 987683bb0ab5844cb71cc9093b4ecac155ba0b4a Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 16:40:59 +0200 Subject: [PATCH 07/37] further pydevice details --- docs/source/pydevice.rst | 2 +- docs/source/references.bib | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/pydevice.rst b/docs/source/pydevice.rst index 2551f06..7eca427 100644 --- a/docs/source/pydevice.rst +++ b/docs/source/pydevice.rst @@ -3,4 +3,4 @@ OpenWFS in PyDevice ============================================== -To smoothly enable end-user interaction with wavefront shaping algorithms, the Micro-Manager device adapter PyDevice was developed :cite:`MMpydevice`. A more detailed description can be found in the mmCoreAndDevices source tree :cite:`mmCoreAndDevices`. In essence, PyDevice is a c++ based adapter that imports objects from a Python script and integrates them in Micro-Manager as devices, e.g. a camera or stage. OpenWFS was written in compliance with the templates required for PyDevice, which means OpenWFS cameras, scanners and algorithms can be loaded into Micro-Manager as devices. Examples of this are found in the example gallery :cite:`ExampleGallery`. \ No newline at end of file +To smoothly enable end-user interaction with wavefront shaping algorithms, the Micro-Manager device adapter PyDevice was developed :cite:`PyDevice`. A more detailed description can be found in the mmCoreAndDevices source tree :cite:`mmCoreAndDevices`. In essence, PyDevice is Micro-Manager adapter that imports objects from a Python script and integrates them as devices, e.g. a camera or stage. OpenWFS was written in compliance with the templates required for PyDevice, which means OpenWFS cameras, scanners and algorithms can be loaded into Micro-Manager as devices. Examples of this are found in the example gallery :cite:`readthedocsOpenWFS`. Further developments due to this seamless connection can be a dedicated Micro-Manager based wavefront shaping GUI. \ No newline at end of file diff --git a/docs/source/references.bib b/docs/source/references.bib index 1fc95a3..dcf3513 100644 --- a/docs/source/references.bib +++ b/docs/source/references.bib @@ -5,6 +5,8 @@ @book{goodman2015statistical publisher = {John Wiley \& Sons} } + + @article{Piestun2012, abstract = {We introduce genetic algorithms (GA) for wavefront control to focus light through highly scattering media. We theoretically and experimentally compare GAs to existing phase control algorithms and show that GAs are particularly advantageous in low signal-to-noise environments.}, author = {Rafael Piestun and Albert N. Brown and Antonio M. Caravaca-Aguirre and Donald B. Conkey}, @@ -453,6 +455,11 @@ @article{Anderson2016 } +@misc{mmCoreAndDevices, + author = {Mark Tsuchida and Sam Griffin}, + title = {Micro-Manager mmCoreAndDevices repository}, + url = {https://github.com/micro-manager/mmCoreAndDevices}, +} @misc{MMoverview, author = {Mark Tsuchida and Sam Griffin}, title = {Micro-Manager Project Overview}, From 2f4a968fb2a0cf9f5e2a280f7484193768a460dd Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Fri, 4 Oct 2024 16:49:09 +0200 Subject: [PATCH 08/37] fixing documentation warnings --- STYLEGUIDE.md | 14 +- docs/source/references.bib | 5 - examples/micro_manager_microscope.py | 2 +- examples/micro_manager_scanning_microscope.py | 4 +- openwfs/algorithms/basic_fourier.py | 12 +- .../algorithms/custom_iter_dual_reference.py | 250 ------------------ openwfs/algorithms/dual_reference.py | 34 +-- openwfs/algorithms/genetic.py | 36 ++- openwfs/algorithms/ssa.py | 2 +- openwfs/devices/galvo_scanner.py | 14 +- 10 files changed, 71 insertions(+), 302 deletions(-) delete mode 100644 openwfs/algorithms/custom_iter_dual_reference.py diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 0f814e0..62ef254 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -6,8 +6,18 @@ # General -- The package `black` is used to ensure correct formatting. Install with `pip install black` and run in the terminal using `black .` when located at the root of the repository. +- The package `black` is used to ensure correct formatting. Install with `pip install black` and run in the terminal + using `black .` when located at the root of the repository. # Tests -- Tests must *not* plot figures. \ No newline at end of file +- Tests must *not* plot figures. + +# Sphinx + +Common warnings: + +- All line numbers are relative to the start of the docstring. +- 'WARNING: Block quote ends without a blank line; unexpected unindent'. This happens if a block of text is not properly + wrapped and one of the lines starts with a space. To fox, remove the space at the beginning of the line. +- 'ERROR: Unexpected indentation' can be caused if a line ends with ':' and the next line is not indented or empty. \ No newline at end of file diff --git a/docs/source/references.bib b/docs/source/references.bib index f46cb57..cb75b43 100644 --- a/docs/source/references.bib +++ b/docs/source/references.bib @@ -500,11 +500,6 @@ @article{Park2012 } -@misc{pydevice, - title = {{PyDevice} {GitHub} repository}, - url = {https://www.github.com/IvoVellekoop/pydevice}, -} - @article{Pinkard2021, author = {Henry Pinkard and Nico Stuurman and Ivan E Ivanov and Nicholas M Anthony and Wei Ouyang and Bin Li and Bin Yang and Mark A Tsuchida and Bryant Chhun and Grace Zhang and Ryan Mei and Michael Anderson and Douglas P Shepherd and Ian Hunt-Isaak and Raymond L Dunn and Wiebke Jahr and Saul Kato and Loïc A Royer and Jay R Thiagarajah and Kevin W Eliceiri and Emma Lundberg and Shalin B Mehta and Laura Waller}, journal = {Nature Methods}, diff --git a/examples/micro_manager_microscope.py b/examples/micro_manager_microscope.py index d038939..8db6edd 100644 --- a/examples/micro_manager_microscope.py +++ b/examples/micro_manager_microscope.py @@ -1,5 +1,5 @@ """ Micro-Manager simulated microscope -======================= +====================================================================== This script simulates a microscope with a random noise image as a mock specimen. The numerical aperture, stage position, and other parameters can be modified through the Micro-Manager GUI. To use this script as a device in Micro-Manager, make sure you have the PyDevice adapter installed and diff --git a/examples/micro_manager_scanning_microscope.py b/examples/micro_manager_scanning_microscope.py index 5e69694..637804e 100644 --- a/examples/micro_manager_scanning_microscope.py +++ b/examples/micro_manager_scanning_microscope.py @@ -1,5 +1,5 @@ """ Micro-Manager simulated scanning microscope -======================= +====================================================================== This script simulates a scanning microscope with a pre-set image as a mock specimen. The scan parameters can be modified through the Micro-Manager GUI. To use this script as a device in Micro-Manager, make sure you have the PyDevice adapter installed and @@ -7,11 +7,11 @@ """ import astropy.units as u +import skimage # add 'openwfs' to the search path. This is only needed when developing openwfs # otherwise it is just installed as a package import set_path # noqa -import skimage from openwfs.devices import ScanningMicroscope, Axis from openwfs.devices.galvo_scanner import InputChannel diff --git a/openwfs/algorithms/basic_fourier.py b/openwfs/algorithms/basic_fourier.py index ec1d8da..822bc0e 100644 --- a/openwfs/algorithms/basic_fourier.py +++ b/openwfs/algorithms/basic_fourier.py @@ -9,14 +9,14 @@ class FourierDualReference(DualReference): - """ - Fourier double reference algorithm, based on Mastiani et al. [1]. + """Fourier double reference algorithm, based on Mastiani et al. [1]. Improvements over [1]: - * The set of plane waves is taken from a disk in k-space instead of a square. - * No overlap between the two halves is needed, instead the final stitching step is done using measurements already in the data set. - * When only a single target is optimized, the algorithm can be used in an iterative version to increase SNR during the measurument, - similar to [2]. + + - The set of plane waves is taken from a disk in k-space instead of a square. + - No overlap between the two halves is needed, instead the final stitching step is done using measurements already in the data set. + - When only a single target is optimized, the algorithm can be used in an iterative version to increase SNR during the measurument, + similar to [2]. [1]: Bahareh Mastiani, Gerwin Osnabrugge, and Ivo M. Vellekoop, "Wavefront shaping for forward scattering," Opt. Express 30, 37436-37445 (2022) diff --git a/openwfs/algorithms/custom_iter_dual_reference.py b/openwfs/algorithms/custom_iter_dual_reference.py deleted file mode 100644 index 8539289..0000000 --- a/openwfs/algorithms/custom_iter_dual_reference.py +++ /dev/null @@ -1,250 +0,0 @@ -from typing import Optional - -import numpy as np -from numpy import ndarray as nd - -from .utilities import analyze_phase_stepping, WFSResult -from ..core import Detector, PhaseSLM - - -def weighted_average(a, b, wa, wb): - """ - Compute the weighted average of two values. - - Args: - a: The first value. - b: The second value. - wa: The weight of the first value. - wb: The weight of the second value. - """ - return (a * wa + b * wb) / (wa + wb) - - -class IterativeDualReference: - """ - A generic iterative dual reference WFS algorithm, which can use a custom set of basis functions. - - This algorithm is adapted from [1], with the addition of the ability to use custom basis functions and specify the number of iterations. - - In this algorithm, the SLM pixels are divided into two groups: A and B, as indicated by the boolean group_mask argument. - The algorithm first keeps the pixels in group B fixed, and displays a sequence on patterns on the pixels of group A. - It uses these measurements to construct an optimized wavefront that is displayed on the pixels of group A. - This process is then repeated for the pixels of group B, now using the *optimized* wavefront on group A as reference. - Optionally, the process can be repeated for a number of iterations, which each iteration using the current correction - pattern as a reference. This makes this algorithm suitable for non-linear feedback, such as multi-photon - excitation fluorescence [2]. - - This algorithm assumes a phase-only SLM. Hence, the input modes are defined by passing the corresponding phase - patterns (in radians) as input argument. - - [1]: X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive - optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). - - [2]: Gerwin Osnabrugge, Lyubov V. Amitonova, and Ivo M. Vellekoop. "Blind focusing through strongly scattering media - using wavefront shaping with nonlinear feedback", Optics Express, 27(8):11673–11688, 2019. - https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 - """ - - def __init__( - self, - feedback: Detector, - slm: PhaseSLM, - phase_patterns: tuple[nd, nd], - group_mask: nd, - phase_steps: int = 4, - iterations: int = 4, - analyzer: Optional[callable] = analyze_phase_stepping, - ): - """ - Args: - feedback: The feedback source, usually a detector that provides measurement data. - slm: Spatial light modulator object. - phase_patterns: A tuple of two 3D arrays, containing the phase patterns for group A and group B, respectively. - The first two dimensions are the spatial dimensions, and should match the size of group_mask. - The 3rd dimension in the array is index of the phase pattern. The number of phase patterns in A and B may be different. - group_mask: A 2D bool array of that defines the pixels used by group A with False and elements used by - group B with True. - phase_steps: The number of phase steps for each mode (default is 4). Depending on the type of - non-linear feedback and the SNR, more might be required. - iterations: Number of times to measure a mode set, e.g. when iterations = 5, the measurements are - A, B, A, B, A. Should be at least 2 - analyzer: The function used to analyze the phase stepping data. Must return a WFSResult object. Defaults to `analyze_phase_stepping` - """ - if (phase_patterns[0].shape[0:2] != group_mask.shape) or (phase_patterns[1].shape[0:2] != group_mask.shape): - raise ValueError("The phase patterns and group mask must all have the same shape.") - if iterations < 2: - raise ValueError("The number of iterations must be at least 2.") - if np.prod(feedback.data_shape) != 1: - raise ValueError("The feedback detector should return a single scalar value.") - - self.slm = slm - self.feedback = feedback - self.phase_steps = phase_steps - self.iterations = iterations - self.analyzer = analyzer - self.phase_patterns = ( - phase_patterns[0].astype(np.float32), - phase_patterns[1].astype(np.float32), - ) - mask = group_mask.astype(bool) - self.masks = (~mask, mask) # masks[0] is True for group A, mask[1] is True for group B - - # Pre-compute the conjugate modes for reconstruction - self.modes = [ - np.exp(-1j * self.phase_patterns[side]) * np.expand_dims(self.masks[side], axis=2) for side in range(2) - ] - - def execute(self, capture_intermediate_results: bool = False, progress_bar=None) -> WFSResult: - """ - Executes the blind focusing dual reference algorithm and compute the SLM transmission matrix. - capture_intermediate_results: When True, measures the feedback from the optimized wavefront after each iteration. - This can be useful to determine how many iterations are needed to converge to an optimal pattern. - This data is stored as the 'intermediate_results' field in the results - progress_bar: Optional progress bar object. Following the convention for tqdm progress bars, - this object should have a `total` attribute and an `update()` function. - - Returns: - WFSResult: An object containing the computed SLM transmission matrix and related data. The amplitude profile - of each mode is assumed to be 1. If a different amplitude profile is desired, this can be obtained by - multiplying that amplitude profile with this transmission matrix. - """ - - # Current estimate of the transmission matrix (start with all 0) - t_full = np.zeros(shape=self.modes[0].shape[0:2]) - t_other_side = t_full - - # Initialize storage lists - t_set_all = [None] * self.iterations - results_all = [None] * self.iterations # List to store all results - results_latest = [ - None, - None, - ] # The two latest results. Used for computing fidelity factors. - intermediate_results = np.zeros(self.iterations) # List to store feedback from full patterns - - # Prepare progress bar - if progress_bar: - num_measurements = ( - np.ceil(self.iterations / 2) * self.modes[0].shape[2] - + np.floor(self.iterations / 2) * self.modes[1].shape[2] - ) - progress_bar.total = num_measurements - - # Switch the phase sets back and forth multiple times - for it in range(self.iterations): - side = it % 2 # pick set A or B for phase stepping - ref_phases = -np.angle(t_full) # use the best estimate so far to construct an optimized reference - side_mask = self.masks[side] - # Perform WFS experiment on one side, keeping the other side sized at the ref_phases - result = self._single_side_experiment( - mod_phases=self.phase_patterns[side], - ref_phases=ref_phases, - mod_mask=side_mask, - progress_bar=progress_bar, - ) - - # Compute transmission matrix for the current side and update - # estimated transmission matrix - t_this_side = self.compute_t_set(result, self.modes[side]) - t_full = t_this_side + t_other_side - t_other_side = t_this_side - - # Store results - t_set_all[it] = t_this_side # Store transmission matrix - results_all[it] = result # Store result - results_latest[side] = result # Store latest result for this set - - # Try full pattern - if capture_intermediate_results: - self.slm.set_phases(-np.angle(t_full)) - intermediate_results[it] = self.feedback.read() - - # Compute average fidelity factors - fidelity_noise = weighted_average( - results_latest[0].fidelity_noise, - results_latest[1].fidelity_noise, - results_latest[0].n, - results_latest[1].n, - ) - fidelity_amplitude = weighted_average( - results_latest[0].fidelity_amplitude, - results_latest[1].fidelity_amplitude, - results_latest[0].n, - results_latest[1].n, - ) - fidelity_calibration = weighted_average( - results_latest[0].fidelity_calibration, - results_latest[1].fidelity_calibration, - results_latest[0].n, - results_latest[1].n, - ) - - result = WFSResult( - t=t_full, - n=self.modes[0].shape[2] + self.modes[1].shape[2], - axis=2, - fidelity_noise=fidelity_noise, - fidelity_amplitude=fidelity_amplitude, - fidelity_calibration=fidelity_calibration, - ) - - # TODO: document the t_set_all and results_all attributes - result.t_set_all = t_set_all - result.results_all = results_all - result.intermediate_results = intermediate_results - return result - - def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, progress_bar=None) -> WFSResult: - """ - Conducts experiments on one part of the SLM. - - Args: - mod_phases: 3D array containing the phase patterns of each mode. Axis 0 and 1 are used as spatial axis. - Axis 2 is used for the 'phase pattern index' or 'mode index'. - ref_phases: 2D array containing the reference phase pattern. - mod_mask: 2D array containing a boolean mask, where True indicates the modulated part of the SLM. - progress_bar: Optional progress bar object. Following the convention for tqdm progress bars, - this object should have a `total` attribute and an `update()` function. - - Returns: - WFSResult: An object containing the computed SLM transmission matrix and related data. - """ - num_modes = mod_phases.shape[2] - measurements = np.zeros((num_modes, self.phase_steps)) - - for m in range(num_modes): - phases = ref_phases.copy() - modulated = mod_phases[:, :, m] - for p in range(self.phase_steps): - phi = p * 2 * np.pi / self.phase_steps - # set the modulated pixel values to the values corresponding to mode m and phase offset phi - phases[mod_mask] = modulated[mod_mask] + phi - self.slm.set_phases(phases) - self.feedback.trigger(out=measurements[m, p, ...]) - - if progress_bar is not None: - progress_bar.update() - - self.feedback.wait() - return self.analyzer(measurements, axis=1) - - @staticmethod - def compute_t_set(wfs_result: WFSResult, mode_set: nd) -> nd: - """ - Compute the transmission matrix in SLM space from transmission matrix in input mode space. - - Note 1: This function computes the transmission matrix for one mode set, and thus returns one part of the full - transmission matrix. The elements that are not part of the mode set will be 0. The full transmission matrix can - be obtained by simply adding the parts, i.e. t_full = t_set0 + t_set1. - - Note 2: As this is a blind focusing WFS algorithm, there may be only one target or 'output mode'. - - Args: - wfs_result (WFSResult): The result of the WFS algorithm. This contains the transmission matrix in the space - of input modes. - mode_set: 3D array with set of modes. - """ - t = wfs_result.t.squeeze().reshape((1, 1, mode_set.shape[2])) - norm_factor = np.prod(mode_set.shape[0:2]) - t_set = (t * mode_set).sum(axis=2) / norm_factor - return t_set diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index b81f828..bffcff9 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional, Union, List import numpy as np from numpy import ndarray as nd @@ -8,28 +8,32 @@ class DualReference: - """ - A generic iterative dual reference WFS algorithm, which can use a custom set of basis functions. + """A generic iterative dual reference WFS algorithm, which can use a custom set of basis functions. - This algorithm is adapted from [1], with the addition of the ability to use custom basis functions and specify the number of iterations. + This algorithm is adapted from [Tao2017]_, with the addition of the ability to use custom basis functions and + specify the number of iterations. - In this algorithm, the SLM pixels are divided into two groups: A and B, as indicated by the boolean group_mask argument. + In this algorithm, the SLM pixels are divided into two groups: + A and B, as indicated by the boolean group_mask argument. The algorithm first keeps the pixels in group B fixed, and displays a sequence on patterns on the pixels of group A. It uses these measurements to construct an optimized wavefront that is displayed on the pixels of group A. This process is then repeated for the pixels of group B, now using the *optimized* wavefront on group A as reference. Optionally, the process can be repeated for a number of iterations, which each iteration using the current correction - pattern as a reference. This makes this algorithm suitable for non-linear feedback, such as multi-photon - excitation fluorescence [2]. + pattern as a reference. This makes this algorithm suitable for non-linear feedback, such as multi-photon + excitation fluorescence [Osnabrugge2019]_. This algorithm assumes a phase-only SLM. Hence, the input modes are defined by passing the corresponding phase patterns (in radians) as input argument. - [1]: X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive - optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). + References + ---------- + .. [Tao2017] X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive + optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). + + .. [Osnabrugge2019] Gerwin Osnabrugge, Lyubov V. Amitonova, and Ivo M. Vellekoop. "Blind focusing through strongly scattering media + using wavefront shaping with nonlinear feedback", Optics Express, 27(8):11673–11688, 2019. + https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 - [2]: Gerwin Osnabrugge, Lyubov V. Amitonova, and Ivo M. Vellekoop. "Blind focusing through strongly scattering media - using wavefront shaping with nonlinear feedback", Optics Express, 27(8):11673–11688, 2019. - https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 """ def __init__( @@ -148,7 +152,7 @@ def phase_patterns(self, value): b0_index = next(i for i in range(value[1].shape[2]) if np.allclose(value[1][:, :, i], 0)) self.zero_indices = (a0_index, b0_index) except StopIteration: - raise ("For multi-target optimization, the both sets must contain a flat wavefront with phase 0.") + raise "For multi-target optimization, the both sets must contain a flat wavefront with phase 0." if (value[0].shape[0:2] != self._shape) or (value[1].shape[0:2] != self._shape): raise ValueError("The phase patterns and group mask must all have the same shape.") @@ -184,7 +188,7 @@ def _compute_cobasis(self): denotes the matrix inverse, and ⁺ denotes the Moore-Penrose pseudo-inverse. """ if self.phase_patterns is None: - raise ("The phase_patterns must be set before computing the cobasis.") + raise "The phase_patterns must be set before computing the cobasis." cobasis = [None, None] for side in range(2): @@ -218,7 +222,7 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No ref_phases = np.zeros(self._shape) # Initialize storage lists - results_all = [None] * self.iterations # List to store all results + results_all: List[Optional[WFSResult]] = [None] * self.iterations # List to store all results intermediate_results = np.zeros(self.iterations) # List to store feedback from full patterns # Prepare progress bar diff --git a/openwfs/algorithms/genetic.py b/openwfs/algorithms/genetic.py index da91588..c59b677 100644 --- a/openwfs/algorithms/genetic.py +++ b/openwfs/algorithms/genetic.py @@ -13,21 +13,28 @@ class SimpleGenetic: in [1] and [2]. The algorithm performs the following steps: + 1. Initialize all wavefronts in the population with random phases - 2. For each generation - 1. Determine the feedback signal for each wavefront - 2. Select the 'elite_size' best wavefronts to keep. Replace the rest: - * If the elite wavefronts are too similar (> 97% identical elements), - randomly generate the new wavefronts - * Otherwise, generate new wavefronts by randomly selecting two elite wavefronts - and mixing them randomly (element-wise). - Then perform a mutation that replaces a fraction (`mutation_probability`) of the - elements by a new value. - - [1]: Conkey D B, Brown A N, Caravaca-Aguirre A M and Piestun R 'Genetic algorithm optimization - for focusing through turbid media in noisy environments' Opt. Express 20 4840–9 (2012). - [2]: Benjamin R Anderson et al 'A modular GUI-based program for genetic algorithm-based - feedback-assisted wavefront shaping', J. Phys. Photonics 6 045008 (2024). + + 2. For each generation: + + 2.1. Determine the feedback signal for each wavefront. + + 2.2. Select the 'elite_size' best wavefronts to keep. Replace the rest with new wavefronts. + 2.2.1 If the elite wavefronts are too similar (> 97% identical elements), + randomly generate the new wavefronts. + + 2.2.2 Otherwise, generate new wavefronts by randomly selecting two elite wavefronts + and mixing them randomly (element-wise). + Then perform a mutation that replaces a fraction (`mutation_probability`) of the + elements by a new value. + + References + ---------- + [^1]: Conkey D B, Brown A N, Caravaca-Aguirre A M and Piestun R 'Genetic algorithm optimization + for focusing through turbid media in noisy environments' Opt. Express 20 4840–9 (2012). + [^2]: Benjamin R Anderson et al 'A modular GUI-based program for genetic algorithm-based + feedback-assisted wavefront shaping', J. Phys. Photonics 6 045008 (2024). """ def __init__( @@ -51,6 +58,7 @@ def __init__( generations (int): The number of generations mutation_probability (int): Fraction of elements in the offspring to mutate generator: a `np.random.Generator`, defaults to np.random.default_rng() + """ if np.prod(feedback.data_shape) != 1: raise ValueError("Only scalar feedback is supported") diff --git a/openwfs/algorithms/ssa.py b/openwfs/algorithms/ssa.py index 48368af..c2cebf0 100644 --- a/openwfs/algorithms/ssa.py +++ b/openwfs/algorithms/ssa.py @@ -11,7 +11,7 @@ class StepwiseSequential: TODO: enable low-res pre-optimization (as in Vellekoop & Mosk 2007) TODO: modulate segments in pupil (circle) only - [^1]:Vellekoop, I. M., & Mosk, A. P. (2007). Focusing coherent light through opaque strongly scattering media. + [1]: Vellekoop, I. M., & Mosk, A. P. (2007). Focusing coherent light through opaque strongly scattering media. Optics Letters, 32(16), 2309-2311. [2]: Ivo M. Vellekoop, "Feedback-based wavefront shaping," Opt. Express 23, 12189-12206 (2015) """ diff --git a/openwfs/devices/galvo_scanner.py b/openwfs/devices/galvo_scanner.py index 21b2259..1fe513f 100644 --- a/openwfs/devices/galvo_scanner.py +++ b/openwfs/devices/galvo_scanner.py @@ -92,18 +92,20 @@ def maximum_scan_speed(self, linear_range: float): It is assumed that the mirror accelerates and decelerates at the maximum acceleration, and scans with a constant velocity over the linear range. There are two limits to the scan speed: - * A practical limit: if it takes longer to perform the acceleration + deceleration than - it does to traverse the linear range, it does not make sense to set the scan speed so high. - The speed at which acceleration + deceleration takes as long as the linear range is the maximum speed. - * A hardware limit: when accelerating with the maximum acceleration over a distance - 0.5 * (V_max-V_min) * (1-linear_range), - the mirror will reach the maximum possible speed. + + - A practical limit: if it takes longer to perform the acceleration + deceleration than + it does to traverse the linear range, it does not make sense to set the scan speed so high. + The speed at which acceleration + deceleration takes as long as the linear range is the maximum speed. + - A hardware limit: when accelerating with the maximum acceleration over a distance + 0.5 · (V_max-V_min) · (1-linear_range), + the mirror will reach the maximum possible speed. Args: linear_range (float): fraction of the full range that is used for the linear part of the scan Returns: Quantity[u.V / u.s]: maximum scan speed + """ # x = 0.5 · a · t² = 0.5 (v_max - v_min) · (1 - linear_range) t_accel = np.sqrt((self.v_max - self.v_min) * (1 - linear_range) / self.maximum_acceleration) From 1f07ac607ed365c38b819595f0d5877cc88ee8d9 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 17:05:54 +0200 Subject: [PATCH 09/37] typos --- docs/source/core.rst | 2 +- docs/source/development.rst | 2 +- docs/source/readme.rst | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/core.rst b/docs/source/core.rst index e67e4c1..68572d8 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -73,7 +73,7 @@ A `Processor` is a `Detector` that takes input from one or more other detectors, Actuators --------- -Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators and derive from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. +Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators are derived from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. Units and metadata ---------------------------------- diff --git a/docs/source/development.rst b/docs/source/development.rst index b83e810..fc60803 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -75,7 +75,7 @@ If the detector is created with the flag ``multi_threaded = True``, then `_fetch Implementing a processor ++++++++++++++++++++++++++++++++++ -To implement a data processing step that dynamically processes date from one or more input detectors, implement a custom processor. This is done by deriving from the `Processor` base class and implementing the `__init__` function. This function should pass a list of all upstream nodes, i. e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth"`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls `_fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. +To implement a data processing step that dynamically processes data from one or more input detectors, implement a custom processor. This is done by deriving from the `Processor` base class and implementing the `__init__` function. This function should pass a list of all upstream nodes, i. e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth"`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls `_fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. Implementing an actuator +++++++++++++++++++++++++++++++ diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 833bfeb..b077112 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -15,7 +15,7 @@ What is wavefront shaping? Wavefront shaping (WFS) is a technique for controlling the propagation of light in arbitrarily complex structures, including strongly scattering materials :cite:`kubby2019`. In WFS, a spatial light modulator (SLM) is used to shape the phase and/or amplitude of the incident light. With a properly constructed wavefront, light can be made to focus through :cite:`Vellekoop2007`, or inside :cite:`vellekoop2008demixing` scattering materials; or light can be shaped to have other desired properties, such as optimal sensitivity for specific measurements :cite:`bouchet2021maximum`, specialized point-spread functions :cite:`boniface2017transmission`, spectral filtering :cite:`Park2012`,, or for functions like optical trapping :cite:`vcivzmar2010situ`. -It stands out that an important driving force in WFS is the development of new algorithms, for example to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed for specific towards the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns, or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019`, and many potential applications have been developed and prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering, :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging such as optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. +It stands out that an important driving force in WFS is the development of new algorithms, for example, to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed specifically for the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019` have also been developed, and many potential applications have been prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging, such as in optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. @@ -50,9 +50,9 @@ OpenWFS is a Python package for performing and for simulating wavefront shaping Getting started ---------------------- -OpenWFS is available on the PyPI repository, and it can be installed with the command ``pip install openwfs``. The latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`. To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 (not all dependencies were available for Python 3.12 yet). OpenWFS is developed and tested on Windows 11 and Manjaro Linux. +OpenWFS is available on the PyPI repository, and it can be installed with the command ``pip install openwfs``. The latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`, and the entire repository can be found on :cite:`openwfsgithub`. To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 (not all dependencies were available for Python 3.12 yet). OpenWFS is developed and tested on Windows 11 and Manjaro Linux. Note that for certain hardware components, third party software needs to be installed. This is always mentioned in the documentation and docstrings of these functions. -:numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. +:numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. .. _hello-wfs: .. literalinclude:: ../../examples/hello_wfs.py From 99dc770f084a29f6d9a91e052865c37247c75c2e Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 17:27:40 +0200 Subject: [PATCH 10/37] font consistency --- docs/source/readme.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/readme.rst b/docs/source/readme.rst index b077112..b924d0e 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -13,7 +13,7 @@ What is wavefront shaping? -------------------------------- -Wavefront shaping (WFS) is a technique for controlling the propagation of light in arbitrarily complex structures, including strongly scattering materials :cite:`kubby2019`. In WFS, a spatial light modulator (SLM) is used to shape the phase and/or amplitude of the incident light. With a properly constructed wavefront, light can be made to focus through :cite:`Vellekoop2007`, or inside :cite:`vellekoop2008demixing` scattering materials; or light can be shaped to have other desired properties, such as optimal sensitivity for specific measurements :cite:`bouchet2021maximum`, specialized point-spread functions :cite:`boniface2017transmission`, spectral filtering :cite:`Park2012`,, or for functions like optical trapping :cite:`vcivzmar2010situ`. +Wavefront shaping (WFS) is a technique for controlling the propagation of light in arbitrarily complex structures, including strongly scattering materials :cite:`kubby2019`. In WFS, a spatial light modulator (SLM) is used to shape the phase and/or amplitude of the incident light. With a properly constructed wavefront, light can be made to focus through :cite:`Vellekoop2007`, or inside :cite:`vellekoop2008demixing` scattering materials; or light can be shaped to have other desired properties, such as optimal sensitivity for specific measurements :cite:`bouchet2021maximum`, specialized point-spread functions :cite:`boniface2017transmission`, spectral filtering :cite:`Park2012`, or for functions like optical trapping :cite:`vcivzmar2010situ`. It stands out that an important driving force in WFS is the development of new algorithms, for example, to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed specifically for the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019` have also been developed, and many potential applications have been prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging, such as in optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. @@ -59,9 +59,9 @@ OpenWFS is available on the PyPI repository, and it can be installed with the co :language: python :caption: ``hello_wfs.py``. Example of a simple wavefront shaping experiment using OpenWFS. -This example uses the `StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. +This example uses the `~.StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. -This code illustrates how OpenWFS separates the concerns of the hardware control (`SLM` and `Camera`), signal processing (`SingleROIProcessor`) and the algorithm itself (`StepwiseSequential`). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a :class:`~.MultiRoiProcessor` object), using different algorithms, or different image sources, such as a :class:`~.ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see :numref:`section-simulations`). +This code illustrates how OpenWFS separates the concerns of the hardware control (:class:`~.SLM` and :class:`~.Camera`), signal processing (:class:`~.SingleRoi(Processor)`) and the algorithm itself (:class:`~.StepwiseSequential`). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a :class:`~.MultiRoi(Processor)` object), using different algorithms, or different image sources, such as a :class:`~.ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see :numref:`section-simulations`). Analysis and troubleshooting From 156edc63f09b991bba9291576824929743879f7d Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 17:59:01 +0200 Subject: [PATCH 11/37] remove bottleneck --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9c927d7..e3e6c33 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -65,7 +65,7 @@ and simulations for testing new algorithms. The complexity of the many different aspects of wavefront shaping software, however, - is becoming a bottleneck for further developments in the field, as well as for end-user adoption. + is becoming a limiting factor for further developments in the field, as well as for end-user adoption. OpenWFS addresses these challenges by providing a Python module that coherently integrates all aspects of wavefront shaping code. The module is designed to be modular and easy to expand. It incorporates elements for hardware control, software simulation, and automated troubleshooting. From 8aeced5a9e563c142b9f1270850a1f08e49ea502 Mon Sep 17 00:00:00 2001 From: Jeroen Doornbos Date: Fri, 4 Oct 2024 18:07:51 +0200 Subject: [PATCH 12/37] rewrite abstract --- docs/source/conf.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index e3e6c33..c62a75b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -58,17 +58,16 @@ Wavefront shaping (WFS) is a technique for controlling the propagation of light. With applications ranging from microscopy to free-space telecommunication, this research field is expanding rapidly. - It stands out that many of the important breakthroughs are made by developing better software that - incorporates increasingly advanced physical models and algorithms. - Typical control software involves individual code for scanning microscopy, image processing, + As the field advances, it stands out that many breakthroughs are driven by the development of better + software that incorporates increasingly advanced physical models and algorithms. + Typical control software involves fragmented implementations for scanning microscopy, image processing, optimization algorithms, low-level hardware control, calibration and troubleshooting, and simulations for testing new algorithms. The complexity of the many different aspects of wavefront shaping software, however, is becoming a limiting factor for further developments in the field, as well as for end-user adoption. - OpenWFS addresses these challenges by providing a Python module that coherently integrates - all aspects of wavefront shaping code. The module is designed to be modular and easy to expand. - It incorporates elements for hardware control, software simulation, and automated troubleshooting. + OpenWFS addresses these challenges by providing a modular and extensible Python library that + incorporates elements for hardware control, software simulation, and automated troubleshooting. Using these elements, the actual wavefront shaping algorithm and its automated tests can be written in just a few lines of code. } From e77b00dce854b51d0ced8e58d11eefec388c4952 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Fri, 4 Oct 2024 22:32:49 +0200 Subject: [PATCH 13/37] grammar and typo fixes --- .readthedocs.yaml | 2 +- README.md | 227 +++++++++++++++++++------- STYLEGUIDE.md | 8 +- docs/source/conf.py | 9 +- docs/source/core.rst | 4 +- docs/source/development.rst | 4 +- docs/source/readme.rst | 4 +- docs/source/slms.rst | 2 +- examples/slm_demo.py | 2 +- openwfs/algorithms/basic_fourier.py | 13 +- openwfs/algorithms/dual_reference.py | 37 +++-- openwfs/algorithms/genetic.py | 2 +- openwfs/algorithms/troubleshoot.py | 17 +- openwfs/algorithms/utilities.py | 24 +-- openwfs/core.py | 27 +-- openwfs/devices/camera.py | 2 +- openwfs/devices/galvo_scanner.py | 8 +- openwfs/devices/slm/geometry.py | 2 +- openwfs/devices/slm/patch.py | 2 +- openwfs/devices/slm/shaders.py | 2 +- openwfs/devices/slm/slm.py | 4 +- openwfs/plot_utilities.py | 8 +- openwfs/simulation/microscope.py | 4 +- openwfs/simulation/slm.py | 2 +- openwfs/simulation/transmission.py | 4 +- openwfs/utilities/patterns.py | 4 +- openwfs/utilities/utilities.py | 2 +- tests/test_algorithms_troubleshoot.py | 4 +- tests/test_scanning_microscope.py | 28 +--- tests/test_simulation.py | 2 +- 30 files changed, 278 insertions(+), 182 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 97bb35f..d0d32a3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,4 +1,4 @@ -version: "2" +version: 2 build: os: "ubuntu-22.04" diff --git a/README.md b/README.md index d7a4e36..4109a04 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,80 @@ # What is wavefront shaping? -Wavefront shaping (WFS) is a technique for controlling the propagation of light in arbitrarily complex structures, including strongly scattering materials [[1](#id76)]. In WFS, a spatial light modulator (SLM) is used to shape the phase and/or amplitude of the incident light. With a properly constructed wavefront, light can be made to focus through [[2](#id57)], or inside [[3](#id46)] scattering materials; or light can be shaped to have other desired properties, such as optimal sensitivity for specific measurements [[4](#id47)], specialized point-spread functions [[5](#id33)], spectral filtering [[6](#id69)],, or for functions like optical trapping [[7](#id36)]. - -It stands out that an important driving force in WFS is the development of new algorithms, for example to account for sample movement [[8](#id35)], experimental conditions [[9](#id64)], to be optimally resilient to noise [[10](#id34)], or to use digital twin models to compute the required correction patterns [[11](#id56), [12](#id55), [13](#id72), [14](#id38)]. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed for specific towards the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns, or Fourier-based approaches [[15](#id51)]. Fast techniques that enable wavefront shaping in dynamic samples [[16](#id59), [17](#id60)], and many potential applications have been developed and prototyped, including endoscopy [[12](#id55)], optical trapping [[18](#id61)], Raman scattering, [[19](#id45)], and deep-tissue imaging [[20](#id62)]. Applications extend beyond that of microscope imaging such as optimizing photoelectrochemical absorption [[21](#id63)] and tuning random lasers [[22](#id68)]. - -With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. A recent c++ based contribution [[23](#id67)], highlights the growing need for software based tools that enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. +Wavefront shaping (WFS) is a technique for controlling the propagation of light in arbitrarily complex structures, +including strongly scattering materials [[1](#id76)]. In WFS, a spatial light modulator (SLM) is used to shape the phase +and/or amplitude of the incident light. With a properly constructed wavefront, light can be made to focus +through [[2](#id57)], or inside [[3](#id46)] scattering materials; or light can be shaped to have other desired +properties, such as optimal sensitivity for specific measurements [[4](#id47)], specialized point-spread +functions [[5](#id33)], spectral filtering [[6](#id69)],, or for functions like optical trapping [[7](#id36)]. + +It stands out that an important driving force in WFS is the development of new algorithms, for example to account for +sample movement [[8](#id35)], experimental conditions [[9](#id64)], to be optimally resilient to noise [[10](#id34)], or +to use digital twin models to compute the required correction +patterns [[11](#id56), [12](#id55), [13](#id72), [14](#id38)]. Much progress has been made towards developing fast and +noise-resilient algorithms, or algorithms designed for specific towards the methodology of wavefront shaping, such as +using algorithms based on Hadamard patterns, or Fourier-based approaches [[15](#id51)]. Fast techniques that enable +wavefront shaping in dynamic samples [[16](#id59), [17](#id60)], and many potential applications have been developed and +prototyped, including endoscopy [[12](#id55)], optical trapping [[18](#id61)], Raman scattering, [[19](#id45)], and +deep-tissue imaging [[20](#id62)]. Applications extend beyond that of microscope imaging such as optimizing +photoelectrochemical absorption [[21](#id63)] and tuning random lasers [[22](#id68)]. + +With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the +field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to +be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level +programming. A recent c++ based contribution [[23](#id67)], highlights the growing need for software based tools that +enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, +sharing and re-using code between different research groups is troublesome. # What is OpenWFS? -OpenWFS is a Python package for performing and for simulating wavefront shaping experiments. It aims to accelerate wavefront shaping research by providing: - -* **Hardware control**. Modular code for controlling spatial light modulators, cameras, and other hardware typically encountered in wavefront shaping experiments. Highlights include: - > * **Spatial light modulator**. The `SLM` object provides a versatile way to control spatial light modulators, allowing for software lookup tables, synchronization, texture warping, and multi-texture functionality accelerated by OpenGL. - > * **Scanning microscope**. The `ScanningMicroscope` object uses a National Instruments data acquisition card to control a laser-scanning microscope. - > * **GenICam cameras**. The `Camera` object uses the harvesters backend [[24](#id39)] to access any camera supporting the GenICam standard [[25](#id42)]. - > * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e. g. an SLM) and detectors (e. g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. -* **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, most algorithms can be implemented cleanly without hardware-specific programming. -* **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms, without the need for physical hardware. -* **Platform for exchange and joint collaboration**. OpenWFS can be used as a platform for sharing and exchanging wavefront shaping algorithms. The package is designed to be modular and easy to expand, and it is our hope that the community will contribute to the package by adding new algorithms, hardware control modules, and simulation tools. Python was specifically chosen for this purpose for its active community, high level of abstraction and the ease of sharing tools. Further expansion of the supported hardware is of high priority, especially wrapping c-based software support with tools like ctypes and the Micro-Manager based device adapters. -* **Platform for simplifying use of wavefront shaping**. OpenWFS is compatible with the recently developed PyDevice [], and can therefore be controlled from Micro-Manager [[26](#id65)], a commonly used microscopy control platform. -* **Automated troubleshooting**. OpenWFS provides tools for automated troubleshooting of wavefront shaping experiments. This includes tools for measuring the performance of wavefront shaping algorithms, and for identifying common problems such as incorrect SLM calibration, drift, measurement noise, and other experimental imperfections. +OpenWFS is a Python package for performing and for simulating wavefront shaping experiments. It aims to accelerate +wavefront shaping research by providing: + +* **Hardware control**. Modular code for controlling spatial light modulators, cameras, and other hardware typically + encountered in wavefront shaping experiments. Highlights include: + > * **Spatial light modulator**. The `SLM` object provides a versatile way to control spatial light modulators, + allowing for software lookup tables, synchronization, texture warping, and multi-texture functionality accelerated + by OpenGL. + > * **Scanning microscope**. The `ScanningMicroscope` object uses a National Instruments data acquisition card to + control a laser-scanning microscope. + > * **GenICam cameras**. The `Camera` object uses the harvesters backend [[24](#id39)] to access any camera supporting + the GenICam standard [[25](#id42)]. + > * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and + detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that + avoid the delay normally caused by the latency of the video card and SLM. +* **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the + hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a + result, most algorithms can be implemented cleanly without hardware-specific programming. +* **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, + including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid + prototyping and testing of new algorithms, without the need for physical hardware. +* **Platform for exchange and joint collaboration**. OpenWFS can be used as a platform for sharing and exchanging + wavefront shaping algorithms. The package is designed to be modular and easy to expand, and it is our hope that the + community will contribute to the package by adding new algorithms, hardware control modules, and simulation tools. + Python was specifically chosen for this purpose for its active community, high level of abstraction and the ease of + sharing tools. Further expansion of the supported hardware is of high priority, especially wrapping c-based software + support with tools like ctypes and the Micro-Manager based device adapters. +* **Platform for simplifying use of wavefront shaping**. OpenWFS is compatible with the recently developed PyDevice [], + and can therefore be controlled from Micro-Manager [[26](#id65)], a commonly used microscopy control platform. +* **Automated troubleshooting**. OpenWFS provides tools for automated troubleshooting of wavefront shaping experiments. + This includes tools for measuring the performance of wavefront shaping algorithms, and for identifying common problems + such as incorrect SLM calibration, drift, measurement noise, and other experimental imperfections. # Getting started -OpenWFS is available on the PyPI repository, and it can be installed with the command `pip install openwfs`. The latest documentation and the example code can be found on the [Read the Docs](https://openwfs.readthedocs.io/en/latest/) website [[27](#id66)]. To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 (not all dependencies were available for Python 3.12 yet). OpenWFS is developed and tested on Windows 11 and Manjaro Linux. +OpenWFS is available on the PyPI repository, and it can be installed with the command `pip install openwfs`. The latest +documentation and the example code can be found on the [Read the Docs](https://openwfs.readthedocs.io/en/latest/) +website [[27](#id66)]. To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS +is tested up to Python version 3.11 (not all dependencies were available for Python 3.12 yet). OpenWFS is developed and +tested on Windows 11 and Manjaro Linux. -[Listing 3.1](#hello-wfs) shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. +[Listing 3.1](#hello-wfs) shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This +example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial +light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. + ```python """ Hello wavefront shaping @@ -68,34 +114,65 @@ after = feedback.read() print(f"Intensity in the target increased from {before} to {after}") ``` -This example uses the StepwiseSequential wavefront shaping algorithm [[28](#id58)]. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a `SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field results.t, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. +This example uses the StepwiseSequential wavefront shaping algorithm [[28](#id58)]. The algorithm needs access to the +SLM for controlling the wavefront. This feedback is obtained from a `SingleRoi` object, which takes images from the +camera, and averages them over the specified circular region of interest. The algorithm returns the measured +transmission matrix in the field results.t, which is used to compute the optimal phase pattern to compensate the +aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase +pattern. -This code illustrates how OpenWFS separates the concerns of the hardware control (SLM and Camera), signal processing (SingleROIProcessor) and the algorithm itself (StepwiseSequential). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a `MultiRoiProcessor` object), using different algorithms, or different image sources, such as a `ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see `section-simulations`). +This code illustrates how OpenWFS separates the concerns of the hardware control (SLM and Camera), signal processing ( +SingleROIProcessor) and the algorithm itself (StepwiseSequential). A large variety of wavefront shaping experiments can +be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using +a `MultiRoiProcessor` object), using different algorithms, or different image sources, such as a `ScanningMicroscope`. +Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and +testing of new algorithms without direct access to wavefront shaping hardware (see `section-simulations`). # Analysis and troubleshooting -The principles of wavefront shaping are well established, and under close-to-ideal experimental conditions, it is possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the measurements from a wavefront shaping experiment. +The principles of wavefront shaping are well established, and under close-to-ideal experimental conditions, it is +possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can +negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the +measurements from a wavefront shaping experiment. -The `result` structure in [Listing 3.1](#hello-wfs), as returned by the wavefront shaping algorithm, was computed with the utility function `analyze_phase_stepping()`. This function extracts the transmission matrix from phase stepping measurements, and additionally computes a series of troubleshooting statistics in the form of a *fidelity*, which is a number that ranges from 0 (no sensible measurement possible) to 1 (perfect situation, optimal focus expected). These fidelities are: +The `result` structure in [Listing 3.1](#hello-wfs), as returned by the wavefront shaping algorithm, was computed with +the utility function `analyze_phase_stepping()`. This function extracts the transmission matrix from phase stepping +measurements, and additionally computes a series of troubleshooting statistics in the form of a *fidelity*, which is a +number that ranges from 0 (no sensible measurement possible) to 1 (perfect situation, optimal focus expected). These +fidelities are: * `fidelity_noise`: The fidelity reduction due to noise in the measurements. * `fidelity_amplitude`: The fidelity reduction due to unequal illumination of the SLM. * `fidelity_calibration`: The fidelity reduction due to imperfect phase response of the SLM. -If these fidelities are much lower than 1, this indicates a problem in the experiment, or a bug in the wavefront shaping experiment. For a comprehensive overview of the practical considerations in wavefront shaping and their effects on the fidelity, please see [[29](#id31)]. +If these fidelities are much lower than 1, this indicates a problem in the experiment, or a bug in the wavefront shaping +experiment. For a comprehensive overview of the practical considerations in wavefront shaping and their effects on the +fidelity, please see [[29](#id31)]. Further troubleshooting can be performed with the `troubleshoot()` function, which estimates the following fidelities: -* `fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e. g. due to reflection from the front surface of the SLM. +* `fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e.g. due to reflection from the front + surface of the SLM. * `fidelity_decorrelation`: The fidelity reduction due to decorrelation of the field during the measurement. -All fidelity estimations are combined to make an order of magnitude estimation of the expected enhancement. `troubleshoot()` returns a `WFSTroubleshootResult` object containing the outcome of the different tests and analyses, which can be printed to the console as a comprehensive troubleshooting report with the method `report()`. See `examples/troubleshooter_demo.py` for an example of how to use the automatic troubleshooter. +All fidelity estimations are combined to make an order of magnitude estimation of the expected +enhancement. `troubleshoot()` returns a `WFSTroubleshootResult` object containing the outcome of the different tests and +analyses, which can be printed to the console as a comprehensive troubleshooting report with the method `report()`. +See `examples/troubleshooter_demo.py` for an example of how to use the automatic troubleshooter. -Lastly, the `troubleshoot()` function computes several image frame metrics such as the *unbiased contrast to noise ratio* and *unbiased contrast enhancement*. These metrics are especially useful for scenarios where the contrast is expected to improve due to wavefront shaping, such as in multi-photon excitation fluorescence (multi-PEF) microscopy. Furthermore, `troubleshoot()` tests the image capturing repeatability and runs a stability test by capturing and comparing many frames over a longer period of time. +Lastly, the `troubleshoot()` function computes several image frame metrics such as the *unbiased contrast to noise +ratio* and *unbiased contrast enhancement*. These metrics are especially useful for scenarios where the contrast is +expected to improve due to wavefront shaping, such as in multi-photon excitation fluorescence (multi-PEF) microscopy. +Furthermore, `troubleshoot()` tests the image capturing repeatability and runs a stability test by capturing and +comparing many frames over a longer period of time. # Acknowledgements -We would like to thank Gerwin Osnabrugge, Bahareh Mastiani, Giulia Sereni, Siebe Meijer, Gijs Hannink, Merle van Gorsel, Michele Gintoli, Karina van Beek, Abhilash Thendiyammal, Lyuba Amitonova, and Tzu-Lun Wang for their contributions to earlier revisions of our wavefront shaping code. This work was supported by the European Research Council under the European Union’s Horizon 2020 Programme / ERC Grant Agreement n° [678919], and the Dutch Research Council (NWO) through Vidi grant number 14879. +We would like to thank Gerwin Osnabrugge, Bahareh Mastiani, Giulia Sereni, Siebe Meijer, Gijs Hannink, Merle van Gorsel, +Michele Gintoli, Karina van Beek, Abhilash Thendiyammal, Lyuba Amitonova, and Tzu-Lun Wang for their contributions to +earlier revisions of our wavefront shaping code. This work was supported by the European Research Council under the +European Union’s Horizon 2020 Programme / ERC Grant Agreement n° [678919], and the Dutch Research Council (NWO) through +Vidi grant number 14879. # Conflict of interest statement @@ -103,107 +180,142 @@ The authors declare no conflict of interest. 1 -Joel Kubby, Sylvain Gigan, and Meng Cui, editors. *Wavefront Shaping for Biomedical Imaging*. Advances in Microscopy and Microanalysis. Cambridge University Press, 2019. [doi:10.1017/9781316403938](https://doi.org/10.1017/9781316403938). +Joel Kubby, Sylvain Gigan, and Meng Cui, editors. *Wavefront Shaping for Biomedical Imaging*. Advances in Microscopy and +Microanalysis. Cambridge University Press, 2019. [doi:10.1017/9781316403938](https://doi.org/10.1017/9781316403938). 2 -Ivo M. Vellekoop and A. P. Mosk. Focusing coherent light through opaque strongly scattering media. *Opt. Lett.*, 32(16):2309–2311, Aug 2007. [doi:10.1364/OL.32.002309](https://doi.org/10.1364/OL.32.002309). +Ivo M. Vellekoop and A. P. Mosk. Focusing coherent light through opaque strongly scattering media. *Opt. Lett.*, 32(16): +2309–2311, Aug 2007. [doi:10.1364/OL.32.002309](https://doi.org/10.1364/OL.32.002309). 3 -Ivo M. Vellekoop, EG Van Putten, A Lagendijk, and AP Mosk. Demixing light paths inside disordered metamaterials. *Optics express*, 16(1):67–80, 2008. +Ivo M. Vellekoop, EG Van Putten, A Lagendijk, and AP Mosk. Demixing light paths inside disordered metamaterials. *Optics +express*, 16(1):67–80, 2008. 4 -Dorian Bouchet, Stefan Rotter, and Allard P Mosk. Maximum information states for coherent scattering measurements. *Nature Physics*, 17(5):564–568, 2021. +Dorian Bouchet, Stefan Rotter, and Allard P Mosk. Maximum information states for coherent scattering measurements. +*Nature Physics*, 17(5):564–568, 2021. 5 -Antoine Boniface et al. Transmission-matrix-based point-spread-function engineering through a complex medium. *Optica*, 4(1):54–59, 2017. +Antoine Boniface et al. Transmission-matrix-based point-spread-function engineering through a complex medium. *Optica*, +4(1):54–59, 2017. 6 -Jung-Hoon Park, ChungHyun Park, YongKeun Park, Hyunseung Yu, and Yong-Hoon Cho. Active spectral filtering through turbid media. *Optics Letters, Vol. 37, Issue 15, pp. 3261-3263*, 37:3261–3263, 8 2012. URL: [https://opg.optica.org/viewmedia.cfm?uri=ol-37-15-3261&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=ol-37-15-3261 https://opg.optica.org/ol/abstract.cfm?uri=ol-37-15-3261](https://opg.optica.org/viewmedia.cfm?uri=ol-37-15-3261&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=ol-37-15-3261 https://opg.optica.org/ol/abstract.cfm?uri=ol-37-15-3261), [doi:10.1364/OL.37.003261](https://doi.org/10.1364/OL.37.003261). +Jung-Hoon Park, ChungHyun Park, YongKeun Park, Hyunseung Yu, and Yong-Hoon Cho. Active spectral filtering through turbid +media. *Optics Letters, Vol. 37, Issue 15, pp. 3261-3263*, 37:3261–3263, 8 2012. +URL: [https://opg.optica.org/viewmedia.cfm?uri=ol-37-15-3261&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=ol-37-15-3261 https://opg.optica.org/ol/abstract.cfm?uri=ol-37-15-3261](https://opg.optica.org/viewmedia.cfm?uri=ol-37-15-3261&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=ol-37-15-3261 https://opg.optica.org/ol/abstract.cfm?uri=ol-37-15-3261), [doi:10.1364/OL.37.003261](https://doi.org/10.1364/OL.37.003261). 7 -Tomáš Čižmár, Michael Mazilu, and Kishan Dholakia. In situ wavefront correction and its application to micromanipulation. *Nature Photonics*, 4(6):388–394, 2010. +Tomáš Čižmár, Michael Mazilu, and Kishan Dholakia. In situ wavefront correction and its application to +micromanipulation. *Nature Photonics*, 4(6):388–394, 2010. 8 -Lorenzo Valzania and Sylvain Gigan. Online learning of the transmission matrix of dynamic scattering media. *Optica*, 10(6):708–716, 2023. +Lorenzo Valzania and Sylvain Gigan. Online learning of the transmission matrix of dynamic scattering media. *Optica*, +10(6):708–716, 2023. 9 -Benjamin R. Anderson, Ray Gunawidjaja, and Hergen Eilers. Effect of experimental parameters on optimal reflection of light from opaque media. *Physical Review A*, 93:013813, 1 2016. URL: [https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.013813](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.013813), [doi:10.1103/PHYSREVA.93.013813/FIGURES/12/MEDIUM](https://doi.org/10.1103/PHYSREVA.93.013813/FIGURES/12/MEDIUM). +Benjamin R. Anderson, Ray Gunawidjaja, and Hergen Eilers. Effect of experimental parameters on optimal reflection of +light from opaque media. *Physical Review A*, 93:013813, 1 2016. +URL: [https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.013813](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.013813), [doi:10.1103/PHYSREVA.93.013813/FIGURES/12/MEDIUM](https://doi.org/10.1103/PHYSREVA.93.013813/FIGURES/12/MEDIUM). 10 -Bahareh Mastiani and Ivo M Vellekoop. Noise-tolerant wavefront shaping in a hadamard basis. *Optics express*, 29(11):17534–17541, 2021. +Bahareh Mastiani and Ivo M Vellekoop. Noise-tolerant wavefront shaping in a hadamard basis. *Optics express*, 29(11): +17534–17541, 2021. 11 -PS Salter, M Baum, I Alexeev, M Schmidt, and MJ Booth. Exploring the depth range for three-dimensional laser machining with aberration correction. *Optics express*, 22(15):17644–17656, 2014. +PS Salter, M Baum, I Alexeev, M Schmidt, and MJ Booth. Exploring the depth range for three-dimensional laser machining +with aberration correction. *Optics express*, 22(15):17644–17656, 2014. 12 -Martin Plöschner, Tomáš Tyc, and Tomáš Čižmár. Seeing through chaos in multimode fibres. *Nature Photonics*, 9(8):529–535, 2015. +Martin Plöschner, Tomáš Tyc, and Tomáš Čižmár. Seeing through chaos in multimode fibres. *Nature Photonics*, 9(8): +529–535, 2015. 13 -Abhilash Thendiyammal, Gerwin Osnabrugge, Tom Knop, and Ivo M. Vellekoop. Model-based wavefront shaping microscopy. *Opt. Lett.*, 45(18):5101–5104, Sep 2020. [doi:10.1364/OL.400985](https://doi.org/10.1364/OL.400985). +Abhilash Thendiyammal, Gerwin Osnabrugge, Tom Knop, and Ivo M. Vellekoop. Model-based wavefront shaping microscopy. +*Opt. Lett.*, 45(18):5101–5104, Sep 2020. [doi:10.1364/OL.400985](https://doi.org/10.1364/OL.400985). 14 -DWS Cox, T Knop, and Ivo M. Vellekoop. Model-based aberration corrected microscopy inside a glass tube. *arXiv preprint arXiv:2311.13363*, 2023. +DWS Cox, T Knop, and Ivo M. Vellekoop. Model-based aberration corrected microscopy inside a glass tube. *arXiv preprint +arXiv:2311.13363*, 2023. 15 -Bahareh Mastiani, Gerwin Osnabrugge, and Ivo M. Vellekoop. Wavefront shaping for forward scattering. *Optics Express*, 30:37436, 10 2022. [doi:10.1364/oe.470194](https://doi.org/10.1364/oe.470194). +Bahareh Mastiani, Gerwin Osnabrugge, and Ivo M. Vellekoop. Wavefront shaping for forward scattering. *Optics Express*, +30:37436, 10 2022. [doi:10.1364/oe.470194](https://doi.org/10.1364/oe.470194). 16 -Yan Liu et al. Focusing light inside dynamic scattering media with millisecond digital optical phase conjugation. *Optica*, 4(2):280–288, Feb 2017. [doi:10.1364/OPTICA.4.000280](https://doi.org/10.1364/OPTICA.4.000280). +Yan Liu et al. Focusing light inside dynamic scattering media with millisecond digital optical phase conjugation. +*Optica*, 4(2):280–288, Feb 2017. [doi:10.1364/OPTICA.4.000280](https://doi.org/10.1364/OPTICA.4.000280). 17 -Omer Tzang et al. Wavefront shaping in complex media with a 350 khz modulator via a 1d-to-2d transform. *Nature Photonics*, 2019. [doi:10.1038/s41566-019-0503-6](https://doi.org/10.1038/s41566-019-0503-6). +Omer Tzang et al. Wavefront shaping in complex media with a 350 khz modulator via a 1d-to-2d transform. *Nature +Photonics*, 2019. [doi:10.1038/s41566-019-0503-6](https://doi.org/10.1038/s41566-019-0503-6). 18 -Tomáš Čižmár, Michael Mazilu, and Kishan Dholakia. In situ wavefront correction and its application to micromanipulation. *Nature Photonics*, 4:388–394, 05 2010. [doi:10.1038/nphoton.2010.85](https://doi.org/10.1038/nphoton.2010.85). +Tomáš Čižmár, Michael Mazilu, and Kishan Dholakia. In situ wavefront correction and its application to +micromanipulation. *Nature Photonics*, 4:388–394, 05 +2010. [doi:10.1038/nphoton.2010.85](https://doi.org/10.1038/nphoton.2010.85). 19 -Jonathan V. Thompson, Graham A. Throckmorton, Brett H. Hokr, and Vladislav V. Yakovlev. Wavefront shaping enhanced raman scattering in a turbid medium. *Optics letters*, 41:1769, 4 2016. URL: [https://pubmed.ncbi.nlm.nih.gov/27082341/](https://pubmed.ncbi.nlm.nih.gov/27082341/), [doi:10.1364/OL.41.001769](https://doi.org/10.1364/OL.41.001769). +Jonathan V. Thompson, Graham A. Throckmorton, Brett H. Hokr, and Vladislav V. Yakovlev. Wavefront shaping enhanced raman +scattering in a turbid medium. *Optics letters*, 41:1769, 4 2016. +URL: [https://pubmed.ncbi.nlm.nih.gov/27082341/](https://pubmed.ncbi.nlm.nih.gov/27082341/), [doi:10.1364/OL.41.001769](https://doi.org/10.1364/OL.41.001769). 20 -Lina Streich et al. High-resolution structural and functional deep brain imaging using adaptive optics three-photon microscopy. *Nature Methods 2021 18:10*, 18:1253–1258, 9 2021. [doi:10.1038/s41592-021-01257-6](https://doi.org/10.1038/s41592-021-01257-6). +Lina Streich et al. High-resolution structural and functional deep brain imaging using adaptive optics three-photon +microscopy. *Nature Methods 2021 18:10*, 18:1253–1258, 9 +2021. [doi:10.1038/s41592-021-01257-6](https://doi.org/10.1038/s41592-021-01257-6). 21 -Seng Fatt Liew, Sébastien M. Popoff, Stafford W. Sheehan, Arthur Goetschy, Charles A. Schmuttenmaer, A. Douglas Stone, and Hui Cao. Coherent control of photocurrent in a strongly scattering photoelectrochemical system. *ACS Photonics*, 3:449–455, 3 2016. URL: [https://technion-staging.elsevierpure.com/en/publications/coherent-control-of-photocurrent-in-a-strongly-scattering-photoel](https://technion-staging.elsevierpure.com/en/publications/coherent-control-of-photocurrent-in-a-strongly-scattering-photoel), [doi:10.1021/ACSPHOTONICS.5B00642](https://doi.org/10.1021/ACSPHOTONICS.5B00642). +Seng Fatt Liew, Sébastien M. Popoff, Stafford W. Sheehan, Arthur Goetschy, Charles A. Schmuttenmaer, A. Douglas Stone, +and Hui Cao. Coherent control of photocurrent in a strongly scattering photoelectrochemical system. *ACS Photonics*, 3: +449–455, 3 2016. +URL: [https://technion-staging.elsevierpure.com/en/publications/coherent-control-of-photocurrent-in-a-strongly-scattering-photoel](https://technion-staging.elsevierpure.com/en/publications/coherent-control-of-photocurrent-in-a-strongly-scattering-photoel), [doi:10.1021/ACSPHOTONICS.5B00642](https://doi.org/10.1021/ACSPHOTONICS.5B00642). 22 -Nicolas Bachelard, Sylvain Gigan, Xavier Noblin, and Patrick Sebbah. Adaptive pumping for spectral control of random lasers. *Nature Physics*, 10:426–431, 2014. URL: [https://ui.adsabs.harvard.edu/abs/2014NatPh..10..426B/abstract](https://ui.adsabs.harvard.edu/abs/2014NatPh..10..426B/abstract), [doi:10.1038/nphys2939](https://doi.org/10.1038/nphys2939). +Nicolas Bachelard, Sylvain Gigan, Xavier Noblin, and Patrick Sebbah. Adaptive pumping for spectral control of random +lasers. *Nature Physics*, 10:426–431, 2014. +URL: [https://ui.adsabs.harvard.edu/abs/2014NatPh..10..426B/abstract](https://ui.adsabs.harvard.edu/abs/2014NatPh..10..426B/abstract), [doi:10.1038/nphys2939](https://doi.org/10.1038/nphys2939). 23 -Benjamin R. Anderson, Andrew O’Kins, Kostiantyn Makrasnov, Rebecca Udby, Patrick Price, and Hergen Eilers. A modular gui-based program for genetic algorithm-based feedback-assisted wavefront shaping. *Journal of Physics: Photonics*, 6:045008, 8 2024. URL: [https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3 https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3/meta](https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3 https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3/meta), [doi:10.1088/2515-7647/AD6ED3](https://doi.org/10.1088/2515-7647/AD6ED3). +Benjamin R. Anderson, Andrew O’Kins, Kostiantyn Makrasnov, Rebecca Udby, Patrick Price, and Hergen Eilers. A modular +gui-based program for genetic algorithm-based feedback-assisted wavefront shaping. *Journal of Physics: Photonics*, 6: +045008, 8 2024. +URL: [https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3 https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3/meta](https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3 https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3/meta), [doi:10.1088/2515-7647/AD6ED3](https://doi.org/10.1088/2515-7647/AD6ED3). 24 -Rod Barman et al. Harvesters. URL: [https://github.com/genicam/harvesters](https://github.com/genicam/harvesters). +Rod Barman et al. Harvesters. URL: [https://github.com/genicam/harvesters](https://github.com/genicam/harvesters). 25 -GenICam - generic interface for cameras. URL: [https://www.emva.org/standards-technology/genicam/](https://www.emva.org/standards-technology/genicam/). +GenICam - generic interface for cameras. +URL: [https://www.emva.org/standards-technology/genicam/](https://www.emva.org/standards-technology/genicam/). 26 -Mark Tsuchida and Sam Griffin. Micro-manager project overview. URL: [https://micro-manager.org/Micro-Manager_Project_Overview](https://micro-manager.org/Micro-Manager_Project_Overview). +Mark Tsuchida and Sam Griffin. Micro-manager project overview. +URL: [https://micro-manager.org/Micro-Manager_Project_Overview](https://micro-manager.org/Micro-Manager_Project_Overview). 27 @@ -211,8 +323,11 @@ OpenWFS documentation. URL: [https://openwfs.readthedocs.io/en/latest/](https:// 28 -Ivo M. Vellekoop and AP Mosk. Phase control algorithms for focusing light through turbid media. *Optics communications*, 281(11):3071–3080, 2008. +Ivo M. Vellekoop and AP Mosk. Phase control algorithms for focusing light through turbid media. *Optics communications*, +281(11):3071–3080, 2008. 29 -Bahareh Mastiani, Daniël W. S. Cox, and Ivo M. Vellekoop. Practical considerations for high-fidelity wavefront shaping experiments. http://arxiv.org/abs/2403.15265, March 2024. [arXiv:2403.15265](https://arxiv.org/abs/2403.15265), [doi:10.48550/arXiv.2403.15265](https://doi.org/10.48550/arXiv.2403.15265). +Bahareh Mastiani, Daniël W. S. Cox, and Ivo M. Vellekoop. Practical considerations for high-fidelity wavefront shaping +experiments. http://arxiv.org/abs/2403.15265, March +2024. [arXiv:2403.15265](https://arxiv.org/abs/2403.15265), [doi:10.48550/arXiv.2403.15265](https://doi.org/10.48550/arXiv.2403.15265). diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 62ef254..f5a9eeb 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -6,8 +6,12 @@ # General -- The package `black` is used to ensure correct formatting. Install with `pip install black` and run in the terminal - using `black .` when located at the root of the repository. +- The package `black` is used to ensure correct formatting. + When using PyCharm, just install black through the settings dialog. +- PyCharm warnings and errors should be fixed. Exceptions: + - PEP 8: E501 line too long. May be disabled. This is already checked by black. For docstrings, keeping a string + line limit can be very cumbersome. + - PEP 8:E203 whitespace before ':'. May be disabled. This is already checked by (and conflicts with) black. # Tests diff --git a/docs/source/conf.py b/docs/source/conf.py index 9c927d7..7b7e635 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -49,7 +49,8 @@ \author[1]{Tom~Knop} \author[1,2]{Harish~Sasikumar} \author[1]{Ivo~M.~Vellekoop} - \affil[1]{University of Twente, Biomedical Photonic Imaging, TechMed Institute, P. O. Box 217, 7500 AE Enschede, The Netherlands} + \affil[1]{University of Twente, Biomedical Photonic Imaging, TechMed Institute, P. O. Box 217, + 7500 AE Enschede, The Netherlands} \affil[2]{Imec (Netherlands), Holst Centre (HTC-31), 5656 AE, Eindhoven, The Netherlands} \publishers{% \normalfont\normalsize% @@ -128,11 +129,11 @@ autodoc_mock_imports = ["PyOpenGL", "OpenGL"] -## Hide some classes that are not production ready yet -def skip(app, what, name, obj, skip, options): +# Hide some classes that are not production ready yet +def skip(app, what, name, obj, do_skip, options): if name in ("WFSController", "Gain"): return True - return skip + return do_skip def visit_citation(self, node): diff --git a/docs/source/core.rst b/docs/source/core.rst index 3e585c2..7b9f956 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -27,7 +27,7 @@ Detectors in OpenWFS are objects that capture, generate, or process data. All de def coordinates(dimension: int) -> Quantity -The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as `numpy` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e. g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`.~Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. +The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as `numpy` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e.g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`.~Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. The detector object inherits some properties and methods from the base class :class:`~.Device`. These are used by the synchronization mechanism to determine when it is safe to start a measurement, as described in :numref:`device-synchronization`. @@ -88,7 +88,7 @@ OpenWFS consistently uses `astropy.units` :cite:`astropy` for quantities with ph c.shutter_time = 0.01 * u.s # equivalent to the previous line c.shutter_time = 10 # raises an error, since the unit is missing -In addition, OpenWFS allows attaching pixel-size metadata to data arrays using the functions :func:`~.set_pixel_size()`. Pixel sizes can represent a physical length (e. g. as in the size pixels on an image sensor), or other units such as time (e. g. as the sampling period in a time series). OpenWFS fully supports anisotropic pixels, where the pixel sizes in the x and y directions are different. +In addition, OpenWFS allows attaching pixel-size metadata to data arrays using the functions :func:`~.set_pixel_size()`. Pixel sizes can represent a physical length (e.g. as in the size pixels on an image sensor), or other units such as time (e.g. as the sampling period in a time series). OpenWFS fully supports anisotropic pixels, where the pixel sizes in the x and y directions are different. The data arrays returned by the :meth:`~.Detector.read()` function of a detector have `pixel_size` metadata attached whenever appropriate. The pixel size can be retrieved from the array using :func:`~.get_pixel_size()`, or obtained from the :attr:`~.Detector.pixel_size` attribute directly. As an alternative to accessing the pixel size directly, :func:`~get_extent()` and :class:`~.Detector.extent` provide access to the extent of the array, which is always equal to the pixel size times the shape of the array. Finally, the convenience function :meth:`~.Detector.coordinates` returns a vector of coordinates with appropriate units along a specified dimension of the array. diff --git a/docs/source/development.rst b/docs/source/development.rst index 7b57fcb..69115c3 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -70,11 +70,11 @@ If the detector is created with the flag ``multi_threaded = True``, then `_fetch Implementing a processor ++++++++++++++++++++++++++++++++++ -To implement a data processing step that dynamically processes date from one or more input detectors, implement a custom processor. This is done by deriving from the `Processor` base class and implementing the `__init__` function. This function should pass a list of all upstream nodes, i. e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth"`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls `_fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. +To implement a data processing step that dynamically processes date from one or more input detectors, implement a custom processor. This is done by deriving from the `Processor` base class and implementing the `__init__` function. This function should pass a list of all upstream nodes, i.e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth"`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls `_fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. Implementing an actuator +++++++++++++++++++++++++++++++ -To implement an actuator, the user should subclass the `Actuator` base class, and implement whatever properties and logic appropriate to the device. All methods that start the actuator (e. g. `update()` or `move()`), should first call `self._start()` to request a state switch to the `moving` state. As for detectors, actuators should either specify a static `duration` and `latency` if known, or override these properties to return run-time values for the duration and latency. Similarly, if the duration of an action of the actuator is not known in advance, the class should override `busy` to poll for the action to complete. +To implement an actuator, the user should subclass the `Actuator` base class, and implement whatever properties and logic appropriate to the device. All methods that start the actuator (e.g. `update()` or `move()`), should first call `self._start()` to request a state switch to the `moving` state. As for detectors, actuators should either specify a static `duration` and `latency` if known, or override these properties to return run-time values for the duration and latency. Similarly, if the duration of an action of the actuator is not known in advance, the class should override `busy` to poll for the action to complete. diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 833bfeb..5b8431b 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -29,7 +29,7 @@ OpenWFS is a Python package for performing and for simulating wavefront shaping * **Spatial light modulator**. The :class:`~.slm.SLM` object provides a versatile way to control spatial light modulators, allowing for software lookup tables, synchronization, texture warping, and multi-texture functionality accelerated by OpenGL. * **Scanning microscope**. The :class:`~.devices.ScanningMicroscope` object uses a National Instruments data acquisition card to control a laser-scanning microscope. * **GenICam cameras**. The :class:`~.devices.Camera` object uses the `harvesters` backend :cite:`harvesters` to access any camera supporting the GenICam standard :cite:`genicam`. - * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e. g. an SLM) and detectors (e. g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. + * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. * **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, most algorithms can be implemented cleanly without hardware-specific programming. @@ -78,7 +78,7 @@ If these fidelities are much lower than 1, this indicates a problem in the exper Further troubleshooting can be performed with the :func:`~.troubleshoot` function, which estimates the following fidelities: -* :attr:`~.WFSTroubleshootResult.fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e. g. due to reflection from the front surface of the SLM. +* :attr:`~.WFSTroubleshootResult.fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e.g. due to reflection from the front surface of the SLM. * :attr:`~.WFSTroubleshootResult.fidelity_decorrelation`: The fidelity reduction due to decorrelation of the field during the measurement. All fidelity estimations are combined to make an order of magnitude estimation of the expected enhancement. :func:`~.troubleshoot` returns a ``WFSTroubleshootResult`` object containing the outcome of the different tests and analyses, which can be printed to the console as a comprehensive troubleshooting report with the method :meth:`~.WFSTroubleshootResult.report()`. See ``examples/troubleshooter_demo.py`` for an example of how to use the automatic troubleshooter. diff --git a/docs/source/slms.rst b/docs/source/slms.rst index a3043f1..b229bc5 100644 --- a/docs/source/slms.rst +++ b/docs/source/slms.rst @@ -14,7 +14,7 @@ The :meth:`~.PhaseSLM.set_phases()` method takes a scalar or a 2-D array of phas Currently, there are two implementations of the `PhaseSLM` interface. The :class:`simulation.SLM` is used for simulating experiments and for testing algorithms (see :numref:`section-simulations`). The :class:`hardware.SLM` is an OpenGL-accelerated controller for using a phase-only SLM that is connected to the video output of a computer. The SLM can be created in windowed mode (useful for debugging), or full screen. It is possible to have multiple windowed SLMs on the same monitor, but only one full-screen SLM per monitor. In addition, the SLM implements some advanced features that are discussed below. -At the time of writing, SLMs that are controlled through other interfaces than the video output are not supported. However, the interface of the `PhaseSLM` class is designed to accommodate these devices in the future. Through this interface, support for intensity-only light modulators (e. g. Digital Mirror Devices) operating in phase-modulation mode (e. g. :cite:`conkey2012high`) may also be added. +At the time of writing, SLMs that are controlled through other interfaces than the video output are not supported. However, the interface of the `PhaseSLM` class is designed to accommodate these devices in the future. Through this interface, support for intensity-only light modulators (e.g. Digital Mirror Devices) operating in phase-modulation mode (e.g. :cite:`conkey2012high`) may also be added. Texture mapping and blending ----------------------------------- diff --git a/examples/slm_demo.py b/examples/slm_demo.py index bbb64c9..7231e3b 100644 --- a/examples/slm_demo.py +++ b/examples/slm_demo.py @@ -1,7 +1,7 @@ """ SLM Demo ======== -Example on how different geometries and patches work for an SLM. Currently uses SLM number 0, which is the left +Example on how different geometries and patches work for an SLM. Currently, uses SLM number 0, which is the left upper corner of the primary monitor. EPILEPSY WARNING: YOUR PRIMARY SCREEN MAY QUICKLY FLASH DURING RUNNING THIS FILE diff --git a/openwfs/algorithms/basic_fourier.py b/openwfs/algorithms/basic_fourier.py index 822bc0e..d7084fc 100644 --- a/openwfs/algorithms/basic_fourier.py +++ b/openwfs/algorithms/basic_fourier.py @@ -14,11 +14,12 @@ class FourierDualReference(DualReference): Improvements over [1]: - The set of plane waves is taken from a disk in k-space instead of a square. - - No overlap between the two halves is needed, instead the final stitching step is done using measurements already in the data set. - - When only a single target is optimized, the algorithm can be used in an iterative version to increase SNR during the measurument, - similar to [2]. + - No overlap between the two halves is needed, instead the final stitching step is done + using measurements already in the data set. + - When only a single target is optimized, the algorithm can be used in an iterative version + to increase SNR during the measurement, similar to [2]. - [1]: Bahareh Mastiani, Gerwin Osnabrugge, and Ivo M. Vellekoop, + [1]: Bahareh Mastiani, Gerwin Osnabrugge, and Ivo M. Vellekoop, "Wavefront shaping for forward scattering," Opt. Express 30, 37436-37445 (2022) [2]: X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive @@ -89,8 +90,8 @@ def _update_modes(self): modes = np.zeros((*self._slm_shape, len(k)), dtype=np.float32) for i, k_i in enumerate(k): # tilt generates a pattern from -2.0 to 2.0 (The convention for Zernike modes normalized to an RMS of 1). - # The natural step to take is the Abbe diffraction limit of the modulated part, which corresponds to a gradient - # from -π to π over the modulated part. + # The natural step to take is the Abbe diffraction limit of the modulated part, + # which corresponds to a gradient from -π to π over the modulated part. modes[..., i] = tilt(self._slm_shape, g=k_i * 0.5 * np.pi) self.phase_patterns = (modes, modes) diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index bffcff9..19c788e 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -17,8 +17,8 @@ class DualReference: A and B, as indicated by the boolean group_mask argument. The algorithm first keeps the pixels in group B fixed, and displays a sequence on patterns on the pixels of group A. It uses these measurements to construct an optimized wavefront that is displayed on the pixels of group A. - This process is then repeated for the pixels of group B, now using the *optimized* wavefront on group A as reference. - Optionally, the process can be repeated for a number of iterations, which each iteration using the current correction + This process is then repeated for the pixels of group B, now using the *optimized* wavefront on group A as + reference. Optionally, the process can be repeated for a number of iterations, which each iteration using the current correction pattern as a reference. This makes this algorithm suitable for non-linear feedback, such as multi-photon excitation fluorescence [Osnabrugge2019]_. @@ -27,12 +27,12 @@ class DualReference: References ---------- - .. [Tao2017] X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive - optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). + .. [Tao2017] X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate + adaptive optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). - .. [Osnabrugge2019] Gerwin Osnabrugge, Lyubov V. Amitonova, and Ivo M. Vellekoop. "Blind focusing through strongly scattering media - using wavefront shaping with nonlinear feedback", Optics Express, 27(8):11673–11688, 2019. - https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 + .. [Osnabrugge2019] Gerwin Osnabrugge, Lyubov V. Amitonova, and Ivo M. Vellekoop. "Blind focusing through strongly + scattering media using wavefront shaping with nonlinear feedback", Optics Express, 27(8):11673–11688, 2019. + https://opg.optica.org/oe/ abstract.cfm?uri=oe-27-8-1167 """ @@ -53,9 +53,11 @@ def __init__( Args: feedback: The feedback source, usually a detector that provides measurement data. slm: Spatial light modulator object. - phase_patterns: A tuple of two 3D arrays, containing the phase patterns for group A and group B, respectively. + phase_patterns: + A tuple of two 3D arrays, containing the phase patterns for group A and group B, respectively. The first two dimensions are the spatial dimensions, and should match the size of group_mask. - The 3rd dimension in the array is index of the phase pattern. The number of phase patterns in A and B may be different. + The 3rd dimension in the array is index of the phase pattern. + The number of phase patterns in A and B may be different. When None, the phase_patterns attribute must be set before executing the algorithm. amplitude: Tuple of 2D arrays, one array for each group. The arrays have shape equal to the shape of group_mask. When None, the amplitude attribute must be set before executing the algorithm. When @@ -68,14 +70,19 @@ def __init__( non-linear feedback and the SNR, more might be required. iterations: Number of times to optimize a mode set, e.g. when iterations = 5, the measurements are A, B, A, B, A. - optimized_reference: When `True`, during each iteration the other half of the SLM displays the optimized pattern so far (as in [1]). - When `False`, the algorithm optimizes A with a flat wavefront on B, and then optimizes B with a flat wavefront on A. - This mode also allows for multi-target optimization, where the algorithm optimizes multiple targets in parallel. + optimized_reference: + When `True`, during each iteration the other half of the SLM displays the optimized pattern so far (as in [1]). + When `False`, the algorithm optimizes A with a flat wavefront on B, + and then optimizes B with a flat wavefront on A. + This mode also allows for multi-target optimization, + where the algorithm optimizes multiple targets in parallel. The two halves are then combined (stitched) to form the full transmission matrix. - In this mode, it is essential that both A and B include a flat wavefront as mode 0. The measurement for - mode A0 and for B0 both give contain relative phase between group A and B, so there is a slight redundancy. + In this mode, it is essential that both A and B include a flat wavefront as mode 0. + The measurement for mode A0 and for B0 both give contain relative phase between group A and B, + so there is a slight redundancy. These two measurements are combined to find the final phase for stitching. - When set to `None` (default), the algorithm uses True if there is a single target, and False if there are multiple targets. + When set to `None` (default), the algorithm uses True if there is a single target, + and False if there are multiple targets. analyzer: The function used to analyze the phase stepping data. Must return a WFSResult object. Defaults to `analyze_phase_stepping` diff --git a/openwfs/algorithms/genetic.py b/openwfs/algorithms/genetic.py index c59b677..10426bb 100644 --- a/openwfs/algorithms/genetic.py +++ b/openwfs/algorithms/genetic.py @@ -33,7 +33,7 @@ class SimpleGenetic: ---------- [^1]: Conkey D B, Brown A N, Caravaca-Aguirre A M and Piestun R 'Genetic algorithm optimization for focusing through turbid media in noisy environments' Opt. Express 20 4840–9 (2012). - [^2]: Benjamin R Anderson et al 'A modular GUI-based program for genetic algorithm-based + [^2]: Benjamin R Anderson et al. 'A modular GUI-based program for genetic algorithm-based feedback-assisted wavefront shaping', J. Phys. Photonics 6 045008 (2024). """ diff --git a/openwfs/algorithms/troubleshoot.py b/openwfs/algorithms/troubleshoot.py index 4a82a15..4c8c5fc 100644 --- a/openwfs/algorithms/troubleshoot.py +++ b/openwfs/algorithms/troubleshoot.py @@ -8,6 +8,7 @@ from ..core import Detector, PhaseSLM +# TODO: review, replace by numpy/scipy functions where possible, remove or hide functions that are too specific def signal_std(signal_with_noise: np.ndarray, noise: np.ndarray) -> float: """ Compute noise corrected standard deviation of signal measurement. @@ -39,7 +40,7 @@ def cnr(signal_with_noise: np.ndarray, noise: np.ndarray) -> np.float64: ND array containing the measured signal including noise. The noise is assumed to be uncorrelated with the signal, such that var(measured) = var(signal) + var(noise). noise: - ND array containing only noise, e. g. a dark frame. + ND array containing only noise, e.g. a dark frame. Returns: Standard deviation of the signal, corrected for the variance due to given noise. @@ -54,9 +55,9 @@ def contrast_enhancement(signal_with_noise: np.ndarray, reference_with_noise: np Args: signal_with_noise: - ND array containing the measured signal including noise, e. g. image signal with shaped wavefront. + ND array containing the measured signal including noise, e.g. image signal with shaped wavefront. reference_with_noise: - ND array containing a reference signal including noise, e. g. image signal with a flat wavefront. + ND array containing a reference signal including noise, e.g. image signal with a flat wavefront. noise: ND array containing only noise. @@ -95,7 +96,7 @@ def find_pixel_shift(f: np.ndarray, g: np.ndarray) -> tuple[int, ...]: def field_correlation(a: np.ndarray, b: np.ndarray) -> float: """ - Compute field correlation, i. e. inner product of two fields, normalized by the product of the L2 norms, + Compute field correlation, i.e. inner product of two fields, normalized by the product of the L2 norms, such that field_correlation(f, s*f) == 1, where s is a scalar value. Also known as normalized first order correlation :math:`g_1`. @@ -137,8 +138,8 @@ def pearson_correlation(a: np.ndarray, b: np.ndarray, noise_var: np.ndarray = 0. a_dev = a - a.mean() # Deviations from mean a b_dev = b - b.mean() # Deviations from mean b covar = (a_dev * b_dev).mean() # Covariance - a_var_signal = a.var() - noise_var # Variance of signal in a, excluding noise - b_var_signal = b.var() - noise_var # Variance of signal in b, excluding noise + a_var_signal = a.var() - noise_var # Variance of signal in ``a``, excluding noise + b_var_signal = b.var() - noise_var # Variance of signal in ``b``, excluding noise return covar / np.sqrt(a_var_signal * b_var_signal) @@ -542,9 +543,9 @@ def troubleshoot( Args: measure_non_modulated_phase_steps: - algorithm: Wavefront Shaping algorithm object, e. g. StepwiseSequential. + algorithm: Wavefront Shaping algorithm object, e.g. StepwiseSequential. background_feedback: Feedback source that determines average background speckle intensity. - frame_source: Source object for reading frames, e. g. Camera. + frame_source: Source object for reading frames, e.g. Camera. shutter: Device object that can block/unblock light source. do_frame_capture: Boolean. If False, skip frame capture before and after running the WFS algorithm. Also skips computation of corresponding metrics. Also skips stability test. diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 3fa72e3..235cd35 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -12,9 +12,9 @@ class WFSResult: Attributes: t (ndarray): Measured transmission matrix. If multiple targets were used, the first dimension(s) of `t` denote the columns of the transmission matrix (`a` indices), and the last dimensions(s) denote the targets, - i. e., the rows of the transmission matrix (`b` indices). + i.e., the rows of the transmission matrix (`b` indices). axis (int): Number of dimensions used for denoting a single column of the transmission matrix - (e. g., 2 dimensions representing the x and y coordinates of the SLM pixels). + (e.g., 2 dimensions representing the x and y coordinates of the SLM pixels). fidelity_noise (ndarray): The estimated loss in fidelity caused by the limited SNR (for each target). fidelity_amplitude (ndarray): Estimated reduction of the fidelity due to phase-only modulation (for each target) (≈ π/4 for fully developed speckle). @@ -42,7 +42,7 @@ def __init__( t(ndarray): measured transmission matrix. axis(int): number of dimensions used for denoting a single columns of the transmission matrix - (e. g. 2 dimensions representing the x and y coordinates of the SLM pixels) + (e.g. 2 dimensions representing the x and y coordinates of the SLM pixels) fidelity_noise(ArrayLike): the estimated loss in fidelity caused by the the limited snr (for each target). fidelity_amplitude(ArrayLike): @@ -82,7 +82,7 @@ def __str__(self) -> str: noise_warning = "OK" if self.fidelity_noise > 0.5 else "WARNING low signal quality." amplitude_warning = "OK" if self.fidelity_amplitude > 0.5 else "WARNING uneven contribution of optical modes." calibration_fidelity_warning = ( - "OK" if self.fidelity_calibration > 0.5 else ("WARNING non-linear phase response, check " "lookup table.") + "OK" if self.fidelity_calibration > 0.5 else "WARNING non-linear phase response, check " "lookup table." ) return f""" Wavefront shaping results: @@ -144,7 +144,7 @@ def weighted_average(attribute): ) -def analyze_phase_stepping(measurements: np.ndarray, axis: int, A: Optional[float] = None): +def analyze_phase_stepping(measurements: np.ndarray, axis: int): """Analyzes the result of phase stepping measurements, returning matrix `t` and noise statistics This function assumes that all measurements were made using the same reference field `A` @@ -159,9 +159,6 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int, A: Optional[floa and the last zero or more dimensions corresponding to the individual targets where the feedback was measured. axis(int): indicates which axis holds the phase steps. - A(Optional[float]): magnitude of the reference field. - This value is used to correctly normalize the returned transmission matrix. - When missing, the value of `A` is estimated from the measurements. With `phase_steps` phase steps, the measurements are given by @@ -187,16 +184,7 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int, A: Optional[floa segments = tuple(range(axis)) # Fourier transform the phase stepping measurements - t_f_raw = np.fft.fft(measurements, axis=axis) / phase_steps - - if A is None: # reference field strength not known: estimate from data - t_abs = np.abs(np.take(t_f_raw, 1, axis=axis)) - offset = np.take(t_f_raw, 0, axis=axis) - a_plus_b = np.sqrt(offset + 2.0 * t_abs) - a_minus_b = np.sqrt(offset - 2.0 * t_abs) - A = 0.5 * np.mean(a_plus_b + a_minus_b) - - t_f = t_f_raw / A + t_f = np.fft.fft(measurements, axis=axis) / phase_steps t = np.take(t_f, 1, axis=axis) # compute the effect of amplitude variations. diff --git a/openwfs/core.py b/openwfs/core.py index 570622b..36ef6f5 100644 --- a/openwfs/core.py +++ b/openwfs/core.py @@ -58,8 +58,8 @@ def _start(self): This function changes the global state to 'moving' or 'measuring' if needed, and it may block until this state switch is completed. - After switching, stores the time at which the operation will have ended in the `_end_time_ns` - field (i. e., `time.time_ns() + self.latency + self.duration`). + After switching, stores the time at which the operation will have ended in the ``_end_time_ns`` + field (i.e., ``time.time_ns() + self.latency + self.duration``). """ # acquire a global lock, to prevent multiple threads to switch moving/measuring state simultaneously @@ -132,7 +132,7 @@ def duration(self) -> Quantity[u.ms]: )` and the stabilization of the device. If the duration of an operation is not known in advance, - (e. g., when waiting for a hardware trigger), this function should return `np.inf * u.ms`. + (e.g., when waiting for a hardware trigger), this function should return `np.inf * u.ms`. Note: A device may update the duration dynamically. For example, a stage may compute the required time to @@ -144,7 +144,7 @@ def duration(self) -> Quantity[u.ms]: return self._duration def wait(self, up_to: Optional[Quantity[u.ms]] = None) -> None: - """Waits until the device is (almost) in the `ready` state, i. e., has finished measuring or moving. + """Waits until the device is (almost) in the `ready` state, i.e., has finished measuring or moving. This function is called by `_start` automatically to ensure proper synchronization between detectors and actuators, and it is called by `__del__` to ensure the device is not active when it is destroyed. @@ -163,7 +163,7 @@ def wait(self, up_to: Optional[Quantity[u.ms]] = None) -> None: *before* the device is finished. Raises: - Any other exception raised by the device in another thread (e. g., during `_fetch`). + Any other exception raised by the device in another thread (e.g., during `_fetch`). TimeoutError: if the device has `duration = ∞`, and `busy` does not return `True` within `self.timeout` RuntimeError: if `wait` is called from inside a setter or from inside `_fetch`. @@ -260,7 +260,7 @@ def __init__( Subclassed can override the `pixel_size` property to return the actual pixel size. duration: The maximum amount of time that elapses between returning from `trigger()` and the end of the measurement. If the duration of an operation is not known in advance, - (e. g., when waiting for a hardware trigger), this value should be `np.inf * u.ms` + (e.g., when waiting for a hardware trigger), this value should be `np.inf * u.ms` and the `busy` method should be overridden to return `False` when the measurement is finished. If None is passed, the subclass should override the `duration` property to return the actual duration. latency: The minimum amount of time between sending a command or trigger to the device @@ -299,7 +299,8 @@ def wait(self, up_to: Quantity[u.ms] = None) -> None: explicitly when waiting for data to be stored in the `out` argument of :meth:`~.Detector.trigger()`. Args: - up_to: if specified, this function may return `up_to` milliseconds *before* the hardware has finished measurements. + up_to: if specified, this function may return `up_to` milliseconds *before* the hardware + has finished measurements. If None, this function waits until the hardware has finished all measurements *and* all data is fetched, and stored in the `out` array if that was passed to trigger(). @@ -315,8 +316,8 @@ def trigger(self, *args, out=None, immediate=False, **kwargs) -> Future: """Triggers the detector to start acquisition of the data. This function does not wait for the measurement to complete. - Instead, it returns a `concurrent.futures.Future`.. - Call `.result()` on the returned object to wait for the data. + Instead, it returns a ``concurrent.futures.Future``. + Call ``.result()`` on the returned object to wait for the data. Here is a typical usage pattern: .. code-block:: python @@ -486,13 +487,13 @@ def coordinates(self, dimension: int) -> Quantity: """Returns an array with the coordinate values along the d-th axis. The coordinates represent the _centers_ of the grid points. For example, - for an array of shape `(2,)` the coordinates are `[0.5, 1.5] * pixel_size` + for an array of shape ``(2,)`` the coordinates are `[0.5, 1.5] * pixel_size` and not `[0, 1] * pixel_size`. If `self.pixel_size is None`, a pixel size of 1.0 is used. The coordinates are returned as an array with the same number of dimensions as `data_shape`, with the d-th dimension holding the coordinates. - This facilitates meshgrid-like computations, e. g. + This facilitates meshgrid-like computations, e.g. `cam.coordinates(0) + cam.coordinates(1)` gives a 2-dimensional array of coordinates. Args: @@ -521,7 +522,7 @@ class Processor(Detector, ABC): """Base class for all Processors. Processors can be used to build data processing graphs, where each Processor takes input from one or - more input Detectors and processes that data (e. g., cropping an image, averaging over an ROI, etc.). + more input Detectors and processes that data (e.g., cropping an image, averaging over an ROI, etc.). A processor, itself, is a Detector to allow chaining multiple processors together to combine functionality. To implement a processor, implement `_fetch`, and optionally override `data_shape`, `pixel_size`, and `__init__`. @@ -569,7 +570,7 @@ def latency(self) -> Quantity[u.ms]: @property def duration(self) -> Quantity[u.ms]: """Returns the last end time minus the first start time for all detectors - i. e., max (duration + latency) - min(latency). + i.e., max (duration + latency) - min(latency). Note that `latency` is allowed to vary over time for devices that can only be triggered periodically, so this `duration` may also vary over time. diff --git a/openwfs/devices/camera.py b/openwfs/devices/camera.py index 0030e90..bbc6c6b 100644 --- a/openwfs/devices/camera.py +++ b/openwfs/devices/camera.py @@ -32,7 +32,7 @@ class Camera(Detector): The node map should not be used to set properties that are available as properties in the Camera object, such as `duration` (exposure time), `width`, `height`, `binning`, etc. - Also, the node map should not be used to set properties while the camera is fetching a frame (i. e., + Also, the node map should not be used to set properties while the camera is fetching a frame (i.e., between `trigger()` and calling `result()` on the returned concurrent.futures.Future object). Note: diff --git a/openwfs/devices/galvo_scanner.py b/openwfs/devices/galvo_scanner.py index 1fe513f..23f8811 100644 --- a/openwfs/devices/galvo_scanner.py +++ b/openwfs/devices/galvo_scanner.py @@ -163,7 +163,7 @@ def scan(self, start: float, stop: float, sample_count: int, sample_rate: Quanti The launch point and landing point are returned along with the scan sequence. This function also returns a slice object, which represents the part of the sequence - that corresponds to a linear movement from start to stop. `slice.stop - slice.start = sample_count`. + that corresponds to a linear movement from start to stop. ``slice.stop - slice.start = sample_count``. The scan follows the coordinate convention used throughout OpenWFS and Astropy, where the coordinates correspond to the centers of the pixels. @@ -221,7 +221,7 @@ def compute_scale( Args: optical_deflection (Quantity[u.deg/u.V]): - The optical deflection (i. e. twice the mechanical angle) of the mirror + The optical deflection (i.e. twice the mechanical angle) of the mirror as a function of applied voltage. galvo_to_pupil_magnification (float): The magnification of the relay system between the galvo mirrors and the pupil. @@ -256,7 +256,7 @@ def compute_acceleration( Args: optical_deflection (Quantity[u.deg/u.V]): - The optical deflection (i. e. twice the mechanical angle) of the mirror + The optical deflection (i.e. twice the mechanical angle) of the mirror as a function of applied voltage. torque_constant (Quantity[u.N*u.m/u.A]): The torque constant of the galvo mirror driving coil. @@ -676,7 +676,7 @@ def pixel_size(self) -> Quantity: @property def duration(self) -> Quantity[u.ms]: """Total duration of scanning for one frame.""" - self._ensure_valid() # make sure _scan_pattern is up to date + self._ensure_valid() # make sure _scan_pattern is up-to-date return (self._scan_pattern.shape[1] / self._sample_rate).to(u.ms) @property diff --git a/openwfs/devices/slm/geometry.py b/openwfs/devices/slm/geometry.py index 560257d..3e492b7 100644 --- a/openwfs/devices/slm/geometry.py +++ b/openwfs/devices/slm/geometry.py @@ -23,7 +23,7 @@ class Geometry: To start a new triangle strip, insert the special index 0xFFFF into the index array. (tx, ty) are the texture coordinates that determine which pixel of the texture - (e. g. the array passed to `set_phases`) is drawn at each vertex. + (e.g. the array passed to `set_phases`) is drawn at each vertex. For each triangle, the screen coordinates (x,y) define a triangle on the screen, whereas the texture coordinates (tx, ty) define a triangle in the texture. OpenGL maps the texture triangle onto the screen triangle, using linear interpolation of the coordinates between diff --git a/openwfs/devices/slm/patch.py b/openwfs/devices/slm/patch.py index eb27dc9..66d2c8e 100644 --- a/openwfs/devices/slm/patch.py +++ b/openwfs/devices/slm/patch.py @@ -185,7 +185,7 @@ def __init__(self, slm, lookup_table: Optional[Sequence[int]], bit_depth: int): slm: SLM object that this patch belongs to lookup_table: 1-D array of gray values that will be used to map the phase values to the gray-scale output. see :attr:`~SLM.lookup_table` for details. - bit_depth: bit depth of the SLM. The maximum value in the lookup table can be 2**bit_depth - 1. + bit_depth: The bit depth of the SLM. The maximum value in the lookup table can be 2**bit_depth - 1. Note: this maximum value is mapped to 1.0 in the opengl shader, and converted back to 2**bit_depth by the opengl hardware. """ diff --git a/openwfs/devices/slm/shaders.py b/openwfs/devices/slm/shaders.py index 966f22d..02fb628 100644 --- a/openwfs/devices/slm/shaders.py +++ b/openwfs/devices/slm/shaders.py @@ -33,7 +33,7 @@ # the range -δ to δ maps to a gray value 0 instead of # negative values mapping to 255 and positive values mapping to 0. # Since the lookup table texture is configured to use GL_WRAP, -# only the fractional part of texCoord is used (i. e., texCoord - floor(texCoord)). +# only the fractional part of texCoord is used (i.e., texCoord - floor(texCoord)). # post_process_fragment_shader = """ #version 440 core diff --git a/openwfs/devices/slm/slm.py b/openwfs/devices/slm/slm.py index 5ab67b5..e7bdb62 100644 --- a/openwfs/devices/slm/slm.py +++ b/openwfs/devices/slm/slm.py @@ -206,7 +206,7 @@ def _on_resize(self): """Updates shape and refresh rate to the actual values of the window. Note that these values are in pixels, which may be different from the window size because the window size is - in screen coordinates, which may not always the same as pixels (e. g. on a retina display). + in screen coordinates, which may not always the same as pixels (e.g. on a retina display). For windowed SLMs, the refresh rate property is set to the refresh rate of the primary monitor. @@ -407,7 +407,7 @@ def update(self): Note: At the moment, :meth:`~.SLM.update` blocks until all OpenGL commands are processed, - and a vertical retrace occurs (i. e., the hardware signals the start of a new frame). + and a vertical retrace occurs (i.e., the hardware signals the start of a new frame). This behavior may change in the future and should not be relied on. Instead, use the automatic synchronization mechanism to synchronize detectors with the SLM hardware. diff --git a/openwfs/plot_utilities.py b/openwfs/plot_utilities.py index 690f274..fcfd450 100644 --- a/openwfs/plot_utilities.py +++ b/openwfs/plot_utilities.py @@ -1,16 +1,19 @@ from typing import Tuple, Union, Optional, Dict import numpy as np -from numpy import ndarray as nd from astropy import units as u from matplotlib import pyplot as plt -from matplotlib.colors import hsv_to_rgb from matplotlib.axes import Axes +from matplotlib.colors import hsv_to_rgb +from numpy import ndarray as nd from .core import Detector from .utilities import get_extent +# TODO: needs review and documentation. Remove single-use functions, simplify code. + + def grab_and_show(cam: Detector, axis=None): return imshow(cam.read(), axis=axis) @@ -169,6 +172,7 @@ def complex_colorwheel( ): """ Create an rgb image for a colorwheel representing the complex unit circle. + TODO: needs review Args: ax: Matplotlib Axes. diff --git a/openwfs/simulation/microscope.py b/openwfs/simulation/microscope.py index 15f7274..4b62746 100644 --- a/openwfs/simulation/microscope.py +++ b/openwfs/simulation/microscope.py @@ -76,7 +76,7 @@ def __init__( incident_field: Produces 2-D complex images containing the field output of the SLM. If no `slm_transform` is specified, the `pixel_size` attribute should correspond to normalized pupil coordinates - (e. g. with a disk of radius 1.0, i. e. an extent of 2.0, corresponding to an NA of 1.0) + (e.g. with a disk of radius 1.0, i.e. an extent of 2.0, corresponding to an NA of 1.0) incident_transform (Optional[Transform]): Optional Transform that transforms the phase pattern from the slm object (in slm.pixel_size units) to normalized pupil coordinates. @@ -91,7 +91,7 @@ def __init__( Optional Transform that transforms the phase pattern from the aberration object (in slm.pixel_size units) to normalized pupil coordinates. Typically, the slm image is already in normalized pupil coordinates, - but this transform may e. g., be used to scale an aberration pattern + but this transform may e.g., be used to scale an aberration pattern from extent 2.0 to 2.0 * NA. Note: diff --git a/openwfs/simulation/slm.py b/openwfs/simulation/slm.py index 9cf682e..97ebd59 100644 --- a/openwfs/simulation/slm.py +++ b/openwfs/simulation/slm.py @@ -27,7 +27,7 @@ def __init__( Args: slm_phases: The `Detector` that returns the phases of the slm pixels. field_amplitude: Field amplitude of the modulated pixels. - non_modulated_field_fraction: Non-modulated field (e. g. a front reflection). + non_modulated_field_fraction: Non-modulated field (e.g. a front reflection). """ super().__init__(slm_phases, multi_threaded=False) self.modulated_field_amplitude = field_amplitude diff --git a/openwfs/simulation/transmission.py b/openwfs/simulation/transmission.py index f0c7a12..99eeaa1 100644 --- a/openwfs/simulation/transmission.py +++ b/openwfs/simulation/transmission.py @@ -38,11 +38,11 @@ def __init__( Args: t: Transmission matrix. aberrations: An array containing the aberrations in radians. Can be used instead of a transmission matrix, - equivalent to specifying t = np.exp(1j * aberrations) / (aberrations.shape[0] * aberrations.shape[1]). + equivalent to specifying ``t = np.exp(1j * aberrations) / (aberrations.shape[0] * aberrations.shape[1])``. slm: multi_threaded (bool, optional): If True, the simulation will use multiple threads to compute the intensity in the focus. If False, the simulation will use a single thread. Defaults to True. - beam_amplitude (ScalarType, optional): The beam profile amplitude. Can be an np.ndarray. Defaults to 1.0. + beam_amplitude (ScalarType, optional): The amplitude profile of the incident beam. Defaults to 1.0. The constructor creates a MockSLM instance based on the shape of the aberrations, calculates the electric field at the SLM considering the aberrations and optionally the Gaussian beam profile, and initializes the diff --git a/openwfs/utilities/patterns.py b/openwfs/utilities/patterns.py index ca650fa..548a8a2 100644 --- a/openwfs/utilities/patterns.py +++ b/openwfs/utilities/patterns.py @@ -103,7 +103,7 @@ def tilt( corresponds to having a ramp from -2 to +2 over the height of the pattern When this pattern is used as a phase in a pupil-conjugate configuration, this corresponds to a displacement of -2/π times the Abbe diffraction limit - (e. g. a positive x-gradient g causes the focal point to move in the _negative_ x-direction) + (e.g. a positive x-gradient g causes the focal point to move in the _negative_ x-direction) extent: see module documentation phase_offset: optional additional phase offset to be added to the pattern """ @@ -183,7 +183,7 @@ def gaussian( Args: shape: see module documentation waist (ScalarType): location of the beam waist (1/e value) - relative to half of the size of the pattern (i. e. relative to the `radius` of the square) + relative to half of the size of the pattern (i.e. relative to the `radius` of the square) truncation_radius (ScalarType): when not None, specifies the radius of a disk that is used to truncate the Gaussian. All values outside the disk are set to 0. extent: see module documentation diff --git a/openwfs/utilities/utilities.py b/openwfs/utilities/utilities.py index 5102792..a81caf8 100644 --- a/openwfs/utilities/utilities.py +++ b/openwfs/utilities/utilities.py @@ -293,7 +293,7 @@ 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. + 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. Args: diff --git a/tests/test_algorithms_troubleshoot.py b/tests/test_algorithms_troubleshoot.py index daae1a1..515018d 100644 --- a/tests/test_algorithms_troubleshoot.py +++ b/tests/test_algorithms_troubleshoot.py @@ -70,7 +70,7 @@ def test_find_pixel_shift(): def test_field_correlation(): """ - Test the field correlation, i. e. g_1 normalized first order correlation function. + Test the field correlation, i.e. g_1 normalized first order correlation function. """ a = np.zeros(shape=(2, 3)) a[1, 2] = 2.0 @@ -93,7 +93,7 @@ def test_field_correlation(): def test_frame_correlation(): """ - Test the frame correlation, i. e. g_2 normalized second order correlation function. + Test the frame correlation, i.e. g_2 normalized second order correlation function. Test the following: g_2 correlation with self == 1/3 for distribution from `random.rand` g_2 correlation with other == 0 diff --git a/tests/test_scanning_microscope.py b/tests/test_scanning_microscope.py index fadee2d..83e8e5f 100644 --- a/tests/test_scanning_microscope.py +++ b/tests/test_scanning_microscope.py @@ -186,30 +186,4 @@ def test_park_beam(bidirectional): assert np.allclose(voltages[1, :], voltages[1, 0]) # all voltages should be the same assert np.allclose(voltages[0, :], voltages[0, 0]) # all voltages should be the same - -# test zooming -# ps = scanner.pixel_size -# scanner.zoom = 2.0 -# assert np.allclose(scanner.pixel_size, ps * 0.5) -# assert scanner.width == width -# assert scanner.height == height -# assert scanner.data_shape == (height, width) -# assert scanner.left == np.floor(2 * left + 0.5 * width) -# assert scanner.top == np.floor(2 * top + 0.5 * height) - -# zoomed = scanner.read().astype('float32') - 0x8000 -# scaled = place(zoomed.shape, 0.5 * ps, set_pixel_size(roi, ps)) -# assert np.allclose(get_pixel_size(scaled), 0.5 * ps) -# step = zoomed[1, 1] - zoomed[0, 0] -# assert np.allclose(zoomed, scaled - step / 2, atol=0.5 * step) - -# scanner.zoom = 1.0 -# reset_zoom = scanner.read().astype('float32') - 0x8000 -# assert np.allclose(reset_zoom, roi) - -# test setting dwell time -# original_duration = scanner.duration -# scanner.delay = 1.0 -# scanner.dwell_time = scanner.dwell_time * 2.0 -# assert scanner.duration == original_duration * 2.0 -# assert scanner.delay == 0.5 + # TODO: add test for zooming diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 87daf5a..4fdfd4d 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -194,7 +194,7 @@ def inverse_phase_response_test_function(f, b, c, gamma): def lookup_table_test_function(f, b, c, gamma): """ - Compute the lookup indices (i. e. a lookup table) + Compute the lookup indices (i.e. a lookup table) for countering the synthetic phase response test function: 2π*(b + c*(phi/2π)^gamma). """ phase = inverse_phase_response_test_function(f, b, c, gamma) From f8be408ef934c902563d21924b56c8f5d8c948ef Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Fri, 4 Oct 2024 23:18:32 +0200 Subject: [PATCH 14/37] cleanup, fixed warnings --- docs/source/conf.py | 4 +-- examples/micro_manager_microscope.py | 7 +++-- examples/sample_microscope.py | 5 ++-- examples/troubleshooter_demo.py | 4 +-- openwfs/algorithms/dual_reference.py | 1 + openwfs/algorithms/troubleshoot.py | 5 ++-- openwfs/devices/camera.py | 2 +- openwfs/simulation/microscope.py | 42 ++------------------------- openwfs/simulation/mockdevices.py | 4 --- openwfs/utilities/utilities.py | 19 +++++++----- tests/test_algorithms_troubleshoot.py | 9 +++--- tests/test_processors.py | 2 +- tests/test_simulation.py | 7 ++--- tests/test_wfs.py | 19 +++++++----- 14 files changed, 51 insertions(+), 79 deletions(-) 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) From 687da83f1bdfb2b82adb6590ab7f4641a2002eaa Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Sat, 5 Oct 2024 16:03:05 +0200 Subject: [PATCH 15/37] fixed tests, now take into account amplitude difference for dual algorithm --- tests/test_wfs.py | 89 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 29 deletions(-) diff --git a/tests/test_wfs.py b/tests/test_wfs.py index 870010a..a3560b0 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -1,3 +1,5 @@ +from typing import Optional + import astropy.units as u import numpy as np import pytest @@ -30,8 +32,6 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm and it also verifies that the enhancement and noise fidelity are estimated correctly by the algorithm. """ - np.random.seed(42) # for reproducibility - M = 100 # number of targets phase_steps = 6 @@ -52,6 +52,7 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm N = np.prod(shape) # number of input modes alg_fidelity = (N - 1) / N # SSA is inaccurate if N is low signal = (N - 1) / N**2 # for estimating SNR + masks = [True] # use all segments for determining fidelity else: # 'fourier': alg = FourierDualReference( feedback=feedback, @@ -63,6 +64,7 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm N = alg.phase_patterns[0].shape[2] + alg.phase_patterns[1].shape[2] # number of input modes alg_fidelity = 1.0 # Fourier is accurate for any N signal = 1 / 2 # for estimating SNR. + masks = alg.masks # use separate halves of the segments for determining fidelity # Execute the algorithm to get the optimized wavefront # for all targets simultaneously @@ -74,27 +76,25 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm # the unknown phases. The normalization of the correlation function # is performed on all rows together, not per row, to increase # the accuracy of the estimate. + # For the dual reference algorithm, the left and right half will have a different overall amplitude factor. + # This is corrected for by computing the correlations for the left and right half separately + # I_opt = np.zeros((M,)) - t_correlation = 0.0 - t_norm = 0.0 for b in range(M): sim.slm.set_phases(-np.angle(result.t[:, :, b])) I_opt[b] = feedback.read()[b] - t_correlation += abs(np.vdot(result.t[:, :, b], sim.t[:, :, b])) ** 2 - t_norm += abs(np.vdot(result.t[:, :, b], result.t[:, :, b]) * np.vdot(sim.t[:, :, b], sim.t[:, :, b])) - t_correlation /= t_norm - # a correlation of 1 means optimal reconstruction of the N modulated modes, which may be less than the total number of inputs in the transmission matrix - t_correlation *= np.prod(shape) / N + t_correlation = t_fidelity(result.t, sim.t, masks) # Check the enhancement, noise fidelity and # the fidelity of the transmission matrix reconstruction + coverage = N / np.prod(shape) theoretical_noise_fidelity = signal / (signal + noise**2 / phase_steps) enhancement = I_opt.mean() / I_0 theoretical_enhancement = np.pi / 4 * theoretical_noise_fidelity * alg_fidelity * (N - 1) + 1 estimated_enhancement = result.estimated_enhancement.mean() * alg_fidelity - theoretical_t_correlation = theoretical_noise_fidelity * alg_fidelity - estimated_t_correlation = result.fidelity_noise * result.fidelity_calibration * alg_fidelity + theoretical_t_correlation = theoretical_noise_fidelity * alg_fidelity * coverage + estimated_t_correlation = result.fidelity_noise * result.fidelity_calibration * alg_fidelity * coverage tolerance = 2.0 / np.sqrt(M) print( f"\nenhancement: \ttheoretical= {theoretical_enhancement},\testimated={estimated_enhancement},\tactual: {enhancement}" @@ -140,9 +140,38 @@ def random_transmission_matrix(shape): """ Create a random transmission matrix with the given shape. """ + np.random.seed(42) # for reproducibility return np.random.normal(size=shape) + 1j * np.random.normal(size=shape) +def t_fidelity(t: np.ndarray, t_correct: np.ndarray, masks: Optional[tuple[np.ndarray, ...]] = (True,)) -> float: + """ + Compute the fidelity of the measured transmission matrix. + + Since the overall phase for each row is unknown, the fidelity is computed row by row. + Moreover, for dual-reference algorithms the left and right half of the wavefront + may have different overall amplitude factors. This is corrected for by computing + the fidelity for the left and right half separately. + + The fidelities for all rows (and halves) are weighted by the 'intensity' in the correct transmission matrix. + + Args: + t: The measured transmission matrix. 'rows' of t are the *last* index + t_correct: The correct transmission matrix + masks: Masks for the left and right half of the wavefront, or None to use the full wavefront + """ + t_fidelity = 0.0 + t_norm = 0.0 + for r in range(t.shape[-1]): # each row + for m in masks: + rm = t[..., r][m] + rm_c = t_correct[..., r][m] + t_fidelity += abs(np.vdot(rm, rm_c) ** 2 / np.vdot(rm, rm)) + t_norm += np.vdot(rm_c, rm_c) + + return t_fidelity / t_norm + + @pytest.mark.skip("Not implemented") def test_fourier2(): """Test the Fourier dual reference algorithm using WFSController.""" @@ -248,8 +277,8 @@ def test_phase_shift_correction(): @pytest.mark.parametrize("optimized_reference", [True, False]) -@pytest.mark.parametrize("step", [True, False]) -def test_flat_wf_response_fourier(optimized_reference, step): +@pytest.mark.parametrize("type", ["flat", "phase_step", "amplitude_step"]) +def test_flat_wf_response_fourier(optimized_reference, type): """ Test the response of the Fourier-based WFS method when the solution is flat A flat solution means that the optimal correction is no correction. @@ -257,26 +286,30 @@ def test_flat_wf_response_fourier(optimized_reference, step): test the optimized wavefront by checking if it has irregularities. """ - aberrations = np.ones(shape=(4, 4)) - if step: - aberrations[:, 2:] = 2.0 - sim = SimulatedWFS(aberrations=aberrations.reshape((*aberrations.shape, 1))) + t = np.ones(shape=(4, 4), dtype=np.complex64) + if type == "phase_step": + t[:, 2:] = np.exp(2.0j) + elif type == "amplitude_step": + t[:, 2:] = 2.0 + + sim = SimulatedWFS(t=t) alg = FourierDualReference( feedback=sim, slm=sim.slm, - slm_shape=np.shape(aberrations), + slm_shape=np.shape(t), k_radius=1.5, phase_steps=3, optimized_reference=optimized_reference, ) - t = alg.execute().t - - # test the optimized wavefront by checking if it has irregularities. - measured_aberrations = np.squeeze(np.angle(t)) - measured_aberrations += aberrations[0, 0] - measured_aberrations[0, 0] - assert np.allclose(measured_aberrations, aberrations, atol=0.02) # The measured wavefront is not flat. + result = alg.execute() + assert ( + abs(field_correlation(result.t / np.abs(result.t), t / np.abs(t))) > 0.99 + ), "The phases were not calculated correctly" + assert ( + t_fidelity(np.expand_dims(result.t, -1), np.expand_dims(t, -1), alg.masks) > 0.99 + ), "The amplitudes were not calculated correctly" def test_flat_wf_response_ssa(): @@ -347,7 +380,7 @@ def test_multidimensional_feedback_fourier(): Expected at least 3.0, got {enhancement}""" -@pytest.mark.parametrize("population_size, elite_size", [(30, 15), (30, 5)]) +@pytest.mark.parametrize("population_size, elite_size", [(30, 15)]) # , (30, 5)]) def test_simple_genetic(population_size: int, elite_size: int): """ Test the SimpleGenetic algorithm. @@ -376,7 +409,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: tuple[int, int]): - """Test dual reference with an orthonormal phase-only basis. + """Test dual reference in iterative mode with an orthonormal phase-only basis. Two types of bases are tested: plane waves and Hadamard""" do_debug = False N = shape[0] * (shape[1] // 2) @@ -455,9 +488,7 @@ def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): result_t_phase_only = np.exp(1j * np.angle(result.t)) assert np.abs(field_correlation(sim_t_phase_only, result_t_phase_only)) > 0.999 - # todo: find out why this is not higher - # Test field correlation - assert np.abs(field_correlation(sim.t, result.t)) > 0.9 + assert t_fidelity(np.expand_dims(result.t, -1), np.expand_dims(sim.t, -1), alg.masks) > 0.9 def test_dual_reference_non_ortho_split(): From aecf9d0308bc4d6f9a99447bc0ec2cc391afe737 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Sat, 5 Oct 2024 16:46:05 +0200 Subject: [PATCH 16/37] simplified cobasis computation --- openwfs/algorithms/dual_reference.py | 76 ++++++++++------------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index 181e9da..edb986a 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -148,35 +148,41 @@ def phase_patterns(self) -> tuple[nd, nd]: @phase_patterns.setter def phase_patterns(self, value): """Sets the phase patterns for group A and group B. This also updates the conjugate modes.""" + self._zero_indices = [0, 0] + self._phase_patterns = [None, None] + self._cobasis = [None, None] + self._gram = [None, None] if value is None: - self._phase_patterns = None return - if not self.optimized_reference: - # find the modes in A and B that correspond to flat wavefronts with phase 0 - try: - a0_index = next(i for i in range(value[0].shape[2]) if np.allclose(value[0][:, :, i], 0)) - b0_index = next(i for i in range(value[1].shape[2]) if np.allclose(value[1][:, :, i], 0)) - self.zero_indices = (a0_index, b0_index) - except StopIteration: - raise "For multi-target optimization, the both sets must contain a flat wavefront with phase 0." - - if (value[0].shape[0:2] != self._shape) or (value[1].shape[0:2] != self._shape): - raise ValueError("The phase patterns and group mask must all have the same shape.") - - self._phase_patterns = ( - value[0].astype(np.float32), - value[1].astype(np.float32), - ) - - self._compute_cobasis() + for side in range(2): + phase = value[side] + if phase.shape[0:2] != self._shape: + raise ValueError("The phase patterns and group mask must all have the same shape.") + if not self.optimized_reference: + # find the modes in A and B that correspond to flat wavefronts with phase 0 + try: + self._zero_indices[side] = next(i for i in range(phase.shape[2]) if np.allclose(phase[:, :, i], 0)) + except StopIteration: + raise "For multi-target optimization, the both sets must contain a flat wavefront with phase 0." + + self._phase_patterns[side] = phase.astype(np.float32) + + # Computes the cobasis + # As a basis matrix is full rank, this is equivalent to the Moore-Penrose pseudo-inverse. + # B⁺ = (B^* B)^(-1) B^* + # Where B is the basis matrix (a column corresponds to a basis vector), ^* denotes the conjugate transpose, ^(-1) + # denotes the matrix inverse, and ⁺ denotes the Moore-Penrose pseudo-inverse. + basis = np.expand_dims(self.amplitude[side] * self.masks[side], -1) * np.exp(1j * phase) + self._gram[side] = np.tensordot(basis, basis.conj(), axes=((0, 1), (0, 1))) + self._cobasis[side] = np.tensordot(basis.conj(), np.linalg.inv(self._gram[side]), 1) @property def cobasis(self) -> tuple[nd, nd]: """ The cobasis corresponding to the given basis. """ - return self._cobasis + return tuple(self._cobasis) @property def gram(self) -> np.matrix: @@ -185,32 +191,6 @@ def gram(self) -> np.matrix: """ return self._gram - def _compute_cobasis(self): - """ - Computes the cobasis from the phase patterns. - - As a basis matrix is full rank, this is equivalent to the Moore-Penrose pseudo-inverse. - B⁺ = (B^* B)^(-1) B^* - Where B is the basis matrix (a column corresponds to a basis vector), ^* denotes the conjugate transpose, ^(-1) - denotes the matrix inverse, and ⁺ denotes the Moore-Penrose pseudo-inverse. - """ - 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 - m = self.phase_patterns[side].shape[2] # Number of modes - phase_factor = np.exp(1j * self.phase_patterns[side]) - amplitude_factor = np.expand_dims(self.amplitude[side] * self.masks[side], axis=2) - B = np.asmatrix((phase_factor * amplitude_factor).reshape((p, m))) # Basis matrix - self._gram = B.H @ B - B_pinv = np.linalg.inv(self.gram) @ B.H # Moore-Penrose pseudo-inverse - cobasis[side] = np.asarray(B_pinv.T).reshape(self.phase_patterns[side].shape) - - self._cobasis = cobasis - def execute(self, *, capture_intermediate_results: bool = False, progress_bar=None) -> WFSResult: """ Executes the blind focusing dual reference algorithm and compute the SLM transmission matrix. @@ -273,8 +253,8 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No # two halves of the wavefront together. For that, we need the # relative phase between the two sides, which we extract from # the measurements of the flat wavefronts. - relative = results_all[0].t[self.zero_indices[0], ...] + np.conjugate( - results_all[1].t[self.zero_indices[1], ...] + relative = results_all[0].t[self._zero_indices[0], ...] + np.conjugate( + results_all[1].t[self._zero_indices[1], ...] ) factor = (relative / np.abs(relative)).reshape((1, *self.feedback.data_shape)) From e931e9ab5233025f18f6931ddf67e47fc1fa131e Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Sat, 5 Oct 2024 22:17:37 +0200 Subject: [PATCH 17/37] simplified amplitude parameter --- openwfs/algorithms/basic_fourier.py | 58 +++++++++++++------ openwfs/algorithms/dual_reference.py | 42 +++++--------- tests/test_wfs.py | 85 +++++++++------------------- 3 files changed, 83 insertions(+), 102 deletions(-) diff --git a/openwfs/algorithms/basic_fourier.py b/openwfs/algorithms/basic_fourier.py index d7084fc..92f26c8 100644 --- a/openwfs/algorithms/basic_fourier.py +++ b/openwfs/algorithms/basic_fourier.py @@ -36,38 +36,51 @@ def __init__( k_radius: float = 3.2, k_step: float = 1.0, iterations: int = 2, + amplitude: np.ndarray = 1.0, analyzer: Optional[callable] = analyze_phase_stepping, optimized_reference: Optional[bool] = None ): """ Args: - feedback (Detector): Source of feedback - slm (PhaseSLM): The spatial light modulator - slm_shape (tuple of two ints): The shape that the SLM patterns & transmission matrices are calculated for, + feedback: Source of feedback + slm: The spatial light modulator + slm_shape: The shape that the SLM patterns & transmission matrices are calculated for, does not necessarily have to be the actual pixel dimensions as the SLM. - k_radius (float): Limit grid points to lie within a circle of this radius. - k_step (float): Make steps in k-space of this value. 1 corresponds to diffraction limited tilt. - phase_steps (int): The number of phase steps. + phase_steps: The number of phase steps. + k_radius: Limit grid points to lie within a circle of this radius. + k_step: Make steps in k-space of this value. 1 corresponds to diffraction limited tilt. + iterations: Number of ping-pong iterations. Defaults to 2. + amplitude: Amplitude profile over the SLM. Defaults to 1.0 (flat) + analyzer: The function used to analyze the phase stepping data. + Must return a WFSResult object. Defaults to `analyze_phase_stepping` + optimized_reference: + When `True`, during each iteration the other half of the SLM displays the optimized pattern so far (as in [1]). + When `False`, the algorithm optimizes A with a flat wavefront on B, + and then optimizes B with a flat wavefront on A. + This mode also allows for multi-target optimization. + When set to `None` (default), the algorithm uses True if there is a single target, + and False if there are multiple targets. + + """ self._k_radius = k_radius - self.k_step = k_step - self._slm_shape = slm_shape + self._k_step = k_step + self._shape = slm_shape group_mask = np.zeros(slm_shape, dtype=bool) group_mask[:, slm_shape[1] // 2 :] = True super().__init__( feedback=feedback, slm=slm, - phase_patterns=None, + phase_patterns=self._construct_modes(), group_mask=group_mask, phase_steps=phase_steps, - amplitude="uniform", iterations=iterations, + amplitude=amplitude, optimized_reference=optimized_reference, analyzer=analyzer, ) - self._update_modes() - def _update_modes(self): + def _construct_modes(self) -> tuple[np.ndarray, np.ndarray]: """Constructs the set of plane wave modes.""" # start with a grid of k-values @@ -87,14 +100,14 @@ def _update_modes(self): k = np.stack((ky[mask], kx[mask])).T # construct the modes for these kx ky values - modes = np.zeros((*self._slm_shape, len(k)), dtype=np.float32) + modes = np.zeros((*self._shape, len(k)), dtype=np.float32) for i, k_i in enumerate(k): # tilt generates a pattern from -2.0 to 2.0 (The convention for Zernike modes normalized to an RMS of 1). # The natural step to take is the Abbe diffraction limit of the modulated part, # which corresponds to a gradient from -π to π over the modulated part. - modes[..., i] = tilt(self._slm_shape, g=k_i * 0.5 * np.pi) + modes[..., i] = tilt(self._shape, g=k_i * 0.5 * np.pi) - self.phase_patterns = (modes, modes) + return modes, modes @property def k_radius(self) -> float: @@ -102,7 +115,16 @@ def k_radius(self) -> float: return self._k_radius @k_radius.setter - def k_radius(self, value): + def k_radius(self, value: float): """Sets the maximum radius of the k-space circle, triggers the building of the internal k-space properties.""" - self._k_radius = value - self._update_modes() + self._k_radius = float(value) + self.phase_patterns = self._construct_modes() + + @property + def k_step(self) -> float: + return self._k_step + + @k_step.setter + def k_step(self, value: float): + self._k_step = float(value) + self.phase_patterns = self._construct_modes() diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index edb986a..200cab1 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -1,4 +1,4 @@ -from typing import Optional, Union, List +from typing import Optional, List import numpy as np from numpy import ndarray as nd @@ -42,8 +42,8 @@ def __init__( feedback: Detector, slm: PhaseSLM, phase_patterns: Optional[tuple[nd, nd]], - amplitude: Optional[Union[tuple[nd, nd], str]], group_mask: nd, + amplitude: nd = 1.0, phase_steps: int = 4, iterations: int = 2, analyzer: Optional[callable] = analyze_phase_stepping, @@ -59,11 +59,8 @@ def __init__( The 3rd dimension in the array is index of the phase pattern. The number of phase patterns in A and B may be different. When None, the phase_patterns attribute must be set before executing the algorithm. - amplitude: Tuple of 2D arrays, one array for each group. The arrays have shape equal to the shape of - group_mask. When None, the amplitude attribute must be set before executing the algorithm. When - 'uniform', a 2D array of normalized uniform values is used, such that ⟨A,A⟩=1, where ⟨.,.⟩ denotes the - inner product and A is the amplitude profile per group. This corresponds to a uniform illumination of - the SLM. Note: if the groups have different sizes, their normalization factors will be different. + amplitude: + Amplitude distribution on the SLM, should have the same size as group_mask and phase_patterns. group_mask: A 2D bool array of that defines the pixels used by group A with False and elements used by group B with True. phase_steps: The number of phase steps for each mode (default is 4). Depending on the type of @@ -109,7 +106,6 @@ def __init__( self.iterations = iterations self._analyzer = analyzer self._phase_patterns = None - self._amplitude = None self._gram = None self._shape = group_mask.shape mask = group_mask.astype(bool) @@ -117,7 +113,8 @@ def __init__( ~mask, mask, ) # self.masks[0] is True for group A, self.masks[1] is True for group B - self.amplitude = amplitude # Note: when 'uniform' is passed, the shape of self.masks[0] is used. + self._amplitude = 1.0 + self.amplitude = amplitude self.phase_patterns = phase_patterns @property @@ -125,18 +122,8 @@ def amplitude(self) -> Optional[nd]: return self._amplitude @amplitude.setter - def amplitude(self, value): - if value is None: - self._amplitude = None - return - - if value == "uniform": - self._amplitude = tuple( - (np.ones(shape=self._shape) / np.sqrt(self.masks[side].sum())).astype(np.float32) for side in range(2) - ) - return - - if value[0].shape != self._shape or value[1].shape != self._shape: + def amplitude(self, value: np.ndarray): + if not np.isscalar(value) and value.shape != self._shape: raise ValueError("The amplitude and group mask must all have the same shape.") self._amplitude = value @@ -146,17 +133,16 @@ def phase_patterns(self) -> tuple[nd, nd]: return self._phase_patterns @phase_patterns.setter - def phase_patterns(self, value): + def phase_patterns(self, value: np.ndarray): """Sets the phase patterns for group A and group B. This also updates the conjugate modes.""" self._zero_indices = [0, 0] self._phase_patterns = [None, None] self._cobasis = [None, None] self._gram = [None, None] - if value is None: - return for side in range(2): phase = value[side] + mask = self.masks[side] if phase.shape[0:2] != self._shape: raise ValueError("The phase patterns and group mask must all have the same shape.") if not self.optimized_reference: @@ -173,9 +159,11 @@ def phase_patterns(self, value): # B⁺ = (B^* B)^(-1) B^* # Where B is the basis matrix (a column corresponds to a basis vector), ^* denotes the conjugate transpose, ^(-1) # denotes the matrix inverse, and ⁺ denotes the Moore-Penrose pseudo-inverse. - basis = np.expand_dims(self.amplitude[side] * self.masks[side], -1) * np.exp(1j * phase) - self._gram[side] = np.tensordot(basis, basis.conj(), axes=((0, 1), (0, 1))) - self._cobasis[side] = np.tensordot(basis.conj(), np.linalg.inv(self._gram[side]), 1) + basis = np.expand_dims(self.amplitude * mask, -1) * np.exp(1j * phase) + basis /= np.linalg.norm(basis, axis=(0, 1)) + gram = np.tensordot(basis, basis.conj(), axes=((0, 1), (0, 1))) + self._cobasis[side] = np.tensordot(basis.conj(), np.linalg.inv(gram), 1) + self._gram[side] = gram @property def cobasis(self) -> tuple[nd, nd]: diff --git a/tests/test_wfs.py b/tests/test_wfs.py index a3560b0..f906553 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -144,6 +144,20 @@ def random_transmission_matrix(shape): return np.random.normal(size=shape) + 1j * np.random.normal(size=shape) +def half_mask(shape): + """ + Args: + shape: shape of the output array + + Returns: + Returns a boolean mask with [:, :shape[2]] set to False + and [:, shape[1]] set to True]. + """ + mask = np.zeros(shape, dtype=bool) + mask[:, shape[1] // 2 :] = True + return mask + + def t_fidelity(t: np.ndarray, t_correct: np.ndarray, masks: Optional[tuple[np.ndarray, ...]] = (True,)) -> float: """ Compute the fidelity of the measured transmission matrix. @@ -160,16 +174,16 @@ def t_fidelity(t: np.ndarray, t_correct: np.ndarray, masks: Optional[tuple[np.nd t_correct: The correct transmission matrix masks: Masks for the left and right half of the wavefront, or None to use the full wavefront """ - t_fidelity = 0.0 - t_norm = 0.0 + fidelity = 0.0 + norm = 0.0 for r in range(t.shape[-1]): # each row for m in masks: rm = t[..., r][m] rm_c = t_correct[..., r][m] - t_fidelity += abs(np.vdot(rm, rm_c) ** 2 / np.vdot(rm, rm)) - t_norm += np.vdot(rm_c, rm_c) + fidelity += abs(np.vdot(rm, rm_c) ** 2 / np.vdot(rm, rm)) + norm += np.vdot(rm_c, rm_c) - return t_fidelity / t_norm + return fidelity / norm @pytest.mark.skip("Not implemented") @@ -411,79 +425,37 @@ def test_simple_genetic(population_size: int, elite_size: int): def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): """Test dual reference in iterative mode with an orthonormal phase-only basis. Two types of bases are tested: plane waves and Hadamard""" - do_debug = False N = shape[0] * (shape[1] // 2) modes_shape = (shape[0], shape[1] // 2, N) if basis_str == "plane_wave": # Create a full plane wave basis for one half of the SLM. - modes = np.fft.fft2(np.eye(N).reshape(modes_shape), axes=(0, 1)) / np.sqrt(N) + phases = np.angle(np.fft.fft2(np.eye(N).reshape(modes_shape), axes=(0, 1))) elif basis_str == "hadamard": - modes = hadamard(N).reshape(modes_shape) / np.sqrt(N) + phases = np.angle(hadamard(N).reshape(modes_shape)) else: raise f'Unknown type of basis "{basis_str}".' - mask = np.concatenate( - (np.zeros(modes_shape[0:2], dtype=bool), np.ones(modes_shape[0:2], dtype=bool)), - axis=1, - ) - mode_set = np.concatenate((modes, np.zeros(shape=modes_shape)), axis=1) - phases_set = np.angle(mode_set) + mask = half_mask(shape) + phases_set = np.concatenate((phases, np.zeros(shape=modes_shape)), axis=1) - if do_debug: - # Plot the modes - import matplotlib.pyplot as plt - - plt.figure(figsize=(12, 7)) - for m in range(N): - plt.subplot(*modes_shape[0:2], m + 1) - plot_field(mode_set[:, :, m]) - plt.title(f"m={m}") - plt.xticks([]) - plt.yticks([]) - plt.suptitle("Basis") - plt.pause(0.01) - - # Create aberrations sim = SimulatedWFS(t=random_transmission_matrix(shape)) alg = DualReference( feedback=sim, slm=sim.slm, phase_patterns=(phases_set, np.flip(phases_set, axis=1)), - amplitude="uniform", group_mask=mask, iterations=4, ) - 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) - plot_field(alg.cobasis[0][:, :, m]) - plt.title(f"{m}") - plt.suptitle("Cobasis") - plt.pause(0.01) - - plt.figure() - plt.imshow(np.angle(sim.t), vmin=-np.pi, vmax=np.pi, cmap="hsv") - plt.title("Aberrations") - - plt.figure() - plt.imshow(np.angle(result.t), vmin=-np.pi, vmax=np.pi, cmap="hsv") - plt.title("t") - plt.colorbar() - plt.show() - # Checks for orthonormal basis properties - assert np.allclose(alg.gram, np.eye(N), atol=1e-6) # Gram matrix must be I - assert np.allclose(alg.cobasis[0], mode_set.conj(), atol=1e-6) # Cobasis vectors are just the complex conjugates + assert np.allclose(alg.gram[0], np.eye(N), atol=1e-6) # Gram matrix must be I + + # Cobasis vectors are just the complex conjugates + assert np.allclose(alg.cobasis[0], np.exp(-1j * phases_set) * abs(alg.cobasis[0]), atol=1e-6) # Test phase-only field correlation + result = alg.execute() sim_t_phase_only = np.exp(1j * np.angle(sim.t)) result_t_phase_only = np.exp(1j * np.angle(result.t)) assert np.abs(field_correlation(sim_t_phase_only, result_t_phase_only)) > 0.999 @@ -533,7 +505,6 @@ def test_dual_reference_non_ortho_split(): feedback=sim, slm=sim.slm, phase_patterns=(phases_set, np.flip(phases_set, axis=1)), - amplitude="uniform", group_mask=mask, phase_steps=4, iterations=4, From 24272936bed4e32678aa7ae310087faae6c6cd36 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Sun, 6 Oct 2024 12:06:39 +0200 Subject: [PATCH 18/37] standardized indices for dual reference algorithm --- STYLEGUIDE.md | 17 +++++- openwfs/algorithms/basic_fourier.py | 5 +- openwfs/algorithms/dual_reference.py | 81 ++++++++++++++-------------- tests/test_wfs.py | 71 ++++++------------------ 4 files changed, 73 insertions(+), 101 deletions(-) diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index f5a9eeb..29236a3 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -24,4 +24,19 @@ Common warnings: - All line numbers are relative to the start of the docstring. - 'WARNING: Block quote ends without a blank line; unexpected unindent'. This happens if a block of text is not properly wrapped and one of the lines starts with a space. To fox, remove the space at the beginning of the line. -- 'ERROR: Unexpected indentation' can be caused if a line ends with ':' and the next line is not indented or empty. \ No newline at end of file +- 'ERROR: Unexpected indentation' can be caused if a line ends with ':' and the next line is not indented or empty. + +# Indexing + +- For 2-D data, the first index is the row, the second index is the column. +- Multi-dimensional data sets should be indexed such that the first index corresponds + to the slowest changing dimension, and the last index corresponds to the fastest changing dimension. For example, a + sequence of camera frames should be indexed as `frames[frame_index, row, column]`. +- If you find yourself needing to index a 3D array with `array[:, :, 0]`, using a lot of `expand_dims` or `reshape` + commands, this is an indication that the indexing is not optimal, both in terms of performance and readability. +- Don't use ``[i,...]`` for indexing, use ``[i]`` instead. +- Prefer ``len(x)`` over ``x.shape(0)`` if the leading dimension represents a different 'type'. So, for a list of + images, use ``len(images)`` instead of ``images.shape(0)``. But to access the number of rows in an image, use + ``image.shape(0)``. + + diff --git a/openwfs/algorithms/basic_fourier.py b/openwfs/algorithms/basic_fourier.py index 92f26c8..53d302d 100644 --- a/openwfs/algorithms/basic_fourier.py +++ b/openwfs/algorithms/basic_fourier.py @@ -100,12 +100,13 @@ def _construct_modes(self) -> tuple[np.ndarray, np.ndarray]: k = np.stack((ky[mask], kx[mask])).T # construct the modes for these kx ky values - modes = np.zeros((*self._shape, len(k)), dtype=np.float32) + modes = np.zeros((len(k), *self._shape), dtype=np.float32) for i, k_i in enumerate(k): # tilt generates a pattern from -2.0 to 2.0 (The convention for Zernike modes normalized to an RMS of 1). # The natural step to take is the Abbe diffraction limit of the modulated part, # which corresponds to a gradient from -π to π over the modulated part. - modes[..., i] = tilt(self._shape, g=k_i * 0.5 * np.pi) + # TODO: modify tilt to take a 2-D argument, returning the mode set directly? + modes[i] = tilt(self._shape, g=k_i * 0.5 * np.pi) return modes, modes diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index 200cab1..f4fb1c5 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -55,14 +55,12 @@ def __init__( slm: Spatial light modulator object. phase_patterns: A tuple of two 3D arrays, containing the phase patterns for group A and group B, respectively. - The first two dimensions are the spatial dimensions, and should match the size of group_mask. - The 3rd dimension in the array is index of the phase pattern. + The 3D arrays have shape ``(pattern_count, height, width)``. The number of phase patterns in A and B may be different. - When None, the phase_patterns attribute must be set before executing the algorithm. amplitude: - Amplitude distribution on the SLM, should have the same size as group_mask and phase_patterns. + 2D amplitude distribution on the SLM, should have shape ``(height, width)``. group_mask: A 2D bool array of that defines the pixels used by group A with False and elements used by - group B with True. + group B with True, should have shape ``(height, width)``. phase_steps: The number of phase steps for each mode (default is 4). Depending on the type of non-linear feedback and the SNR, more might be required. iterations: Number of times to optimize a mode set, e.g. when iterations = 5, the measurements are @@ -141,43 +139,47 @@ def phase_patterns(self, value: np.ndarray): self._gram = [None, None] for side in range(2): - phase = value[side] + patterns = value[side] mask = self.masks[side] - if phase.shape[0:2] != self._shape: + if patterns[0].shape != self._shape: raise ValueError("The phase patterns and group mask must all have the same shape.") if not self.optimized_reference: # find the modes in A and B that correspond to flat wavefronts with phase 0 try: - self._zero_indices[side] = next(i for i in range(phase.shape[2]) if np.allclose(phase[:, :, i], 0)) + self._zero_indices[side] = next(i for i, p in enumerate(patterns) if np.allclose(p, 0)) except StopIteration: raise "For multi-target optimization, the both sets must contain a flat wavefront with phase 0." - self._phase_patterns[side] = phase.astype(np.float32) + self._phase_patterns[side] = patterns.astype(np.float32) # Computes the cobasis - # As a basis matrix is full rank, this is equivalent to the Moore-Penrose pseudo-inverse. - # B⁺ = (B^* B)^(-1) B^* - # Where B is the basis matrix (a column corresponds to a basis vector), ^* denotes the conjugate transpose, ^(-1) - # denotes the matrix inverse, and ⁺ denotes the Moore-Penrose pseudo-inverse. - basis = np.expand_dims(self.amplitude * mask, -1) * np.exp(1j * phase) - basis /= np.linalg.norm(basis, axis=(0, 1)) - gram = np.tensordot(basis, basis.conj(), axes=((0, 1), (0, 1))) - self._cobasis[side] = np.tensordot(basis.conj(), np.linalg.inv(gram), 1) + # As a basis matrix B is full rank, the cobasis is equivalent to the Moore-Penrose pseudo-inverse B⁺. + # B⁺ = (B^* B)⁻¹ B^* + # Where B is the basis matrix, ^* denotes the conjugate transpose, and ^(-1) + # denotes the matrix inverse. Note: we store the cobasis in transposed form, + # with shape (mode, y, x) to allow for easy + # multiplication with the transmission matrix t_ba. + basis = self.amplitude * mask * np.exp(1j * patterns) + basis /= np.linalg.norm(basis, axis=(1, 2), keepdims=True) + gram = np.tensordot(basis, basis.conj(), axes=((1, 2), (1, 2))) # inner product (contracts x and y axis) + self._cobasis[side] = np.tensordot(np.linalg.inv(gram), basis.conj(), 1) self._gram[side] = gram @property def cobasis(self) -> tuple[nd, nd]: """ The cobasis corresponding to the given basis. + + Note: The cobasis is stored in transposed form, with shape = (mode_count, height, width) """ return tuple(self._cobasis) @property - def gram(self) -> np.matrix: + def gram(self) -> tuple[nd, nd]: """ The Gram matrix corresponding to the given basis (i.e. phase pattern and amplitude profile). """ - return self._gram + return tuple(self._gram) def execute(self, *, capture_intermediate_results: bool = False, progress_bar=None) -> WFSResult: """ @@ -226,7 +228,8 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No if self.optimized_reference: # use the best estimate so far to construct an optimized reference - t_this_side = self.compute_t_set(results_all[it].t, self.cobasis[side]).squeeze() + # TODO: see if the squeeze can be removed + t_this_side = self.compute_t_set(results_all[it].t, side).squeeze() ref_phases[self.masks[side]] = -np.angle(t_this_side[self.masks[side]]) # Try full pattern @@ -234,6 +237,13 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No self.slm.set_phases(ref_phases) intermediate_results[it] = self.feedback.read() + if self.iterations % 2 == 0: + t_side_0 = results_all[-2].t + t_side_1 = results_all[-1].t + else: + t_side_0 = results_all[-1].t + t_side_1 = results_all[-2].t + if self.optimized_reference: factor = 1.0 else: @@ -241,14 +251,10 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No # two halves of the wavefront together. For that, we need the # relative phase between the two sides, which we extract from # the measurements of the flat wavefronts. - relative = results_all[0].t[self._zero_indices[0], ...] + np.conjugate( - results_all[1].t[self._zero_indices[1], ...] - ) + relative = t_side_0[self._zero_indices[0]] + np.conjugate(t_side_1[self._zero_indices[1]]) factor = (relative / np.abs(relative)).reshape((1, *self.feedback.data_shape)) - t_full = self.compute_t_set(results_all[0].t, self.cobasis[0]) + self.compute_t_set( - factor * results_all[1].t, self.cobasis[1] - ) + t_full = self.compute_t_set(t_side_0, 0) + self.compute_t_set(factor * t_side_1, 1) # Compute average fidelity factors # subtract 1 from n, because both sets (usually) contain a flat wavefront, @@ -267,8 +273,8 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, Conducts experiments on one part of the SLM. Args: - mod_phases: 3D array containing the phase patterns of each mode. Axis 0 and 1 are used as spatial axis. - Axis 2 is used for the 'phase pattern index' or 'mode index'. + mod_phases: 3D array containing the phase patterns of each mode. + ``shape = mode_count × height × width`` ref_phases: 2D array containing the reference phase pattern. mod_mask: 2D array containing a boolean mask, where True indicates the modulated part of the SLM. progress_bar: Optional progress bar object. Following the convention for tqdm progress bars, @@ -277,12 +283,10 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, Returns: WFSResult: An object containing the computed SLM transmission matrix and related data. """ - num_modes = mod_phases.shape[2] - measurements = np.zeros((num_modes, self.phase_steps, *self.feedback.data_shape)) + measurements = np.zeros((len(mod_phases), self.phase_steps, *self.feedback.data_shape)) - for m in range(num_modes): + for m, modulated in enumerate(mod_phases): phases = ref_phases.copy() - modulated = mod_phases[:, :, m] for p in range(self.phase_steps): phi = p * 2 * np.pi / self.phase_steps # set the modulated pixel values to the values corresponding to mode m and phase offset phi @@ -296,22 +300,15 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, self.feedback.wait() return self._analyzer(measurements, axis=1) - @staticmethod - def compute_t_set(t, cobasis: nd) -> nd: + def compute_t_set(self, t, side) -> nd: """ Compute the transmission matrix in SLM space from transmission matrix in input mode space. - Note 1: This function computes the transmission matrix for one mode set, and thus returns one part of the full - transmission matrix. The elements that are not part of the mode set will be 0. The full transmission matrix can - be obtained by simply adding the parts, i.e. t_full = t_set0 + t_set1. - - Note 2: As this is a blind focusing WFS algorithm, there may be only one target or 'output mode'. + TODO: make sure t is in t_ba form, so that this is equivalent to np.tensordot(t, cobasis, 1) Args: t: transmission matrix in mode-index space. The first axis corresponds to the input modes. - cobasis: 3D array with set of modes (conjugated) Returns: nd: The transmission matrix in SLM space. The last two axes correspond to SLM coordinates """ - norm_factor = np.prod(cobasis.shape[0:2]) - return np.tensordot(cobasis, t, 1) / norm_factor + return np.tensordot(self.cobasis[side], t, ((0,), (0,))) diff --git a/tests/test_wfs.py b/tests/test_wfs.py index f906553..3e2afdc 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -15,15 +15,14 @@ ) 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 import SimulatedWFS, StaticSource, SLM, Microscope from ..openwfs.simulation.mockdevices import GaussianNoise, Camera +@pytest.mark.parametrize("algorithm", ["ssa", "fourier"]) @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: tuple[int, int], noise: float, algorithm: str): """ Test the multi-target capable algorithms (SSA and Fourier dual ref). @@ -61,7 +60,7 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm k_radius=(np.min(shape) - 1) // 2, phase_steps=phase_steps, ) - N = alg.phase_patterns[0].shape[2] + alg.phase_patterns[1].shape[2] # number of input modes + N = len(alg.phase_patterns[0]) + len(alg.phase_patterns[1]) # number of input modes alg_fidelity = 1.0 # Fourier is accurate for any N signal = 1 / 2 # for estimating SNR. masks = alg.masks # use separate halves of the segments for determining fidelity @@ -426,24 +425,24 @@ def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): """Test dual reference in iterative mode with an orthonormal phase-only basis. Two types of bases are tested: plane waves and Hadamard""" N = shape[0] * (shape[1] // 2) - modes_shape = (shape[0], shape[1] // 2, N) + modes_shape = (N, shape[0], shape[1] // 2) if basis_str == "plane_wave": # Create a full plane wave basis for one half of the SLM. - phases = np.angle(np.fft.fft2(np.eye(N).reshape(modes_shape), axes=(0, 1))) + phases = np.angle(np.fft.fft2(np.eye(N).reshape(modes_shape), axes=(1, 2))) elif basis_str == "hadamard": phases = np.angle(hadamard(N).reshape(modes_shape)) else: raise f'Unknown type of basis "{basis_str}".' mask = half_mask(shape) - phases_set = np.concatenate((phases, np.zeros(shape=modes_shape)), axis=1) + phases_set = np.pad(phases, ((0, 0), (0, 0), (0, shape[1] // 2))) sim = SimulatedWFS(t=random_transmission_matrix(shape)) alg = DualReference( feedback=sim, slm=sim.slm, - phase_patterns=(phases_set, np.flip(phases_set, axis=1)), + phase_patterns=(phases_set, np.flip(phases_set, axis=2)), group_mask=mask, iterations=4, ) @@ -467,34 +466,21 @@ def test_dual_reference_non_ortho_split(): """ Test dual reference with a non-orthogonal basis. """ - do_debug = False - # Create set of modes that are barely linearly independent N1 = 6 N2 = 3 M = N1 * N2 - mode_set_half = np.exp(2j * np.pi / 3 * np.eye(M).reshape((N1, N2, M))) / np.sqrt(M) - mode_set = np.concatenate((mode_set_half, np.zeros(shape=(N1, N2, M))), axis=1) + mode_set_half = np.exp(2j * np.pi / 3 * np.eye(M).reshape((N2, N1, M))).T / np.sqrt(M) + + # note: typically we just use the other half for the mode set B, but here we set the half to 0 to + # make sure it is not used. + mode_set = np.pad(mode_set_half, ((0, 0), (0, 0), (0, N2))) phases_set = np.angle(mode_set) - mask = np.concatenate((np.zeros((N1, N2)), np.ones((N1, N2))), axis=1) - - if do_debug: - # Plot the modes - import matplotlib.pyplot as plt - - plt.figure(figsize=(12, 7)) - for m in range(M): - plt.subplot(N2, N1, m + 1) - plot_field(mode_set[:, :, m]) - plt.title(f"m={m}") - plt.xticks([]) - plt.yticks([]) - plt.pause(0.01) - plt.suptitle("Phase of basis functions for one half") + mask = half_mask((N1, 2 * N2)) # Create aberrations - x = np.linspace(-1, 1, 1 * N1).reshape((1, -1)) - y = np.linspace(-1, 1, 1 * N1).reshape((-1, 1)) + x = np.linspace(-1, 1, N1).reshape((1, -1)) + y = np.linspace(-1, 1, N1).reshape((-1, 1)) aberrations = (np.sin(0.8 * np.pi * x) * np.cos(1.3 * np.pi * y) * (0.8 * np.pi + 0.4 * x + 0.4 * y)) % (2 * np.pi) aberrations[0:1, :] = 0 aberrations[:, 0:2] = 0 @@ -504,7 +490,7 @@ def test_dual_reference_non_ortho_split(): alg = DualReference( feedback=sim, slm=sim.slm, - phase_patterns=(phases_set, np.flip(phases_set, axis=1)), + phase_patterns=(phases_set, np.flip(phases_set, axis=2)), group_mask=mask, phase_steps=4, iterations=4, @@ -515,31 +501,4 @@ def test_dual_reference_non_ortho_split(): aberration_field = np.exp(1j * aberrations) 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) - plot_field(alg.cobasis[0][:, :, m], scale=2) - plt.title(f"{m}") - plt.suptitle("Cobasis") - plt.pause(0.01) - - plt.figure() - plt.imshow(abs(alg.gram), vmin=0, vmax=1) - plt.title("Gram matrix abs values") - plt.colorbar() - - plt.figure() - plt.subplot(1, 2, 1) - plot_field(aberration_field) - plt.title("Aberrations") - - plt.subplot(1, 2, 2) - plot_field(t_field) - plt.title("t") - plt.show() - assert np.abs(field_correlation(aberration_field, t_field)) > 0.999 From 2aa4bd9e489690c2b8000f0d507a52f0f2919947 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Sun, 6 Oct 2024 21:30:49 +0200 Subject: [PATCH 19/37] swapped order of transmission matrix axes, now t_ba --- openwfs/algorithms/dual_reference.py | 8 +-- openwfs/algorithms/utilities.py | 41 +++++++++------- openwfs/simulation/transmission.py | 9 ++-- tests/__init__.py | 8 +++ tests/test_algorithms_troubleshoot.py | 23 +++++++++ tests/test_wfs.py | 71 ++++++++++++++------------- 6 files changed, 100 insertions(+), 60 deletions(-) diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index f4fb1c5..0eb882d 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -251,8 +251,8 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No # two halves of the wavefront together. For that, we need the # relative phase between the two sides, which we extract from # the measurements of the flat wavefronts. - relative = t_side_0[self._zero_indices[0]] + np.conjugate(t_side_1[self._zero_indices[1]]) - factor = (relative / np.abs(relative)).reshape((1, *self.feedback.data_shape)) + relative = t_side_0[..., self._zero_indices[0]] + np.conjugate(t_side_1[..., self._zero_indices[1]]) + factor = np.expand_dims(relative / np.abs(relative), -1) t_full = self.compute_t_set(t_side_0, 0) + self.compute_t_set(factor * t_side_1, 1) @@ -304,11 +304,11 @@ def compute_t_set(self, t, side) -> nd: """ Compute the transmission matrix in SLM space from transmission matrix in input mode space. - TODO: make sure t is in t_ba form, so that this is equivalent to np.tensordot(t, cobasis, 1) + Equivalent to ``np.tensordot(t, cobasis[side], 1)`` Args: t: transmission matrix in mode-index space. The first axis corresponds to the input modes. Returns: nd: The transmission matrix in SLM space. The last two axes correspond to SLM coordinates """ - return np.tensordot(self.cobasis[side], t, ((0,), (0,))) + return np.tensordot(t, self.cobasis[side], 1) diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 235cd35..86e46c1 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -154,9 +154,9 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): Args: measurements(ndarray): array of phase stepping measurements. The array holds measured intensities - with the first one or more dimensions corresponding to the segments(pixels) of the SLM, + with the first one or more dimensions (a1, a2, ...) corresponding to the segments of the SLM, one dimension corresponding to the phase steps, - and the last zero or more dimensions corresponding to the individual targets + and the last zero or more dimensions (b1, b2, ...) corresponding to the individual targets where the feedback was measured. axis(int): indicates which axis holds the phase steps. @@ -172,16 +172,14 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): \\frac{1}{phase_{steps}} \\sum I_p \\exp(-i 2\\pi p / phase_{steps}) = A^* B - The value of A^* B for each set of measurements is stored in the `field` attribute of the return - value. - Other attributes hold an estimate of the signal-to-noise ratio, - and an estimate of the maximum enhancement that can be expected - if these measurements are used for wavefront shaping. + Returns: + WFSResult: The result of the analysis. The attribute `t` holds the complex transmission matrix. + Note that the dimensions of t are reversed with respect to the input, so t has shape b1×b2×...×a1×a2×... + Other attributes hold fidelity estimates (see WFSResult). """ phase_steps = measurements.shape[axis] - n = int(np.prod(measurements.shape[:axis])) - # M = np.prod(measurements.shape[axis + 1:]) - segments = tuple(range(axis)) + a_count = int(np.prod(measurements.shape[:axis])) + a_axes = tuple(range(axis)) # Fourier transform the phase stepping measurements t_f = np.fft.fft(measurements, axis=axis) / phase_steps @@ -189,7 +187,7 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): # compute the effect of amplitude variations. # for perfectly developed speckle, and homogeneous illumination, this factor will be pi/4 - amplitude_factor = np.mean(np.abs(t), segments) ** 2 / np.mean(np.abs(t) ** 2, segments) + fidelity_amplitude = np.mean(np.abs(t), a_axes) ** 2 / np.mean(np.abs(t) ** 2, a_axes) # estimate the calibration error # we first construct a matrix that can be used to fit @@ -203,6 +201,9 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): cc = ff_inv @ np.take(t_f, k, axis=axis).ravel() signal_energy = signal_energy + np.sum(np.abs(ff @ cc) ** 2) c[k] = cc[0] + fidelity_calibration = np.abs(c[1]) ** 2 / np.sum(np.abs(c[1:]) ** 2) + + # TODO: use the pinv fit to estimate the signal strength (requires special treatment of offset) # Estimate the error due to noise # The signal consists of the response with incorrect modulation, @@ -212,26 +213,28 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): # average over all targets to get the most accurate result (assuming all targets are similar) axes = tuple([i for i in range(t_f.ndim) if i != axis]) energies = np.sum(np.abs(t_f) ** 2, axis=axes) - offset_energy = energies[0] total_energy = np.sum(energies) + offset_energy = energies[0] signal_energy = energies[1] + energies[-1] if phase_steps > 3: # estimate the noise energy as the energy that is not explained # by the signal or the offset. noise_energy = (total_energy - signal_energy - offset_energy) / (phase_steps - 3) - noise_factor = np.abs(np.maximum(signal_energy - 2 * noise_energy, 0.0) / signal_energy) + fidelity_noise = np.abs(np.maximum(signal_energy - 2 * noise_energy, 0.0) / signal_energy) else: - noise_factor = 1.0 # cannot estimate reliably + fidelity_noise = 1.0 # cannot estimate reliably - calibration_fidelity = np.abs(c[1]) ** 2 / np.sum(np.abs(c[1:]) ** 2) + # convert from t_ab to t_ba form + a_destination = np.array(a_axes) + t.ndim - len(a_axes) + t = np.moveaxis(t, a_axes, a_destination) return WFSResult( t, axis=axis, - fidelity_amplitude=amplitude_factor, - fidelity_noise=noise_factor, - fidelity_calibration=calibration_fidelity, - n=n, + fidelity_amplitude=fidelity_amplitude, + fidelity_noise=fidelity_noise, + fidelity_calibration=fidelity_calibration, + n=a_count, ) diff --git a/openwfs/simulation/transmission.py b/openwfs/simulation/transmission.py index 99eeaa1..bb52d3f 100644 --- a/openwfs/simulation/transmission.py +++ b/openwfs/simulation/transmission.py @@ -36,7 +36,8 @@ def __init__( Gaussian beam. Args: - t: Transmission matrix. + t: Transmission matrix. Must have the form (*feedback_shape, height, width), where feedback_shape + is the shape of the feedback signal and may be 0 or more dimensional. aberrations: An array containing the aberrations in radians. Can be used instead of a transmission matrix, equivalent to specifying ``t = np.exp(1j * aberrations) / (aberrations.shape[0] * aberrations.shape[1])``. slm: @@ -51,7 +52,7 @@ def __init__( # transmission matrix (normalized so that the maximum transmission is 1) self.t = t if t is not None else np.exp(1.0j * aberrations) / (aberrations.shape[0] * aberrations.shape[1]) - self.slm = slm if slm is not None else SLM(self.t.shape[0:2]) + self.slm = slm if slm is not None else SLM(self.t.shape[-2:]) super().__init__(self.slm.field, multi_threaded=multi_threaded) self.beam_amplitude = beam_amplitude @@ -71,9 +72,9 @@ def _fetch(self, incident_field): # noqa np.ndarray: A numpy array containing the calculated intensity in the focus. """ - field = np.tensordot(incident_field * self.beam_amplitude, self.t, 2) + field = np.tensordot(self.t, incident_field * self.beam_amplitude, 2) return np.abs(field) ** 2 @property def data_shape(self): - return self.t.shape[2:] + return self.t.shape[:-2] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..ac52d7f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,8 @@ +import numpy as np + + +def complex_random(shape): + """ + Create an array with normally distributed complex elements with variance 1. + """ + return np.sqrt(0.5) * (np.random.normal(size=shape) + 1j * np.random.normal(size=shape)) diff --git a/tests/test_algorithms_troubleshoot.py b/tests/test_algorithms_troubleshoot.py index 130a20f..fd83257 100644 --- a/tests/test_algorithms_troubleshoot.py +++ b/tests/test_algorithms_troubleshoot.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from . import complex_random from .test_simulation import phase_response_test_function, lookup_table_test_function from ..openwfs.algorithms import StepwiseSequential from ..openwfs.algorithms.troubleshoot import ( @@ -14,11 +15,33 @@ measure_modulated_light, measure_modulated_light_dual_phase_stepping, ) +from ..openwfs.algorithms.utilities import analyze_phase_stepping from ..openwfs.processors import SingleRoi from ..openwfs.simulation import Camera from ..openwfs.simulation import SimulatedWFS, StaticSource, SLM, Microscope +@pytest.mark.parametrize("phase_steps", [5, 6, 10, 20]) +@pytest.mark.parametrize("noise", [0.0, 0.5, 1.0, 2.0, 4.0]) +def test_analyze_phase_stepping(phase_steps, noise): + """Test the analyze_phase_stepping function""" + # TODO: find out why there is a (small) systematic error when the noise is high + # Construct a perfect phase stepping signal + np.random.seed(123) + t = complex_random((10000, 1)) + ref = np.exp(np.arange(phase_steps) * 2j * np.pi / phase_steps) + signal = np.abs(ref + t) ** 2 + signal += np.random.normal(scale=noise, size=signal.shape) + result = analyze_phase_stepping(signal, axis=1) + # the signal energy for a signal 2·cos(φ) is 2 P (with P the number of phase steps), distributed over 2 bins, + # giving P per bin. + # the noise energy is σ²·P, distributed over P bins, giving σ² per bin. + tolerance = 4 / np.sqrt(t.size) + theoretical_fidelity = phase_steps / (phase_steps + noise**2) + print(f"noise fidelity. theoretical: {theoretical_fidelity} measured: {result.fidelity_noise}") + assert np.isclose(result.fidelity_noise, theoretical_fidelity, rtol=tolerance) + + def test_signal_std(): """ Test signal std, corrected for (uncorrelated) noise in the signal. diff --git a/tests/test_wfs.py b/tests/test_wfs.py index 3e2afdc..1057090 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -7,6 +7,7 @@ from scipy.linalg import hadamard from scipy.ndimage import zoom +from . import complex_random from ..openwfs.algorithms import ( StepwiseSequential, FourierDualReference, @@ -20,10 +21,15 @@ from ..openwfs.simulation.mockdevices import GaussianNoise, Camera +@pytest.fixture(autouse=True) +def reset(): + np.random.seed(42) # for reproducibility + + @pytest.mark.parametrize("algorithm", ["ssa", "fourier"]) -@pytest.mark.parametrize("shape", [(4, 7), (10, 7), (20, 31)]) +@pytest.mark.parametrize("shape, feedback_shape", [((4, 7), (210,)), ((10, 7), (21, 10)), ((20, 31), (3, 7, 10))]) @pytest.mark.parametrize("noise", [0.0, 0.1]) -def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm: str): +def test_multi_target_algorithms(shape: tuple[int, int], feedback_shape: tuple[int, ...], noise: float, algorithm: str): """ Test the multi-target capable algorithms (SSA and Fourier dual ref). @@ -31,11 +37,11 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm and it also verifies that the enhancement and noise fidelity are estimated correctly by the algorithm. """ - M = 100 # number of targets + M = np.prod(feedback_shape) # number of targets phase_steps = 6 # create feedback object, with noise if needed - sim = SimulatedWFS(t=random_transmission_matrix((*shape, M))) + sim = SimulatedWFS(t=complex_random((*feedback_shape, *shape))) sim.slm.set_phases(0.0) I_0 = np.mean(sim.read()) feedback = GaussianNoise(sim, std=I_0 * noise) @@ -49,7 +55,10 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm phase_steps=phase_steps, ) N = np.prod(shape) # number of input modes - alg_fidelity = (N - 1) / N # SSA is inaccurate if N is low + # SSA is inaccurate if N is slightly lower because the reference beam varies with each segment. + # The variation is of order 1/N in the signal, so the fidelity is (N-1)/N. + # todo: check! + alg_fidelity = (N - 1) / N signal = (N - 1) / N**2 # for estimating SNR masks = [True] # use all segments for determining fidelity else: # 'fourier': @@ -78,23 +87,26 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm # For the dual reference algorithm, the left and right half will have a different overall amplitude factor. # This is corrected for by computing the correlations for the left and right half separately # - I_opt = np.zeros((M,)) - for b in range(M): - sim.slm.set_phases(-np.angle(result.t[:, :, b])) + I_opt = np.zeros(feedback_shape) + for b in np.ndindex(feedback_shape): + sim.slm.set_phases(-np.angle(result.t[b])) I_opt[b] = feedback.read()[b] - t_correlation = t_fidelity(result.t, sim.t, masks) + t_correlation = t_fidelity(result.t, sim.t, masks=masks) # Check the enhancement, noise fidelity and # the fidelity of the transmission matrix reconstruction coverage = N / np.prod(shape) - theoretical_noise_fidelity = signal / (signal + noise**2 / phase_steps) + print(signal) + print(noise) + theoretical_noise_fidelity = signal * phase_steps / (signal * phase_steps + noise**2) + enhancement = I_opt.mean() / I_0 theoretical_enhancement = np.pi / 4 * theoretical_noise_fidelity * alg_fidelity * (N - 1) + 1 estimated_enhancement = result.estimated_enhancement.mean() * alg_fidelity theoretical_t_correlation = theoretical_noise_fidelity * alg_fidelity * coverage estimated_t_correlation = result.fidelity_noise * result.fidelity_calibration * alg_fidelity * coverage - tolerance = 2.0 / np.sqrt(M) + tolerance = 4.0 / np.sqrt(M) # TODO: find out if this should be stricter print( f"\nenhancement: \ttheoretical= {theoretical_enhancement},\testimated={estimated_enhancement},\tactual: {enhancement}" ) @@ -135,14 +147,6 @@ def test_multi_target_algorithms(shape: tuple[int, int], noise: float, algorithm Expected {theoretical_noise_fidelity}, got {result.fidelity_noise}""" -def random_transmission_matrix(shape): - """ - Create a random transmission matrix with the given shape. - """ - np.random.seed(42) # for reproducibility - return np.random.normal(size=shape) + 1j * np.random.normal(size=shape) - - def half_mask(shape): """ Args: @@ -157,7 +161,9 @@ def half_mask(shape): return mask -def t_fidelity(t: np.ndarray, t_correct: np.ndarray, masks: Optional[tuple[np.ndarray, ...]] = (True,)) -> float: +def t_fidelity( + t: np.ndarray, t_correct: np.ndarray, *, columns: int = 2, masks: Optional[tuple[np.ndarray, ...]] = (True,) +) -> float: """ Compute the fidelity of the measured transmission matrix. @@ -171,14 +177,15 @@ def t_fidelity(t: np.ndarray, t_correct: np.ndarray, masks: Optional[tuple[np.nd Args: t: The measured transmission matrix. 'rows' of t are the *last* index t_correct: The correct transmission matrix + columns: The number of columns in the transmission matrix masks: Masks for the left and right half of the wavefront, or None to use the full wavefront """ fidelity = 0.0 norm = 0.0 - for r in range(t.shape[-1]): # each row + for r in np.ndindex(t.shape[:-columns]): # each row for m in masks: - rm = t[..., r][m] - rm_c = t_correct[..., r][m] + rm = t[r][m] + rm_c = t_correct[r][m] fidelity += abs(np.vdot(rm, rm_c) ** 2 / np.vdot(rm, rm)) norm += np.vdot(rm_c, rm_c) @@ -320,9 +327,7 @@ def test_flat_wf_response_fourier(optimized_reference, type): assert ( abs(field_correlation(result.t / np.abs(result.t), t / np.abs(t))) > 0.99 ), "The phases were not calculated correctly" - assert ( - t_fidelity(np.expand_dims(result.t, -1), np.expand_dims(t, -1), alg.masks) > 0.99 - ), "The amplitudes were not calculated correctly" + assert t_fidelity(result.t, t, masks=alg.masks) > 0.99, "The amplitudes were not calculated correctly" def test_flat_wf_response_ssa(): @@ -344,7 +349,7 @@ def test_flat_wf_response_ssa(): def test_multidimensional_feedback_ssa(): - aberrations = np.random.uniform(0.0, 2 * np.pi, (256, 256, 5, 2)) + aberrations = np.random.uniform(0.0, 2 * np.pi, (5, 2, 256, 256)) sim = SimulatedWFS(aberrations=aberrations) alg = StepwiseSequential(feedback=sim, slm=sim.slm) @@ -352,7 +357,7 @@ def test_multidimensional_feedback_ssa(): # compute the phase pattern to optimize the intensity in target 2,1 target = (2, 1) - optimised_wf = -np.angle(t[(..., *target)]) + optimised_wf = -np.angle(t[*target, ...]) # Calculate the enhancement factor # Note: technically this is not the enhancement, just the ratio after/before @@ -369,7 +374,7 @@ def test_multidimensional_feedback_ssa(): def test_multidimensional_feedback_fourier(): - aberrations = np.random.uniform(0.0, 2 * np.pi, (256, 256, 5, 2)) + aberrations = np.random.uniform(0.0, 2 * np.pi, (5, 2, 256, 256)) sim = SimulatedWFS(aberrations=aberrations) # input the camera as a feedback object, such that it is multidimensional @@ -377,7 +382,7 @@ def test_multidimensional_feedback_fourier(): t = alg.execute().t # compute the phase pattern to optimize the intensity in target 0 - optimised_wf = -np.angle(t[:, :, 2, 1]) + optimised_wf = -np.angle(t[2, 1, :, :]) # Calculate the enhancement factor # Note: technically this is not the enhancement, just the ratio after/before @@ -400,7 +405,7 @@ def test_simple_genetic(population_size: int, elite_size: int): Note: this is not very rigid test, as we currently don't have theoretical expectations for the performance. """ shape = (100, 71) - sim = SimulatedWFS(t=random_transmission_matrix(shape), multi_threaded=False) + sim = SimulatedWFS(t=complex_random(shape), multi_threaded=False) alg = SimpleGenetic( feedback=sim, slm=sim.slm, @@ -437,7 +442,7 @@ def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): mask = half_mask(shape) phases_set = np.pad(phases, ((0, 0), (0, 0), (0, shape[1] // 2))) - sim = SimulatedWFS(t=random_transmission_matrix(shape)) + sim = SimulatedWFS(t=complex_random(shape)) alg = DualReference( feedback=sim, @@ -459,7 +464,7 @@ def test_dual_reference_ortho_split(basis_str: str, shape: tuple[int, int]): result_t_phase_only = np.exp(1j * np.angle(result.t)) assert np.abs(field_correlation(sim_t_phase_only, result_t_phase_only)) > 0.999 - assert t_fidelity(np.expand_dims(result.t, -1), np.expand_dims(sim.t, -1), alg.masks) > 0.9 + assert t_fidelity(result.t, sim.t, masks=alg.masks) > 0.9 def test_dual_reference_non_ortho_split(): From 03ccf5af5153ea3dc23073a2673d81701e1dcc7b Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Mon, 7 Oct 2024 14:12:43 +0200 Subject: [PATCH 20/37] reordered sections, editing documentation --- docs/source/conclusion.rst | 2 +- docs/source/conf.py | 27 +-- docs/source/core.rst | 166 +++++++++--------- docs/source/index.rst | 3 +- docs/source/index_latex.rst | 5 +- .../source/{pydevice.rst => micromanager.rst} | 4 +- docs/source/readme.rst | 49 ++---- docs/source/references.bib | 50 ++++-- docs/source/troubleshooting.rst | 22 +++ 9 files changed, 182 insertions(+), 146 deletions(-) rename docs/source/{pydevice.rst => micromanager.rst} (95%) create mode 100644 docs/source/troubleshooting.rst diff --git a/docs/source/conclusion.rst b/docs/source/conclusion.rst index 73d5843..855b48a 100644 --- a/docs/source/conclusion.rst +++ b/docs/source/conclusion.rst @@ -5,7 +5,7 @@ In this work we presented an open-source Python package for conducting and simul OpenWFS incorporates features to reduce the chances of errors in the design of wavefront shaping code. Notably, the use of units of measure prevents the accidental mixing of units, and the automatic synchronization mechanism ensures that hardware is properly synchronized without the need to write any synchronization code. Finally, the ability to simulate full experiments, and to mock individual components, allows the user to test wavefront shaping algorithms without the need for physical hardware. We find this feature particularly useful since there is a lot that can go wrong in an experiment, (also see :cite:`Mastiani2024PracticalConsiderations`), and experimental issues and software issues are not always easy to distinguish. With OpenWFS, it is now possible to fully test the algorithms before entering the lab, which can save a lot of time and frustration. -We envision that OpenWFS will hold a growing collection of components for hardware control, advanced simulations, and wavefront shaping. The standardised interfaces for detectors, actuators and SLMs, enables the cooperative development of complex functionality. Additionally, standardized components and algorithms will greatly simplify developing reusable code that can be used across different setups and experiments. The simulation tools may additionally be used for research and education, ushering in a new phase of applications in wavefront shaping. We therefore encourage the reader to join us in developing new algorithms and components for this framework. +We envision that OpenWFS will hold a growing collection of components for hardware control, advanced simulations, and wavefront shaping. Further expansion of the supported hardware is of high priority, especially wrapping c-based libraries and adding support for Micro-Manager device adapters. The standardised interfaces for detectors, actuators and SLMs will greatly simplify developing reusable code that can be used across different setups and experiments. The simulation tools may additionally be used for research and education, ushering in a new phase of applications in wavefront shaping. We therefore encourage the reader to join us in developing new algorithms and components for this framework. Code availability ------------------------------------------------ diff --git a/docs/source/conf.py b/docs/source/conf.py index 2022b77..6a05b01 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -43,6 +43,8 @@ latex_elements = { "preamble": r""" \usepackage{authblk} + \usepackage{etoolbox} % Reduce font size for all tables + \AtBeginEnvironment{tabular}{\small} """, "maketitle": r""" \author[1]{Daniël~W.~S.~Cox} @@ -61,16 +63,21 @@ this research field is expanding rapidly. As the field advances, it stands out that many breakthroughs are driven by the development of better software that incorporates increasingly advanced physical models and algorithms. - Typical control software involves fragmented implementations for scanning microscopy, image processing, - optimization algorithms, low-level hardware control, calibration and troubleshooting, - and simulations for testing new algorithms. - - The complexity of the many different aspects of wavefront shaping software, however, - is becoming a limiting factor for further developments in the field, as well as for end-user adoption. - OpenWFS addresses these challenges by providing a modular and extensible Python library that - incorporates elements for hardware control, software simulation, and automated troubleshooting. - Using these elements, the actual wavefront shaping algorithm and its automated tests can be written - in just a few lines of code. + + Typical WFS software involves a complex combination of low-level hardware control, signal processing, + calibration, troubleshooting, simulation, and the wavefront shaping algorithm itself. + This complexity makes it hard to compare different algorithms and to extend existing software with new + hardware or algorithms. Moreover, the complexity of the software can be a significant barrier for end + users of microscopes to adopt wavefront shaping. + + OpenWFS addresses these challenges by providing a modular Python library that + separates hardware control from the wavefront shaping algorithm itself. + Using these elements, an wavefront shaping algorithm can be written + in just a few lines of code, with OpenWFS taking care of low-level hardware control, synchronization, + and troubleshooting. Algorithms can be used on different hardware or in a completely + simulated environment without changing the code. Moreover, we provide full integration with + the \textmu Manager microscope control software, enabling wavefront shaping experiments to be + executed from a user-friendly graphical user interface. } } \maketitle diff --git a/docs/source/core.rst b/docs/source/core.rst index ede5a49..293a397 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -10,7 +10,27 @@ In addition, OpenWFS maintains metadata and units for all data arrays and proper Detectors ------------ -Detectors in OpenWFS are objects that capture, generate, or process data. All detectors derive from the :class:`~.Detector` base class. A Detector object may correspond to a physical device such as a camera, or it may be a software component that generates synthetic data (see :numref:`section-simulations`). Detectors have the following properties and methods: +Detectors in OpenWFS are objects that capture, generate, or process data. A Detector object may correspond to a physical device such as a camera, or it may be a software component that generates synthetic data (see :numref:`section-simulations`). Currently, the following detectors are supported: + +.. list-table:: + :header-rows: 1 + + * - Detector + - + * - Camera + - Supports all GenICam/GenTL cameras. + * - ScanningMicroscope + - Laser scanning microscope using galvo mirrors and National Instruments data acquisition card. + * - SimulatedWFS + - Simulated detector for testing wavefront shaping algorithms. + * - Microscope + - Fully simulated microscope, including aberrations, diffraction limit, and translation stage. + * - StaticSource + - Returns pre-set data, simulating a static source. + * - NoiseSource + - Generates uniform or Gaussian noise as a source. + +All detectors derive from the :class:`~.Detector` base class. and have the following properties and methods: .. code-block:: python @@ -27,7 +47,7 @@ Detectors in OpenWFS are objects that capture, generate, or process data. All de def coordinates(dimension: int) -> Quantity -The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as `numpy` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e.g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`.~Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. +The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as ``numpy`` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e.g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`.~Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. The detector object inherits some properties and methods from the base class :class:`~.Device`. These are used by the synchronization mechanism to determine when it is safe to start a measurement, as described in :numref:`device-synchronization`. @@ -36,7 +56,7 @@ Asynchronous measurements +++++++++++++++++++++++++++ :meth:`.~Detector.read()` blocks the program until the captured data is available. This behavior is not ideal when multiple detectors are used simultaneously, or when transferring or processing the data takes a long time. In these cases, it is preferable to use :meth:`.~Detector.trigger()`, which initiates the process of capturing or generating data and returns directly. The program can continue operation while the data is being captured/transferred/generated in a worker thread. While fetching and processing data is underway, any attempt to modify a property of the detector will block until the fetching and processing is complete. This way, all properties (such as the region of interest) are guaranteed to be constant between the calls to :meth:`.~Detector.trigger` and the moment the data is actually fetched and processed in the worker thread. -The asynchronous measurement mechanism can be seen in action in the `StepwiseSequential` algorithm used in :numref:`hello-wfs`. The `execute()` function of this algorithm is implemented as +The asynchronous measurement mechanism can be seen in action in the :class:`.~StepwiseSequential` algorithm used in :numref:`hello-wfs`. The :meth:`execute() ` function of this algorithm is implemented as .. code-block:: python @@ -55,7 +75,7 @@ The asynchronous measurement mechanism can be seen in action in the `StepwiseSeq self.feedback.wait() return analyze_phase_stepping(measurements, axis=2) -This code performs a wavefront shaping algorithm similar to the one described in :cite:`Vellekoop2007`. In this version, there is no pre-optimization. It works by cycling the phase of each of the n_x × n_y segments on the SLM between 0 and 2π, and measuring the feedback signal at each step. `self.feedback` holds a `Detector` object that is triggered, and stores the measurement in a pre-allocated `measurements` array when it becomes available. It is possible to find the optimized wavefront for multiple targets simultaneously by using a detector that returns an array of size `feedback.data_shape`, which contains a feedback value for each of the targets. +This code performs a wavefront shaping algorithm similar to the one described in :cite:`Vellekoop2007`. In this version, there is no pre-optimization. It works by cycling the phase of each of the ``n_x × n_y`` segments on the SLM between 0 and 2π, and measuring the feedback signal at each step. ``self.feedback`` holds a :class:`~.Detector` object that is triggered, and stores the measurement in a pre-allocated ``measurements`` array when it becomes available. It is possible to find the optimized wavefront for multiple targets simultaneously by using a detector that returns an array of size ``feedback.data_shape``, which contains a feedback value for each of the targets. The program does not wait for the data to become available and can directly proceed with preparing the next pattern to send to the SLM (also see :numref:`device-synchronization`). After running the algorithm, `wait` is called to wait until all measurement data is stored in the `measurements` array, and the utility function `analyze_phase_stepping` is used to extract the transmission matrix from the measurements, as well as a series of troubleshooting statistics (see :numref:`Analysis and troubleshooting`). @@ -69,15 +89,68 @@ Note that, except for this asynchronous mechanism for fetching and processing da Processors ------------ -A `Processor` is a `Detector` that takes input from one or more other detectors, and combines/processes this data. We already encountered an example in :numref:`Getting started`, where the `SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. Since a processor, itself, is a `Detector`, multiple processors can be chained together to combine their functionality. The OpenWFS further includes various processors, such as a `CropProcessor` to crop data to a rectangular region of interest, and a `TransformProcessor` to perform affine image transformations to image produced by a source. +A :class:`.~Processor` is an object that takes input from one or more other detectors, and combines/processes this data. By itself, a processor is a :class:`.~Detector`, enabling multiple processors to be chained together to combine their functionality. We already encountered an example in :numref:`Getting started`, where the :class:`.~SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. The OpenWFS currently includes the following processors: + +.. list-table:: + :header-rows: 1 + + * - Processor + - + * - SingleRoi + - Averages signal over a single ROI. + * - MultipleRoi + - Averages signals over multiple regions of interest (ROIs). + * - CropProcessor + - Crops data from the source to a region of interest. + * - TransformProcessor + - Performs affine transformations on the source data. + * - GaussianNoise + - Adds Gaussian noise to the source data. + * - ADCProcessor + - Simulates an analog-digital converter, including optional shot-noise and readout noise. + Actuators --------- -Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators are derived from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. + +Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators are derived from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. A list of actuators currently supported by OpenWFS can be found in the table below. + +.. list-table:: + :header-rows: 1 + :name: supported-actuators + + * - SLM + - Controls and renders patterns on a Spatial Light Modulator (SLM) using OpenGL + * - simulation.SLM + - Simulates a phase-only spatial light modulator, including timing and non-linear phase response. + * - simulation.XYStage + - Simulates a translation stage, used in :class:`~Microscope`. + + +Algorithms +------------ +OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could be implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the the Micro-Manager graphical user interface (GUI), see :ref:`micromanager`. + +All algorithms are designed to be completely hardware-agnostic, so that they can be used with any type of feedback signal and either use real hardware or simulated hardware without the need to change a single line of code in the algorithm implementation. The :class:`.~FourierDualReference`, :class:`.~DualReference` and :class:`.~StepwiseSequential` algorithms provide support for optimizing multiple targets simulaneously in a single run of the algorithm. + +.. list-table:: + :header-rows: 1 + + * - Algorithm + - + * - FourierDualReference + - A dual reference algorithm that uses plane waves from a disk in k-space for wavefront shaping :cite:`Mastiani2022`. + * - DualReference + - A generic dual reference algorithm with a configurable basis set :cite:`Cox2024`. + * - SimpleGenetic + - A simple genetic algorithm for optimiziang wavefronts :cite:`Piestun2012`. + * - StepwiseSequential + - A simplified version of the original wavefront shaping algorithm :cite:`Vellekoop2007`, with pre-optimization omitted. + Units and metadata ---------------------------------- -OpenWFS consistently uses `astropy.units` :cite:`astropy` for quantities with physical dimensions, which allows for calculations to be performed with correct units, and for automatic unit conversion where necessary. Importantly, it prevents errors caused by passing a quantity in incorrect units, such as passing a wavelength in micrometers when the function expects a wavelength in nanometers. By using `astropy.units`, the quantities are converted automatically, so one may for example specify a time in milliseconds, minutes or days. The use of units is illustrated in the following snippet: +OpenWFS consistently uses ``astropy.units`` :cite:`astropy` for quantities with physical dimensions, which allows for calculations to be performed with correct units, and for automatic unit conversion where necessary. Importantly, it prevents errors caused by passing a quantity in incorrect units, such as passing a wavelength in micrometers when the function expects a wavelength in nanometers. By using ``astropy.units``, the quantities are converted automatically, so one may for example specify a time in milliseconds, minutes or days. The use of units is illustrated in the following snippet: .. code-block:: python @@ -89,7 +162,7 @@ OpenWFS consistently uses `astropy.units` :cite:`astropy` for quantities with ph In addition, OpenWFS allows attaching pixel-size metadata to data arrays using the functions :func:`~.set_pixel_size()`. Pixel sizes can represent a physical length (e.g. as in the size pixels on an image sensor), or other units such as time (e.g. as the sampling period in a time series). OpenWFS fully supports anisotropic pixels, where the pixel sizes in the x and y directions are different. -The data arrays returned by the :meth:`~.Detector.read()` function of a detector have `pixel_size` metadata attached whenever appropriate. The pixel size can be retrieved from the array using :func:`~.get_pixel_size()`, or obtained from the :attr:`~.Detector.pixel_size` attribute directly. As an alternative to accessing the pixel size directly, :func:`~get_extent()` and :class:`~.Detector.extent` provide access to the extent of the array, which is always equal to the pixel size times the shape of the array. Finally, the convenience function :meth:`~.Detector.coordinates` returns a vector of coordinates with appropriate units along a specified dimension of the array. +The data arrays returned by the :meth:`~.Detector.read()` function of a detector have ``pixel_size`` metadata attached whenever appropriate. The pixel size can be retrieved from the array using :func:`~.get_pixel_size()`, or obtained from the :attr:`~.Detector.pixel_size` attribute directly. As an alternative to accessing the pixel size directly, :func:`~get_extent()` and :class:`~.Detector.extent` provide access to the extent of the array, which is always equal to the pixel size times the shape of the array. Finally, the convenience function :meth:`~.Detector.coordinates` returns a vector of coordinates with appropriate units along a specified dimension of the array. .. _device-synchronization: @@ -115,9 +188,9 @@ Each device can either be *busy* or *ready*, and this state can be polled by cal - before starting a measurement, wait until all motion is (almost) completed - before starting any movement, wait until all measurements are (almost) completed -Here, 'almost' refers to the fact that devices may have a *latency*. Latency is the time between sending a command to a device, and the moment the device starts responding. An important example is the SLM, which typically takes one or two frame periods to transfer the image data to the liquid crystal chip. Such devices can specify a non-zero `latency` attribute. When specified, the device 'promises' not to do anything until `latency` milliseconds after the start of the measurement or movement. When a latency is specified, detectors or actuators can be started slightly before the devices of the other type (actuators or detectors, respectively) have finished their operation. For example, this mechanism allows sending a new frame to the SLM *before* the measurements of the current frame are finished, since it is known that the SLM will not respond for `latency` milliseconds anyway. This way, measurements and SLM updates can be pipelined to maximize the number of measurements that can be done in a certain amount of time. To enable these pipelined measurements, the `Device` class also provides a `duration` attribute, which is the maximum time interval between the start and end of a measurement or actuator action. +Here, 'almost' refers to the fact that devices may have a *latency*. Latency is the time between sending a command to a device, and the moment the device starts responding. An important example is the SLM, which typically takes one or two frame periods to transfer the image data to the liquid crystal chip. Such devices can specify a non-zero ``latency`` attribute. When specified, the device 'promises' not to do anything until ``latency`` milliseconds after the start of the measurement or movement. When a latency is specified, detectors or actuators can be started slightly before the devices of the other type (actuators or detectors, respectively) have finished their operation. For example, this mechanism allows sending a new frame to the SLM *before* the measurements of the current frame are finished, since it is known that the SLM will not respond for ``latency`` milliseconds anyway. This way, measurements and SLM updates can be pipelined to maximize the number of measurements that can be done in a certain amount of time. To enable these pipelined measurements, the ``Device`` class also provides a `duration` attribute, which is the maximum time interval between the start and end of a measurement or actuator action. -This synchronization is performed automatically. If desired, it is possible to explicitly wait for the device to become ready by calling :meth:`~.Device.wait()`. To accommodate taking into account the latency, this function takes an optional parameter `up_to`, which indicates that the function may return the specified time *before* the device hardware is ready. In user code, it is only necessary to call `wait` when using the `out` parameter to store measurements in a pre-defined location (see :numref:`Asynchronous measurements` above). A typical usage pattern is illustrated in the following snippet: +This synchronization is performed automatically. If desired, it is possible to explicitly wait for the device to become ready by calling :meth:`~.Device.wait()`. To accommodate taking into account the latency, this function takes an optional parameter ``up_to``, which indicates that the function may return the specified time *before* the device hardware is ready. In user code, it is only necessary to call ``wait`` when using the ``out`` parameter to store measurements in a pre-defined location (see :numref:`Asynchronous measurements` above). A typical usage pattern is illustrated in the following snippet: .. code-block:: python @@ -137,76 +210,5 @@ This synchronization is performed automatically. If desired, it is possible to e cam1.wait() # wait until camera 1 is done grabbing frames cam2.wait() # wait until camera 2 is done grabbing frames -Finally, devices have a `timeout` attribute, which is the maximum time to wait for a device to become ready. This timeout is used in the state-switching mechanism, and when explicitly waiting for results using :meth:`~.Device.wait()` or :meth:`~.Device.read()`. - -Currently available devices ----------------------------- - -The following devices are currently implemented in OpenWFS: - -.. list-table:: - :header-rows: 1 +Finally, devices have a ``timeout`` attribute, which is the maximum time to wait for a device to become ready. This timeout is used in the state-switching mechanism, and when explicitly waiting for results using :meth:`~.Device.wait()` or :meth:`~.Device.read()`. - * - Device Name - - Device Type - - Description - * - Camera - - Detector - - Adapter for GenICam/GenTL cameras - * - ScanningMicroscope - - Detector - - Laser scanning microscope using galvo mirrors and NI DAQ - * - StaticSource - - Detector - - Returns pre-set data, simulating a static source - * - NoiseSource - - Detector - - Generates uniform or Gaussian noise as a source - * - SingleRoi - - Processor (Detector) - - Averages signal over a single ROI - * - MultipleRoi - - Processor (Detector) - - Averages signals over multiple regions of interest (ROIs) - * - CropProcessor - - Processor (Detector) - - Crops data from the source to a region of interest - * - TransformProcessor - - Processor (Detector) - - Performs affine transformations on the source data - * - ADCProcessor - - Processor (Detector) - - Simulates an analog-digital converter - * - SimulatedWFS - - Processor - - Simulates wavefront shaping experiment using Fourier transform-based intensity computation at the focal plane - * - Gain - - Actuator - - Controls PMT gain voltage using NI data acquisition card - * - PhaseSLM - - Actuator - - Simulates a phase-only spatial light modulator - * - SLM - - Actuator - - Controls and renders patterns on a Spatial Light Modulator (SLM) using OpenGL - -Available Algorithms ---------------------- - -The following algorithms are available in OpenWFS for wavefront shaping: - -.. list-table:: - :header-rows: 1 - - * - Algorithm Name - - Description - * - FourierDualReference - - A Fourier dual reference algorithm that uses plane waves from a disk in k-space for wavefront shaping :cite:`Mastiani2022`. - * - IterativeDualReference - - A generic iterative dual reference algorithm with the ability to use custom basis functions for non-linear feedback applications. - * - DualReference - - A generic dual reference algorithm with the option for optimized reference, suitable for multi-target optimization and iterative feedback. - * - SimpleGenetic - - A simple genetic algorithm that optimizes wavefronts by selecting elite individuals and introducing mutations for focusing through scattering media :cite:`Piestun2012`. - * - StepwiseSequential - - A stepwise sequential algorithm which systematically modifies the phase pattern of each SLM element :cite:`Vellekoop2007`. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index c6da15e..c7886ca 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,8 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments core slms simulations + micromanager + troubleshooting development - pydevice api auto_examples/index diff --git a/docs/source/index_latex.rst b/docs/source/index_latex.rst index 5945f93..aad09ab 100644 --- a/docs/source/index_latex.rst +++ b/docs/source/index_latex.rst @@ -9,8 +9,7 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments core slms simulations - pydevice + micromanager + troubleshooting development conclusion - - auto_examples/index diff --git a/docs/source/pydevice.rst b/docs/source/micromanager.rst similarity index 95% rename from docs/source/pydevice.rst rename to docs/source/micromanager.rst index 7eca427..6069b5c 100644 --- a/docs/source/pydevice.rst +++ b/docs/source/micromanager.rst @@ -1,6 +1,6 @@ -.. _section-pydevice: +.. _micromanager: -OpenWFS in PyDevice +OpenWFS in μ-Manager ============================================== To smoothly enable end-user interaction with wavefront shaping algorithms, the Micro-Manager device adapter PyDevice was developed :cite:`PyDevice`. A more detailed description can be found in the mmCoreAndDevices source tree :cite:`mmCoreAndDevices`. In essence, PyDevice is Micro-Manager adapter that imports objects from a Python script and integrates them as devices, e.g. a camera or stage. OpenWFS was written in compliance with the templates required for PyDevice, which means OpenWFS cameras, scanners and algorithms can be loaded into Micro-Manager as devices. Examples of this are found in the example gallery :cite:`readthedocsOpenWFS`. Further developments due to this seamless connection can be a dedicated Micro-Manager based wavefront shaping GUI. \ No newline at end of file diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 6284454..c709b38 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -17,12 +17,14 @@ Wavefront shaping (WFS) is a technique for controlling the propagation of light It stands out that an important driving force in WFS is the development of new algorithms, for example, to account for sample movement :cite:`valzania2023online`, experimental conditions :cite:`Anderson2016`, to be optimally resilient to noise :cite:`mastiani2021noise`, or to use digital twin models to compute the required correction patterns :cite:`salter2014exploring,ploschner2015seeing,Thendiyammal2020,cox2023model`. Much progress has been made towards developing fast and noise-resilient algorithms, or algorithms designed specifically for the methodology of wavefront shaping, such as using algorithms based on Hadamard patterns or Fourier-based approaches :cite:`Mastiani2022`. Fast techniques that enable wavefront shaping in dynamic samples :cite:`Liu2017,Tzang2019` have also been developed, and many potential applications have been prototyped, including endoscopy :cite:`ploschner2015seeing`, optical trapping :cite:`Cizmar2010`, Raman scattering :cite:`Thompson2016`, and deep-tissue imaging :cite:`Streich2021`. Applications extend beyond that of microscope imaging, such as in optimizing photoelectrochemical absorption :cite:`Liew2016` and tuning random lasers :cite:`Bachelard2014`. -With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. A recent c++ based contribution :cite:`Anderson2024`, highlights the growing need for software based tools that enable use and development. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. +With the development of these advanced algorithms, however, the complexity of WFS software is steadily increasing as the field matures, which hinders cooperation as well as end-user adoption. Code for controlling wavefront shaping tends to be complex and setup-specific, and developing this code typically requires detailed technical knowledge and low-level programming. Moreover, since many labs use their own in-house programs to control the experiments, sharing and re-using code between different research groups is troublesome. + +Even though authors are increasingly sharing their code, for example for controlling spatial light modulators (SLMs) :cite:`PopoffslmPy`, or running genetic algorithms :cite:`Anderson2024`, a modular framework that combines all aspects of hardware control, simulation, and graphical user interface (GUI) integration is still lacking. What is OpenWFS? ---------------------- -OpenWFS is a Python package for performing and for simulating wavefront shaping experiments. It aims to accelerate wavefront shaping research by providing: +OpenWFS is a Python package that is primarily designed for performing and for simulating wavefront shaping experiments. It aims to accelerate wavefront shaping research by providing: * **Hardware control**. Modular code for controlling spatial light modulators, cameras, and other hardware typically encountered in wavefront shaping experiments. Highlights include: @@ -31,26 +33,34 @@ OpenWFS is a Python package for performing and for simulating wavefront shaping * **GenICam cameras**. The :class:`~.devices.Camera` object uses the `harvesters` backend :cite:`harvesters` to access any camera supporting the GenICam standard :cite:`genicam`. * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. -* **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, most algorithms can be implemented cleanly without hardware-specific programming. +* **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms without the need for physical hardware. -* **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms, without the need for physical hardware. +* **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, even advanced algorithms can be implemented in a few dozens of lines of code, and automaticallyt work with any combination of hardware and simulation tools that OpenWFS supports. -* **Platform for exchange and joint collaboration**. OpenWFS can be used as a platform for sharing and exchanging wavefront shaping algorithms. The package is designed to be modular and easy to expand, and it is our hope that the community will contribute to the package by adding new algorithms, hardware control modules, and simulation tools. Python was specifically chosen for this purpose for its active community, high level of abstraction and the ease of sharing tools. Further expansion of the supported hardware is of high priority, especially wrapping c-based software support with tools like ctypes and the Micro-Manager based device adapters. +* **Platform for exchange and joint collaboration**. OpenWFS can be used as a platform for sharing and exchanging wavefront shaping algorithms. The package is designed to be modular and easy to expand, and it is our hope that the community will contribute to the package by adding new algorithms, hardware control modules, and simulation tools. Python was specifically chosen for this purpose for its active community, high level of abstraction and the ease of sharing tools. * **Platform for simplifying use of wavefront shaping**. OpenWFS is compatible with the recently developed PyDevice :cite:`PyDevice`, and can therefore be controlled from Micro-Manager :cite:`MMoverview`, a commonly used microscopy control platform. * **Automated troubleshooting**. OpenWFS provides tools for automated troubleshooting of wavefront shaping experiments. This includes tools for measuring the performance of wavefront shaping algorithms, and for identifying common problems such as incorrect SLM calibration, drift, measurement noise, and other experimental imperfections. + + .. only:: latex - Here, we first show how to get started using OpenWFS for simulating and controlling wavefront shaping experiments. An in-depth discussion of the core design of OpenWFS is given in :numref:`Key concepts`. Key to any wavefront shaping experiment is the SLM. The support for advanced options like texture warping and the use of a software lookup table are explained in :numref:`section-slms`. + Here, we first show how to get started using OpenWFS for simulating and controlling wavefront shaping experiments. An in-depth discussion of the core design of OpenWFS is given in :numref:`Key concepts`. Key to any wavefront shaping experiment is the SLM. The support for advanced options like texture mapping and the use of a software lookup table are explained in :numref:`section-slms`. The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. Getting started ---------------------- -OpenWFS is available on the PyPI repository, and it can be installed with the command ``pip install openwfs``. The latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`, and the entire repository can be found on :cite:`openwfsgithub`. To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 (not all dependencies were available for Python 3.12 yet). OpenWFS is developed and tested on Windows 11 and Manjaro Linux. Note that for certain hardware components, third party software needs to be installed. This is always mentioned in the documentation and docstrings of these functions. +To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 on Windows 11 and Manjaro Linux. OpenWFS is available on the PyPI repository. To install it, run the following command: + +.. code-block:: bash + + pip install openwfs[all] + +This will also install the optional dependencies for OpenWFS, such as ``PyOpenGL``, ``nidaqmx`` and ``harvesters``, which are used for OpenGL-accelerated SLM control, scanning microscopy, and camera control, respectively. These dependencies cannot be installed on your system, the installation will fail. At the time of writing, this can happen with ``PyOpenGL`` on systems without OpenGL driver installed, or for ``harvesters``, which currently only works for Python versions up to 3.11. You can instead install OpenWFS without dependencies by omitting ``[all]`` in the installation command, and then install only the required dependencies as indicated in the API documentation for each hardware component. The latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`, and the source code can be found on :cite:`openwfsgithub`. :numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. @@ -59,31 +69,10 @@ OpenWFS is available on the PyPI repository, and it can be installed with the co :language: python :caption: ``hello_wfs.py``. Example of a simple wavefront shaping experiment using OpenWFS. -This example uses the `~.StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. - -This code illustrates how OpenWFS separates the concerns of the hardware control (:class:`~.SLM` and :class:`~.Camera`), signal processing (:class:`~.SingleRoi(Processor)`) and the algorithm itself (:class:`~.StepwiseSequential`). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a :class:`~.MultiRoi(Processor)` object), using different algorithms, or different image sources, such as a :class:`~.ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see :numref:`section-simulations`). - - -Analysis and troubleshooting ------------------------------------------------- -The principles of wavefront shaping are well established, and under close-to-ideal experimental conditions, it is possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the measurements from a wavefront shaping experiment. - -The ``result`` structure in :numref:`hello-wfs`, as returned by the wavefront shaping algorithm, was computed with the utility function :func:`analyze_phase_stepping`. This function extracts the transmission matrix from phase stepping measurements, and additionally computes a series of troubleshooting statistics in the form of a *fidelity*, which is a number that ranges from 0 (no sensible measurement possible) to 1 (perfect situation, optimal focus expected). These fidelities are: - -* :attr:`~.WFSResults.fidelity_noise`: The fidelity reduction due to noise in the measurements. -* :attr:`~.WFSResults.fidelity_amplitude`: The fidelity reduction due to unequal illumination of the SLM. -* :attr:`~.WFSResults.fidelity_calibration`: The fidelity reduction due to imperfect phase response of the SLM. - -If these fidelities are much lower than 1, this indicates a problem in the experiment, or a bug in the wavefront shaping experiment. For a comprehensive overview of the practical considerations in wavefront shaping and their effects on the fidelity, please see :cite:`Mastiani2024PracticalConsiderations`. - -Further troubleshooting can be performed with the :func:`~.troubleshoot` function, which estimates the following fidelities: - -* :attr:`~.WFSTroubleshootResult.fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e.g. due to reflection from the front surface of the SLM. -* :attr:`~.WFSTroubleshootResult.fidelity_decorrelation`: The fidelity reduction due to decorrelation of the field during the measurement. +This example uses the :class:`~.StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. -All fidelity estimations are combined to make an order of magnitude estimation of the expected enhancement. :func:`~.troubleshoot` returns a ``WFSTroubleshootResult`` object containing the outcome of the different tests and analyses, which can be printed to the console as a comprehensive troubleshooting report with the method :meth:`~.WFSTroubleshootResult.report()`. See ``examples/troubleshooter_demo.py`` for an example of how to use the automatic troubleshooter. +This code illustrates how OpenWFS separates the concerns of the hardware control (:class:`~.SLM` and :class:`~.Camera`), signal processing (:class:`~.SingleRoi`) and the algorithm itself (:class:`~.StepwiseSequential`). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a :class:`~.MultiRoi` object), using different algorithms, or different image sources, such as a :class:`~.ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see :numref:`section-simulations`). -Lastly, the :func:`~.troubleshoot` function computes several image frame metrics such as the *unbiased contrast to noise ratio* and *unbiased contrast enhancement*. These metrics are especially useful for scenarios where the contrast is expected to improve due to wavefront shaping, such as in multi-photon excitation fluorescence (multi-PEF) microscopy. Furthermore, :func:`~.troubleshoot` tests the image capturing repeatability and runs a stability test by capturing and comparing many frames over a longer period of time. %endmatter% diff --git a/docs/source/references.bib b/docs/source/references.bib index 404a9f5..fc7d9b5 100644 --- a/docs/source/references.bib +++ b/docs/source/references.bib @@ -6,23 +6,22 @@ @book{goodman2015statistical } - @article{Piestun2012, - abstract = {We introduce genetic algorithms (GA) for wavefront control to focus light through highly scattering media. We theoretically and experimentally compare GAs to existing phase control algorithms and show that GAs are particularly advantageous in low signal-to-noise environments.}, - author = {Rafael Piestun and Albert N. Brown and Antonio M. Caravaca-Aguirre and Donald B. Conkey}, - doi = {10.1364/OE.20.004840}, - issn = {1094-4087}, - issue = {5}, - journal = {Optics Express, Vol. 20, Issue 5, pp. 4840-4849}, - keywords = {Optical trapping,Phase conjugation,Phase shift,Scattering media,Spatial light modulators,Turbid media}, - month = {2}, - pages = {4840-4849}, - pmid = {22418290}, - publisher = {Optica Publishing Group}, - title = {Genetic algorithm optimization for focusing through turbid media in noisy environments}, - volume = {20}, - url = {https://opg.optica.org/viewmedia.cfm?uri=oe-20-5-4840&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=oe-20-5-4840 https://opg.optica.org/oe/abstract.cfm?uri=oe-20-5-4840}, - year = {2012}, + abstract = {We introduce genetic algorithms (GA) for wavefront control to focus light through highly scattering media. We theoretically and experimentally compare GAs to existing phase control algorithms and show that GAs are particularly advantageous in low signal-to-noise environments.}, + author = {Rafael Piestun and Albert N. Brown and Antonio M. Caravaca-Aguirre and Donald B. Conkey}, + doi = {10.1364/OE.20.004840}, + issn = {1094-4087}, + issue = {5}, + journal = {Optics Express, Vol. 20, Issue 5, pp. 4840-4849}, + keywords = {Optical trapping,Phase conjugation,Phase shift,Scattering media,Spatial light modulators,Turbid media}, + month = {2}, + pages = {4840-4849}, + pmid = {22418290}, + publisher = {Optica Publishing Group}, + title = {Genetic algorithm optimization for focusing through turbid media in noisy environments}, + volume = {20}, + url = {https://opg.optica.org/viewmedia.cfm?uri=oe-20-5-4840&seq=0&html=true https://opg.optica.org/abstract.cfm?uri=oe-20-5-4840 https://opg.optica.org/oe/abstract.cfm?uri=oe-20-5-4840}, + year = {2012}, } @@ -368,6 +367,13 @@ @article{vellekoop2008phase publisher = {Elsevier} } +@misc{PopoffslmPy, + author = {S. Popoff}, + title = {slmPy: A simple Python module to interact with spatial light modulators}, + year = {2017}, + howpublished = {\url{https://github.com/wavefrontshaping/slmPy}}, +} + @article{Liu2017, author = {Yan Liu et al.}, journal = {Optica}, @@ -460,6 +466,7 @@ @misc{mmCoreAndDevices title = {Micro-Manager mmCoreAndDevices repository}, url = {https://github.com/micro-manager/mmCoreAndDevices}, } + @misc{MMoverview, author = {Mark Tsuchida and Sam Griffin}, title = {Micro-Manager Project Overview}, @@ -484,7 +491,7 @@ @article{Anderson2024 publisher = {IOP Publishing}, title = {A modular GUI-based program for genetic algorithm-based feedback-assisted wavefront shaping}, volume = {6}, - url = {https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3 https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3/meta}, + url = {https://iopscience.iop.org/article/10.1088/2515-7647/ad6ed3}, year = {2024}, } @@ -566,6 +573,15 @@ @ARTICLE{Astropy2022 adsnote = {Provided by the SAO/NASA Astrophysics Data System} } +@article{Cox2024, + author = {Dani{\"e}l W.S. Cox and Ivo M. Vellekoop}, + title = {Othonormalization of phase-only basis functions}, + journal = {ArXiv}, + volume = {2409.04565}, + year = {2024}, + doi = {https://doi.org/10.48550/arXiv.2409.04565}, +} + @article{Lai2015, author = {Lai, Puxiang and Wang, Li and Wang, Lihong}, year = {2015}, diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst new file mode 100644 index 0000000..78966e4 --- /dev/null +++ b/docs/source/troubleshooting.rst @@ -0,0 +1,22 @@ +.. _troubleshooting: + +Analysis and troubleshooting +================================================== +The principles of wavefront shaping are well established, and under close-to-ideal experimental conditions, it is possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the measurements from a wavefront shaping experiment. + +The ``result`` structure in :numref:`hello-wfs`, as returned by the wavefront shaping algorithm, was computed with the utility function :func:`analyze_phase_stepping`. This function extracts the transmission matrix from phase stepping measurements, and additionally computes a series of troubleshooting statistics in the form of a *fidelity*, which is a number that ranges from 0 (no sensible measurement possible) to 1 (perfect situation, optimal focus expected). These fidelities are: + +* :attr:`~.WFSResults.fidelity_noise`: The fidelity reduction due to noise in the measurements. +* :attr:`~.WFSResults.fidelity_amplitude`: The fidelity reduction due to unequal illumination of the SLM. +* :attr:`~.WFSResults.fidelity_calibration`: The fidelity reduction due to imperfect phase response of the SLM. + +If these fidelities are much lower than 1, this indicates a problem in the experiment, or a bug in the wavefront shaping experiment. For a comprehensive overview of the practical considerations in wavefront shaping and their effects on the fidelity, please see :cite:`Mastiani2024PracticalConsiderations`. + +Further troubleshooting can be performed with the :func:`~.troubleshoot` function, which estimates the following fidelities: + +* :attr:`~.WFSTroubleshootResult.fidelity_non_modulated`: The fidelity reduction due to non-modulated light., e.g. due to reflection from the front surface of the SLM. +* :attr:`~.WFSTroubleshootResult.fidelity_decorrelation`: The fidelity reduction due to decorrelation of the field during the measurement. + +All fidelity estimations are combined to make an order of magnitude estimation of the expected enhancement. :func:`~.troubleshoot` returns a ``WFSTroubleshootResult`` object containing the outcome of the different tests and analyses, which can be printed to the console as a comprehensive troubleshooting report with the method :meth:`~.WFSTroubleshootResult.report()`. See ``examples/troubleshooter_demo.py`` for an example of how to use the automatic troubleshooter. + +Lastly, the :func:`~.troubleshoot` function computes several image frame metrics such as the *unbiased contrast to noise ratio* and *unbiased contrast enhancement*. These metrics are especially useful for scenarios where the contrast is expected to improve due to wavefront shaping, such as in multi-photon excitation fluorescence (multi-PEF) microscopy. Furthermore, :func:`~.troubleshoot` tests the image capturing repeatability and runs a stability test by capturing and comparing many frames over a longer period of time. From 2605789f36d7c0088078adf39741d6349cc3a9c4 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Mon, 7 Oct 2024 15:41:16 +0200 Subject: [PATCH 21/37] fixing references in documentation --- README.md | 12 +++++++++--- docs/source/core.rst | 14 +++++++------- docs/source/development.rst | 28 ++++++++++++++-------------- docs/source/readme.rst | 2 +- docs/source/slms.rst | 12 ++++++------ 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 4109a04..0e2daeb 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,15 @@ wavefront shaping research by providing: * **Hardware control**. Modular code for controlling spatial light modulators, cameras, and other hardware typically encountered in wavefront shaping experiments. Highlights include: - > * **Spatial light modulator**. The `SLM` object provides a versatile way to control spatial light modulators, + > * **Spatial light modulator**. The :class:`.~SLM` object provides a versatile way to control spatial light + modulators, allowing for software lookup tables, synchronization, texture warping, and multi-texture functionality accelerated by OpenGL. - > * **Scanning microscope**. The `ScanningMicroscope` object uses a National Instruments data acquisition card to + > * **Scanning microscope**. The :class:`.~ScanningMicroscope` object uses a National Instruments data acquisition + card to control a laser-scanning microscope. - > * **GenICam cameras**. The `Camera` object uses the harvesters backend [[24](#id39)] to access any camera supporting + > * **GenICam cameras**. The :class:`.~Camera` object uses the harvesters backend [[24](#id39)] to access any camera + supporting the GenICam standard [[25](#id42)]. > * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that @@ -269,6 +272,7 @@ Photonics*, 2019. [doi:10.1038/s41566-019-0503-6](https://doi.org/10.1038/s41566 Tomáš Čižmár, Michael Mazilu, and Kishan Dholakia. In situ wavefront correction and its application to micromanipulation. *Nature Photonics*, 4:388–394, 05 + 2010. [doi:10.1038/nphoton.2010.85](https://doi.org/10.1038/nphoton.2010.85). 19 @@ -281,6 +285,7 @@ URL: [https://pubmed.ncbi.nlm.nih.gov/27082341/](https://pubmed.ncbi.nlm.nih.gov Lina Streich et al. High-resolution structural and functional deep brain imaging using adaptive optics three-photon microscopy. *Nature Methods 2021 18:10*, 18:1253–1258, 9 + 2021. [doi:10.1038/s41592-021-01257-6](https://doi.org/10.1038/s41592-021-01257-6). 21 @@ -330,4 +335,5 @@ Ivo M. Vellekoop and AP Mosk. Phase control algorithms for focusing light throug Bahareh Mastiani, Daniël W. S. Cox, and Ivo M. Vellekoop. Practical considerations for high-fidelity wavefront shaping experiments. http://arxiv.org/abs/2403.15265, March + 2024. [arXiv:2403.15265](https://arxiv.org/abs/2403.15265), [doi:10.48550/arXiv.2403.15265](https://doi.org/10.48550/arXiv.2403.15265). diff --git a/docs/source/core.rst b/docs/source/core.rst index 293a397..3d921bd 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -47,16 +47,16 @@ All detectors derive from the :class:`~.Detector` base class. and have the follo def coordinates(dimension: int) -> Quantity -The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as ``numpy`` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e.g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`.~Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. +The :meth:`~.Detector.read()` method of a detector starts a measurement and returns the captured data. It triggers the detector and blocks until the data is available. Data is always returned as ``numpy`` array :cite:`numpy`. Subclasses of :class:`~.Detector` typically add properties specific to that detector (e.g. shutter time, gain, etc.). In the simplest case, setting these properties and calling :meth:`~.Detector.read()` is all that is needed to capture data. The :meth:`~.Detector.trigger()` method is used for asynchronous measurements as described below. All other properties and methods are used for metadata and units, as described in :numref:`Units and metadata`. The detector object inherits some properties and methods from the base class :class:`~.Device`. These are used by the synchronization mechanism to determine when it is safe to start a measurement, as described in :numref:`device-synchronization`. Asynchronous measurements +++++++++++++++++++++++++++ -:meth:`.~Detector.read()` blocks the program until the captured data is available. This behavior is not ideal when multiple detectors are used simultaneously, or when transferring or processing the data takes a long time. In these cases, it is preferable to use :meth:`.~Detector.trigger()`, which initiates the process of capturing or generating data and returns directly. The program can continue operation while the data is being captured/transferred/generated in a worker thread. While fetching and processing data is underway, any attempt to modify a property of the detector will block until the fetching and processing is complete. This way, all properties (such as the region of interest) are guaranteed to be constant between the calls to :meth:`.~Detector.trigger` and the moment the data is actually fetched and processed in the worker thread. +:meth:`~.Detector.read()` blocks the program until the captured data is available. This behavior is not ideal when multiple detectors are used simultaneously, or when transferring or processing the data takes a long time. In these cases, it is preferable to use :meth:`~.Detector.trigger()`, which initiates the process of capturing or generating data and returns directly. The program can continue operation while the data is being captured/transferred/generated in a worker thread. While fetching and processing data is underway, any attempt to modify a property of the detector will block until the fetching and processing is complete. This way, all properties (such as the region of interest) are guaranteed to be constant between the calls to :meth:`~.Detector.trigger` and the moment the data is actually fetched and processed in the worker thread. -The asynchronous measurement mechanism can be seen in action in the :class:`.~StepwiseSequential` algorithm used in :numref:`hello-wfs`. The :meth:`execute() ` function of this algorithm is implemented as +The asynchronous measurement mechanism can be seen in action in the :class:`~.StepwiseSequential` algorithm used in :numref:`hello-wfs`. The :meth:`~.StepwiseSequential.execute()` function of this algorithm is implemented as .. code-block:: python @@ -77,7 +77,7 @@ The asynchronous measurement mechanism can be seen in action in the :class:`.~St This code performs a wavefront shaping algorithm similar to the one described in :cite:`Vellekoop2007`. In this version, there is no pre-optimization. It works by cycling the phase of each of the ``n_x × n_y`` segments on the SLM between 0 and 2π, and measuring the feedback signal at each step. ``self.feedback`` holds a :class:`~.Detector` object that is triggered, and stores the measurement in a pre-allocated ``measurements`` array when it becomes available. It is possible to find the optimized wavefront for multiple targets simultaneously by using a detector that returns an array of size ``feedback.data_shape``, which contains a feedback value for each of the targets. -The program does not wait for the data to become available and can directly proceed with preparing the next pattern to send to the SLM (also see :numref:`device-synchronization`). After running the algorithm, `wait` is called to wait until all measurement data is stored in the `measurements` array, and the utility function `analyze_phase_stepping` is used to extract the transmission matrix from the measurements, as well as a series of troubleshooting statistics (see :numref:`Analysis and troubleshooting`). +The program does not wait for the data to become available and can directly proceed with preparing the next pattern to send to the SLM (also see :numref:`device-synchronization`). After running the algorithm, :meth:`~.Detector.wait` is called to wait until all measurement data is stored in the `measurements` array, and the utility function `analyze_phase_stepping` is used to extract the transmission matrix from the measurements, as well as a series of troubleshooting statistics (see :numref:`Analysis and troubleshooting`). Note that, except for this asynchronous mechanism for fetching and processing data, OpenWFS is not designed to be thread-safe, and the user is responsible for guaranteeing that devices are only accessed from a single thread at a time. @@ -89,7 +89,7 @@ Note that, except for this asynchronous mechanism for fetching and processing da Processors ------------ -A :class:`.~Processor` is an object that takes input from one or more other detectors, and combines/processes this data. By itself, a processor is a :class:`.~Detector`, enabling multiple processors to be chained together to combine their functionality. We already encountered an example in :numref:`Getting started`, where the :class:`.~SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. The OpenWFS currently includes the following processors: +A :class:`~.Processor` is an object that takes input from one or more other detectors, and combines/processes this data. By itself, a processor is a :class:`~.Detector`, enabling multiple processors to be chained together to combine their functionality. We already encountered an example in :numref:`Getting started`, where the :class:`~.SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. The OpenWFS currently includes the following processors: .. list-table:: :header-rows: 1 @@ -131,7 +131,7 @@ Algorithms ------------ OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could be implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the the Micro-Manager graphical user interface (GUI), see :ref:`micromanager`. -All algorithms are designed to be completely hardware-agnostic, so that they can be used with any type of feedback signal and either use real hardware or simulated hardware without the need to change a single line of code in the algorithm implementation. The :class:`.~FourierDualReference`, :class:`.~DualReference` and :class:`.~StepwiseSequential` algorithms provide support for optimizing multiple targets simulaneously in a single run of the algorithm. +All algorithms are designed to be completely hardware-agnostic, so that they can be used with any type of feedback signal and either use real hardware or simulated hardware without the need to change a single line of code in the algorithm implementation. The :class:`~.FourierDualReference`, :class:`~.DualReference` and :class:`~.StepwiseSequential` algorithms provide support for optimizing multiple targets simulaneously in a single run of the algorithm. .. list-table:: :header-rows: 1 @@ -188,7 +188,7 @@ Each device can either be *busy* or *ready*, and this state can be polled by cal - before starting a measurement, wait until all motion is (almost) completed - before starting any movement, wait until all measurements are (almost) completed -Here, 'almost' refers to the fact that devices may have a *latency*. Latency is the time between sending a command to a device, and the moment the device starts responding. An important example is the SLM, which typically takes one or two frame periods to transfer the image data to the liquid crystal chip. Such devices can specify a non-zero ``latency`` attribute. When specified, the device 'promises' not to do anything until ``latency`` milliseconds after the start of the measurement or movement. When a latency is specified, detectors or actuators can be started slightly before the devices of the other type (actuators or detectors, respectively) have finished their operation. For example, this mechanism allows sending a new frame to the SLM *before* the measurements of the current frame are finished, since it is known that the SLM will not respond for ``latency`` milliseconds anyway. This way, measurements and SLM updates can be pipelined to maximize the number of measurements that can be done in a certain amount of time. To enable these pipelined measurements, the ``Device`` class also provides a `duration` attribute, which is the maximum time interval between the start and end of a measurement or actuator action. +Here, 'almost' refers to the fact that devices may have a *latency*. Latency is the time between sending a command to a device, and the moment the device starts responding. An important example is the SLM, which typically takes one or two frame periods to transfer the image data to the liquid crystal chip. Such devices can specify a non-zero ``latency`` attribute. When specified, the device 'promises' not to do anything until ``latency`` milliseconds after the start of the measurement or movement. When a latency is specified, detectors or actuators can be started slightly before the devices of the other type (actuators or detectors, respectively) have finished their operation. For example, this mechanism allows sending a new frame to the SLM *before* the measurements of the current frame are finished, since it is known that the SLM will not respond for ``latency`` milliseconds anyway. This way, measurements and SLM updates can be pipelined to maximize the number of measurements that can be done in a certain amount of time. To enable these pipelined measurements, the :class:`~.Device` class also provides a :attr:`~.Device.duration` attribute, which is the maximum time interval between the start and end of a measurement or actuator action. This synchronization is performed automatically. If desired, it is possible to explicitly wait for the device to become ready by calling :meth:`~.Device.wait()`. To accommodate taking into account the latency, this function takes an optional parameter ``up_to``, which indicates that the function may return the specified time *before* the device hardware is ready. In user code, it is only necessary to call ``wait`` when using the ``out`` parameter to store measurements in a pre-defined location (see :numref:`Asynchronous measurements` above). A typical usage pattern is illustrated in the following snippet: diff --git a/docs/source/development.rst b/docs/source/development.rst index 078a639..ea0add9 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -5,7 +5,7 @@ OpenWFS Development Running the tests and examples -------------------------------------------------- -To download the source code, including tests and examples, clone the repository from GitHub :cite:`openwfsgithub`. OpenWFS uses `poetry` :cite:`Poetry` for package management, so you have to download and install Poetry first. Then, navigate to the location where you want to store the source code, and execute the following commands to clone the repository, set up the poetry environment, and run the tests. +To download the source code, including tests and examples, clone the repository from GitHub :cite:`openwfsgithub`. OpenWFS uses ``poetry`` :cite:`Poetry` for package management, so you have to download and install Poetry first. Then, navigate to the location where you want to store the source code, and execute the following commands to clone the repository, set up the poetry environment, and run the tests. .. code-block:: shell @@ -23,13 +23,13 @@ Building the documentation .. only:: html or markdown - The html, and pdf versions of the documentation, as well as the `README.md` file in the root directory of the repository, are automatically generated from the docstrings in the source code and reStructuredText source files in the repository. + The html, and pdf versions of the documentation, as well as the ``README.md`` file in the root directory of the repository, are automatically generated from the docstrings in the source code and reStructuredText source files in the repository. .. only:: latex - The html version of the documentation, as well as the `README.md` file in the root directory of the repository, and the pdf document you are currently reading are automatically generated from the docstrings in the source code and reStructuredText source files in the repository. + The html version of the documentation, as well as the ``README.md`` file in the root directory of the repository, and the pdf document you are currently reading are automatically generated from the docstrings in the source code and reStructuredText source files in the repository. -Note that for building the pdf version of the documentation, you need to have `xelatex` installed, which comes with the MiKTeX distribution of LaTeX :cite:`MiKTeX`. Then, run the following commands to build the html and pdf versions of the documentation, and to auto-generate `README.md`. +Note that for building the pdf version of the documentation, you need to have ``xelatex`` installed, which comes with the MiKTeX distribution of LaTeX :cite:`MiKTeX`. Then, run the following commands to build the html and pdf versions of the documentation, and to auto-generate ``README.md``. .. code-block:: shell @@ -51,35 +51,35 @@ Bugs can be reported through the GitHub issue tracking system. Better than repor Implementing new algorithms -------------------------------------------------- To implement a new algorithm, the currently existing algorithms can be consulted for a few examples. -Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away. Using `slm.set_phases` and `feedback.trigger` the algorithm can be naive to specific hardware. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. +Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away in the calls to ``slm.set_phases`` and ``feedback.trigger``. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. Implementing new devices -------------------------------------------------- -To implement a custom device (actuator, detector, processor), it is important to first understand the implementation of the mechanism that synchronizes detectors and actuators. To implement this mechanism, the `Device` class keeps a global state which can be either +To implement a custom device (actuator, detector, processor), it is important to first understand the implementation of the mechanism that synchronizes detectors and actuators. To implement this mechanism, the :class:`~.Device` class keeps a global state which can be either - - `moving = True`. One or more actuators may be busy. No measurements can be made (none of the detectors is busy). - - `moving = False` (the 'measuring' state). One or more detectors may be busy. All actuators must remain static (none of the actuators is busy). + - ``moving = True``. One or more actuators may be busy. No measurements can be made (none of the detectors is busy). + - ``moving = False`` (the 'measuring' state). One or more detectors may be busy. All actuators must remain static (none of the actuators is busy). -When an actuator is started, or when a detector is triggered, it calls ``self._start`` to request a switch to the correct global state. If a state switch is needed, this function blocks until all devices of the other device type are ready. For example, if an actuator calls ``_start``, the framework waits for all detectors to complete their measurements (up to latency, see :numref:`device-synchronization`) before the switch is made. Note that for detectors and processors, ``_start`` is called automatically by `trigger()`, so there is never a need to call it explicitly. +When an actuator is started, or when a detector is triggered, it calls ``self._start`` to request a switch to the correct global state. If a state switch is needed, this function blocks until all devices of the other device type are ready. For example, if an actuator calls ``_start``, the framework waits for all detectors to complete their measurements (up to latency, see :numref:`device-synchronization`) before the switch is made. Note that for detectors and processors, ``_start`` is called automatically by :meth:`~.Device.trigger()`, so there is never a need to call it explicitly. Implementing a detector ++++++++++++++++++++++++++++++++++ -To implement a detector, the user should subclass the `Detector` base class, and implement properties and logic to control the detector hardware. In particular, the user should implement the `~Detector._do_trigger` method to start the measurement process, and the `~Detector._fetch()` method to fetch the data from the hardware, optionally process it, and return it as a numpy array. +To implement a detector, the user should subclass the :meth:`~.Detector` base class, and implement properties and logic to control the detector hardware. In particular, the user should implement the :meth:`~Detector._do_trigger` method to start the measurement process, and the :meth:`~Detector._fetch()` method to fetch the data from the hardware, optionally process it, and return it as a numpy array. -If `duration`, `pixel_size` and `data_shape` are constants, they should be passed to the base class constructor. If these properties may change during operation, the user should override the `duration`, `pixel_size` and `data_shape` properties to provide the correct values dynamically. If the `duration` is not known in advance (for example, when waiting for a hardware trigger), the Detector should implement the `busy` function to poll the hardware for the busy state. +If ``duration``, ``pixel_size`` and ``data_shape`` are constants, they should be passed to the base class constructor. If these properties may change during operation, the user should override the ``duration``, ``pixel_size`` and ``data_shape`` properties to provide the correct values dynamically. If the ``duration`` is not known in advance (for example, when waiting for a hardware trigger), the Detector should implement the ``busy`` function to poll the hardware for the busy state. -If the detector is created with the flag ``multi_threaded = True``, then `_fetch` will be called from a worker thread. This way, the rest of the program does not need to wait for transferring data from the hardware, or for computationally expensive processing tasks. OpenWFS automatically prevents any modification of public properties between the calls to `_do_trigger` and `_fetch`, which means that the `_fetch` function can safely read (not write) these properties without the chance of a race condition. Care must be taken, however, not to read or write private fields from `_fetch`, since this is not thread-safe. +If the detector is created with the flag ``multi_threaded = True``, then :meth:`~Detector._fetch()` will be called from a worker thread. This way, the rest of the program does not need to wait for transferring data from the hardware, or for computationally expensive processing tasks. OpenWFS automatically prevents any modification of public properties between the calls to :meth:`~Detector._do_trigger` and :meth:`~Detector._fetch`, which means that the ``_fetch`` function can safely read (not write) these properties without the chance of a race condition. Care must be taken, however, not to read or write private fields from ``_fetch``, since this is not thread-safe. Implementing a processor ++++++++++++++++++++++++++++++++++ -To implement a data processing step that dynamically processes data from one or more input detectors, implement a custom processor. This is done by deriving from the `Processor` base class and implementing the `__init__` function. This function should pass a list of all upstream nodes, i.e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth"`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls `_fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. +To implement a data processing step that dynamically processes data from one or more input detectors, implement a custom processor. This is done by deriving from the :class:`~.Processor` base class and implementing the ``__init__`` function. This function should pass a list of all upstream nodes, i.e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth:`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls :meth:`~.Detector._fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. Implementing an actuator +++++++++++++++++++++++++++++++ -To implement an actuator, the user should subclass the `Actuator` base class, and implement whatever properties and logic appropriate to the device. All methods that start the actuator (e.g. `update()` or `move()`), should first call `self._start()` to request a state switch to the `moving` state. As for detectors, actuators should either specify a static `duration` and `latency` if known, or override these properties to return run-time values for the duration and latency. Similarly, if the duration of an action of the actuator is not known in advance, the class should override `busy` to poll for the action to complete. +To implement an actuator, the user should subclass the :class:`~Actuator` base class, and implement whatever properties and logic appropriate to the device. All methods that start the actuator (e.g. ``update()`` or ``move()``), should first call ``self._start()`` to request a state switch to the ``moving`` state. As for detectors, actuators should either specify a static ``duration` and ``latency`` if known, or override these properties to return run-time values for the duration and latency. Similarly, if the duration of an action of the actuator is not known in advance, the class should override ``busy`` to poll for the action to complete. diff --git a/docs/source/readme.rst b/docs/source/readme.rst index c709b38..30267e9 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -30,7 +30,7 @@ OpenWFS is a Python package that is primarily designed for performing and for si * **Spatial light modulator**. The :class:`~.slm.SLM` object provides a versatile way to control spatial light modulators, allowing for software lookup tables, synchronization, texture warping, and multi-texture functionality accelerated by OpenGL. * **Scanning microscope**. The :class:`~.devices.ScanningMicroscope` object uses a National Instruments data acquisition card to control a laser-scanning microscope. - * **GenICam cameras**. The :class:`~.devices.Camera` object uses the `harvesters` backend :cite:`harvesters` to access any camera supporting the GenICam standard :cite:`genicam`. + * **GenICam cameras**. The :class:`~.devices.Camera` object uses the ``harvesters`` backend :cite:`harvesters` to access any camera supporting the GenICam standard :cite:`genicam`. * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. * **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms without the need for physical hardware. diff --git a/docs/source/slms.rst b/docs/source/slms.rst index b229bc5..5d56689 100644 --- a/docs/source/slms.rst +++ b/docs/source/slms.rst @@ -12,9 +12,9 @@ Spatial Light Modulators are the heart of any wavefront shaping experiment. Curr The :meth:`~.PhaseSLM.set_phases()` method takes a scalar or a 2-D array of phase values in radians, which is wrapped to the range [0, 2π) and displayed on the SLM. This function calls :meth:`~.PhaseSLM.update()` by default to send the image to the SLM hardware. In more advanced scenarios, like texture blending (see below), it can be useful to postpone the update by passing ``update=False`` and manually cal :meth:`~.PhaseSLM.update()` later. The algorithms in OpenWFS only access SLMs through this simple interface. As a result, the details of the SLM hardware are decoupled from the wavefront shaping algorithm itself. -Currently, there are two implementations of the `PhaseSLM` interface. The :class:`simulation.SLM` is used for simulating experiments and for testing algorithms (see :numref:`section-simulations`). The :class:`hardware.SLM` is an OpenGL-accelerated controller for using a phase-only SLM that is connected to the video output of a computer. The SLM can be created in windowed mode (useful for debugging), or full screen. It is possible to have multiple windowed SLMs on the same monitor, but only one full-screen SLM per monitor. In addition, the SLM implements some advanced features that are discussed below. +Currently, there are two implementations of the :class:`~.PhaseSLM` interface. The :class:`simulation.SLM` is used for simulating experiments and for testing algorithms (see :numref:`section-simulations`). The :class:`hardware.SLM` is an OpenGL-accelerated controller for using a phase-only SLM that is connected to the video output of a computer. The SLM can be created in windowed mode (useful for debugging), or full screen. It is possible to have multiple windowed SLMs on the same monitor, but only one full-screen SLM per monitor. In addition, the SLM implements some advanced features that are discussed below. -At the time of writing, SLMs that are controlled through other interfaces than the video output are not supported. However, the interface of the `PhaseSLM` class is designed to accommodate these devices in the future. Through this interface, support for intensity-only light modulators (e.g. Digital Mirror Devices) operating in phase-modulation mode (e.g. :cite:`conkey2012high`) may also be added. +At the time of writing, SLMs that are controlled through other interfaces than the video output are not supported. However, the interface of the :class:`~.PhaseSLM` class is designed to accommodate these devices in the future. Through this interface, support for intensity-only light modulators (e.g. Digital Mirror Devices) operating in phase-modulation mode (e.g. :cite:`conkey2012high`) may also be added. Texture mapping and blending ----------------------------------- @@ -49,10 +49,10 @@ The combination of texture mapping and blending allows for a wide range of use c - Aligning the size and position of a square phase map with the illuminating beam. - Correcting phase maps for distortions in the optical system, such as barrel distortion. - - Using two parts of the same SLM independently. This feature is possible because each patch object can independently be used as a `:class:`~.PhaseSLM` object. - - Blocking part of a wavefront by drawing a different patch on top of it, with `:attr:`~.Patch.additive_blend` = ``False``. + - Using two parts of the same SLM independently. This feature is possible because each patch object can independently be used as a :class:`~.PhaseSLM` object. + - Blocking part of a wavefront by drawing a different patch on top of it, with :attr:`~.Patch.additive_blend` ``= False``. - Modifying an existing wavefront by adding a gradient or defocus pattern. - - Compensating for curvature in the SLM and other system aberrations by adding an offset layer with `:attr:`~.Patch.additive_blend` = ``True`` to compensate for these aberrations. + - Compensating for curvature in the SLM and other system aberrations by adding an offset layer with :attr:`~.Patch.additive_blend` ``= True`` to compensate for these aberrations. All of these corrections can be done in real time using OpenGL acceleration, making the SLM object a versatile tool for wavefront shaping experiments. @@ -64,7 +64,7 @@ For debugging or demonstration purposes, it is often useful to receive feedback Lookup table --------------------------------------- -Even though the SLM hardware itself often includes a hardware lookup table, there usually is no standard way to set it from Python, making switching between lookup tables cumbersome. The OpenGL-accelerated lookup table in the SLM object provides a solution to this problem, which is especially useful when working with tunable lasers, for which the lookup table needs to be adjusted often. The SLM object has a :attr:`~.slm.SLM.lookup_table` property, which holds a table that is used to convert phase values from radians to gray values on the screen. By default, this table is set to `range(2 ** bit_depth)`, meaning that if an 8-bit video mode is selected, a phase of 0 produces a gray value of 0, and a phase of 255/256·2π produces a gray value of 255. A phase of 2π again produces a gray value of 0. +Even though the SLM hardware itself often includes a hardware lookup table, there usually is no standard way to set it from Python, making switching between lookup tables cumbersome. The OpenGL-accelerated lookup table in the SLM object provides a solution to this problem, which is especially useful when working with tunable lasers, for which the lookup table needs to be adjusted often. The SLM object has a :attr:`~.slm.SLM.lookup_table` property, which holds a table that is used to convert phase values from radians to gray values on the screen. By default, this table is set to ``range(2 ** bit_depth)``, meaning that if an 8-bit video mode is selected, a phase of 0 produces a gray value of 0, and a phase of 255/256·2π produces a gray value of 255. A phase of 2π again produces a gray value of 0. .. _slm-synchronization: From ed74c0463b7b3aaff75c27ff3d847039bb01e5f1 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Mon, 7 Oct 2024 16:34:55 +0200 Subject: [PATCH 22/37] fixing installation for readthedocs --- .gitignore | 1 + .readthedocs.yaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 48a6f3f..18a0b50 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__ auto_examples build _build +requirements.txt .vs .bak PyDevice.sln diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d0d32a3..b7eb196 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,8 @@ build: post_create_environment: - pip install poetry - pip install poetry-plugin-export - - poetry export -f requirements.txt -o requirements.txt --with docs --without opengl + - poetry config warnings.export false' + - poetry export -f requirements.txt -o requirements.txt --with docs --extras "nidaq" --extras "genicam" - cat requirements.txt python: From ed0c4bd74b41b4ea098619bcb76e132bb5f27a0d Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Tue, 8 Oct 2024 09:01:32 +0200 Subject: [PATCH 23/37] Update testpypi.yml --- .github/workflows/testpypi.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testpypi.yml b/.github/workflows/testpypi.yml index 5405469..a9fc5c7 100644 --- a/.github/workflows/testpypi.yml +++ b/.github/workflows/testpypi.yml @@ -9,11 +9,11 @@ on: jobs: build: runs-on: ubuntu-latest + environment: name: testpypi url: https://test.pypi.org/p/openwfs - - # enable openid authentication for PyPi + permissions: id-token: write # Required for OIDC @@ -50,4 +50,5 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ + verbose: true From 2e9c38af8c3721b488b509fa357f91bccc3c9810 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Tue, 8 Oct 2024 10:24:30 +0200 Subject: [PATCH 24/37] fixing errors in documentation build --- .readthedocs.yaml | 1 + docs/source/core.rst | 44 ++++++++++++++++++--------------- docs/source/micromanager.rst | 4 +-- docs/source/readme.rst | 20 +++++++++------ docs/source/troubleshooting.rst | 4 +-- openwfs/devices/__init__.py | 7 ++---- openwfs/devices/slm/__init__.py | 4 ++- tests/test_wfs.py | 2 +- 8 files changed, 47 insertions(+), 39 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b7eb196..b88f599 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,7 @@ build: jobs: post_create_environment: + - pip install --upgrade pip - pip install poetry - pip install poetry-plugin-export - poetry config warnings.export false' diff --git a/docs/source/core.rst b/docs/source/core.rst index 3d921bd..0864162 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -13,21 +13,22 @@ Detectors Detectors in OpenWFS are objects that capture, generate, or process data. A Detector object may correspond to a physical device such as a camera, or it may be a software component that generates synthetic data (see :numref:`section-simulations`). Currently, the following detectors are supported: .. list-table:: + :widths: 30 70 :header-rows: 1 * - Detector - - * - Camera + * - devices.Camera - Supports all GenICam/GenTL cameras. - * - ScanningMicroscope + * - devices.ScanningMicroscope - Laser scanning microscope using galvo mirrors and National Instruments data acquisition card. - * - SimulatedWFS + * - simulation.SimulatedWFS - Simulated detector for testing wavefront shaping algorithms. - * - Microscope + * - simulation.Microscope - Fully simulated microscope, including aberrations, diffraction limit, and translation stage. - * - StaticSource + * - simulation.StaticSource - Returns pre-set data, simulating a static source. - * - NoiseSource + * - simulation.NoiseSource - Generates uniform or Gaussian noise as a source. All detectors derive from the :class:`~.Detector` base class. and have the following properties and methods: @@ -92,21 +93,22 @@ Processors A :class:`~.Processor` is an object that takes input from one or more other detectors, and combines/processes this data. By itself, a processor is a :class:`~.Detector`, enabling multiple processors to be chained together to combine their functionality. We already encountered an example in :numref:`Getting started`, where the :class:`~.SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. The OpenWFS currently includes the following processors: .. list-table:: + :widths: 30 70 :header-rows: 1 * - Processor - - * - SingleRoi + * - :class:`processors.SingleRoi` - Averages signal over a single ROI. - * - MultipleRoi + * - :class:`processors.MultipleRoi` - Averages signals over multiple regions of interest (ROIs). - * - CropProcessor + * - :class:`processors.CropProcessor` - Crops data from the source to a region of interest. - * - TransformProcessor + * - :class:`processors.TransformProcessor` - Performs affine transformations on the source data. - * - GaussianNoise + * - :class:`simulation.GaussianNoise` - Adds Gaussian noise to the source data. - * - ADCProcessor + * - :class:`simulation.ADCProcessor` - Simulates an analog-digital converter, including optional shot-noise and readout noise. @@ -116,35 +118,37 @@ Actuators Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators are derived from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. A list of actuators currently supported by OpenWFS can be found in the table below. .. list-table:: + :widths: 30 70 :header-rows: 1 :name: supported-actuators - * - SLM + * - :class:`devices.SLM` - Controls and renders patterns on a Spatial Light Modulator (SLM) using OpenGL - * - simulation.SLM + * - :class:`simulation.SLM` - Simulates a phase-only spatial light modulator, including timing and non-linear phase response. - * - simulation.XYStage + * - :class:`simulation.XYStage` - Simulates a translation stage, used in :class:`~Microscope`. Algorithms ------------ -OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could be implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the the Micro-Manager graphical user interface (GUI), see :ref:`micromanager`. +OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could be implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the the Micro-Manager graphical user interface (GUI), see :ref:`section-micromanager`. All algorithms are designed to be completely hardware-agnostic, so that they can be used with any type of feedback signal and either use real hardware or simulated hardware without the need to change a single line of code in the algorithm implementation. The :class:`~.FourierDualReference`, :class:`~.DualReference` and :class:`~.StepwiseSequential` algorithms provide support for optimizing multiple targets simulaneously in a single run of the algorithm. .. list-table:: + :widths: 30 70 :header-rows: 1 * - Algorithm - - * - FourierDualReference + * - :class:`algorithms.FourierDualReference` - A dual reference algorithm that uses plane waves from a disk in k-space for wavefront shaping :cite:`Mastiani2022`. - * - DualReference + * - :class:`algorithms.DualReference` - A generic dual reference algorithm with a configurable basis set :cite:`Cox2024`. - * - SimpleGenetic + * - :class:`algorithms.SimpleGenetic` - A simple genetic algorithm for optimiziang wavefronts :cite:`Piestun2012`. - * - StepwiseSequential + * - :class:`algorithms.StepwiseSequential` - A simplified version of the original wavefront shaping algorithm :cite:`Vellekoop2007`, with pre-optimization omitted. diff --git a/docs/source/micromanager.rst b/docs/source/micromanager.rst index 6069b5c..a4efa16 100644 --- a/docs/source/micromanager.rst +++ b/docs/source/micromanager.rst @@ -1,6 +1,6 @@ -.. _micromanager: +.. _section-micromanager: -OpenWFS in μ-Manager +OpenWFS in Micro-Manager ============================================== To smoothly enable end-user interaction with wavefront shaping algorithms, the Micro-Manager device adapter PyDevice was developed :cite:`PyDevice`. A more detailed description can be found in the mmCoreAndDevices source tree :cite:`mmCoreAndDevices`. In essence, PyDevice is Micro-Manager adapter that imports objects from a Python script and integrates them as devices, e.g. a camera or stage. OpenWFS was written in compliance with the templates required for PyDevice, which means OpenWFS cameras, scanners and algorithms can be loaded into Micro-Manager as devices. Examples of this are found in the example gallery :cite:`readthedocsOpenWFS`. Further developments due to this seamless connection can be a dedicated Micro-Manager based wavefront shaping GUI. \ No newline at end of file diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 30267e9..c26e055 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -35,11 +35,11 @@ OpenWFS is a Python package that is primarily designed for performing and for si * **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms without the need for physical hardware. -* **Wavefront shaping algorithms**. A (growing) collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, even advanced algorithms can be implemented in a few dozens of lines of code, and automaticallyt work with any combination of hardware and simulation tools that OpenWFS supports. +* **Wavefront shaping algorithms**. A growing collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, even advanced algorithms can be implemented in a few dozens of lines of code, and automatically work with any combination of hardware and simulation tools that OpenWFS supports. * **Platform for exchange and joint collaboration**. OpenWFS can be used as a platform for sharing and exchanging wavefront shaping algorithms. The package is designed to be modular and easy to expand, and it is our hope that the community will contribute to the package by adding new algorithms, hardware control modules, and simulation tools. Python was specifically chosen for this purpose for its active community, high level of abstraction and the ease of sharing tools. -* **Platform for simplifying use of wavefront shaping**. OpenWFS is compatible with the recently developed PyDevice :cite:`PyDevice`, and can therefore be controlled from Micro-Manager :cite:`MMoverview`, a commonly used microscopy control platform. +* **Micro-Manager compatibility**. Micro-Manager :cite:`MMoverview`, a widely used open-source microscopy control platform. The devices in OpenWFS, such as GenICam camera's, or the scanning microscope, as well as all algorithms, can be controlled from Micro-Manager using the recently developed :cite:`PyDevice` adapter that imports Python scripts into Micro-Manager * **Automated troubleshooting**. OpenWFS provides tools for automated troubleshooting of wavefront shaping experiments. This includes tools for measuring the performance of wavefront shaping algorithms, and for identifying common problems such as incorrect SLM calibration, drift, measurement noise, and other experimental imperfections. @@ -49,28 +49,32 @@ OpenWFS is a Python package that is primarily designed for performing and for si Here, we first show how to get started using OpenWFS for simulating and controlling wavefront shaping experiments. An in-depth discussion of the core design of OpenWFS is given in :numref:`Key concepts`. Key to any wavefront shaping experiment is the SLM. The support for advanced options like texture mapping and the use of a software lookup table are explained in :numref:`section-slms`. - The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. + The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. The automatic troubleshooter and Micro-Manager integration are discussed in :numref:`section-troubleshooting` and :numref:`section-micromanager`, respectively. + + Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. Getting started ---------------------- -To use OpenWFS, you need to have Python 3.9 or later installed. At the time of writing, OpenWFS is tested up to Python version 3.11 on Windows 11 and Manjaro Linux. OpenWFS is available on the PyPI repository. To install it, run the following command: +To use OpenWFS, Python 3.9 or later is required. Since it is available on the PyPI repository, OpenWFS can be installed using ``pip``: .. code-block:: bash pip install openwfs[all] -This will also install the optional dependencies for OpenWFS, such as ``PyOpenGL``, ``nidaqmx`` and ``harvesters``, which are used for OpenGL-accelerated SLM control, scanning microscopy, and camera control, respectively. These dependencies cannot be installed on your system, the installation will fail. At the time of writing, this can happen with ``PyOpenGL`` on systems without OpenGL driver installed, or for ``harvesters``, which currently only works for Python versions up to 3.11. You can instead install OpenWFS without dependencies by omitting ``[all]`` in the installation command, and then install only the required dependencies as indicated in the API documentation for each hardware component. The latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`, and the source code can be found on :cite:`openwfsgithub`. +This will also install the optional dependencies for OpenWFS, such as ``PyOpenGL``, ``nidaqmx`` and ``harvesters``, which are used for OpenGL-accelerated SLM control, scanning microscopy, and camera control, respectively. If these dependencies cannot be installed on your system, the installation will fail. This can happen with ``PyOpenGL`` on systems with no OpenGL driver installed, or for ``harvesters``, which currently only works for Python versions up to 3.11. In this case, you can instead install OpenWFS without dependencies by omitting ``[all]`` in the installation command, and manually install only the required dependencies as indicated in the documentation for each hardware component. + +At the time of writing, OpenWFS is tested up to Python version 3.11 on Windows 11 and Manjaro Linux, and the latest version is 0.1.0. Note that the latest versions of the package will be available on the PyPI repository, and the latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`. The source code can be found on :cite:`openwfsgithub`. + +:numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. It then runs a WFS algorithm to focus the light. -:numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. +This example uses the :class:`~.StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. .. _hello-wfs: .. literalinclude:: ../../examples/hello_wfs.py :language: python :caption: ``hello_wfs.py``. Example of a simple wavefront shaping experiment using OpenWFS. -This example uses the :class:`~.StepwiseSequential` wavefront shaping algorithm :cite:`vellekoop2008phase`. The algorithm needs access to the SLM for controlling the wavefront. This feedback is obtained from a :class:`~.SingleRoi` object, which takes images from the camera, and averages them over the specified circular region of interest. The algorithm returns the measured transmission matrix in the field `results.t`, which is used to compute the optimal phase pattern to compensate the aberrations. Finally, the code measures the intensity at the detector before and after applying the optimized phase pattern. - This code illustrates how OpenWFS separates the concerns of the hardware control (:class:`~.SLM` and :class:`~.Camera`), signal processing (:class:`~.SingleRoi`) and the algorithm itself (:class:`~.StepwiseSequential`). A large variety of wavefront shaping experiments can be performed by using different types of feedback signals (such as optimizing multiple foci simultaneously using a :class:`~.MultiRoi` object), using different algorithms, or different image sources, such as a :class:`~.ScanningMicroscope`. Notably, these objects can be replaced by *mock* objects, that simulate the hardware and allow for rapid prototyping and testing of new algorithms without direct access to wavefront shaping hardware (see :numref:`section-simulations`). diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst index 78966e4..5e63abe 100644 --- a/docs/source/troubleshooting.rst +++ b/docs/source/troubleshooting.rst @@ -1,8 +1,8 @@ -.. _troubleshooting: +.. _section-troubleshooting: Analysis and troubleshooting ================================================== -The principles of wavefront shaping are well established, and under close-to-ideal experimental conditions, it is possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the measurements from a wavefront shaping experiment. +The principles of wavefront shaping are well established and, under close-to-ideal experimental conditions, it is possible to accurately predict the signal enhancement. In practice, however, there exist many practical issues that can negatively affect the outcome of the experiment. OpenWFS has built-in functions to analyze and troubleshoot the measurements from a wavefront shaping experiment. The ``result`` structure in :numref:`hello-wfs`, as returned by the wavefront shaping algorithm, was computed with the utility function :func:`analyze_phase_stepping`. This function extracts the transmission matrix from phase stepping measurements, and additionally computes a series of troubleshooting statistics in the form of a *fidelity*, which is a number that ranges from 0 (no sensible measurement possible) to 1 (perfect situation, optimal focus expected). These fidelities are: diff --git a/openwfs/devices/__init__.py b/openwfs/devices/__init__.py index 43c69e6..924c169 100644 --- a/openwfs/devices/__init__.py +++ b/openwfs/devices/__init__.py @@ -10,8 +10,5 @@ except ImportError: pass # ok, we don't have nidaqmx installed -try: - from . import slm - from .slm import SLM -except ImportError: - pass # ok, we don't have glfw or PyOpenGL installed +from . import slm +from .slm import SLM diff --git a/openwfs/devices/slm/__init__.py b/openwfs/devices/slm/__init__.py index 02ef2d2..076fb93 100644 --- a/openwfs/devices/slm/__init__.py +++ b/openwfs/devices/slm/__init__.py @@ -1,3 +1,5 @@ +import warnings + try: import glfw import OpenGL @@ -10,7 +12,7 @@ Alternatively, specify the opengl extra when installing openwfs: ```pip install openwfs[opengl]``` - Note that these installs will fail if no suitable OpenGL driver is found on the system. + Note that these installs will fail if no suitable *OpenGL driver* is found on the system. Please make sure you have the latest video drivers installed. """ ) diff --git a/tests/test_wfs.py b/tests/test_wfs.py index 1057090..d0fe69f 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -357,7 +357,7 @@ def test_multidimensional_feedback_ssa(): # compute the phase pattern to optimize the intensity in target 2,1 target = (2, 1) - optimised_wf = -np.angle(t[*target, ...]) + optimised_wf = -np.angle(t[(*target, ...)]) # Calculate the enhancement factor # Note: technically this is not the enhancement, just the ratio after/before From 2696ebe15af4c7c5e1929e844674a93a82789142 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Tue, 8 Oct 2024 10:50:38 +0200 Subject: [PATCH 25/37] fixed typo in .readthedocs.yaml --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b88f599..e1f88da 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,7 +10,7 @@ build: - pip install --upgrade pip - pip install poetry - pip install poetry-plugin-export - - poetry config warnings.export false' + - poetry config warnings.export false - poetry export -f requirements.txt -o requirements.txt --with docs --extras "nidaq" --extras "genicam" - cat requirements.txt From d771256879ecee08172f77ba4c0950e2dec54456 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Tue, 8 Oct 2024 20:44:51 +0200 Subject: [PATCH 26/37] documentation and documentation build --- docs/source/core.rst | 40 +++++++++++-------------------- docs/source/development.rst | 15 ++++++------ docs/source/index.rst | 2 +- docs/source/index_latex.rst | 2 +- docs/source/readme.rst | 9 +++---- docs/source/slms.rst | 15 +++++------- openwfs/devices/__init__.py | 24 ++++++++++++++++--- openwfs/devices/slm/__init__.py | 19 --------------- openwfs/simulation/mockdevices.py | 5 +++- 9 files changed, 58 insertions(+), 73 deletions(-) diff --git a/docs/source/core.rst b/docs/source/core.rst index 0864162..be45186 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -13,11 +13,8 @@ Detectors Detectors in OpenWFS are objects that capture, generate, or process data. A Detector object may correspond to a physical device such as a camera, or it may be a software component that generates synthetic data (see :numref:`section-simulations`). Currently, the following detectors are supported: .. list-table:: - :widths: 30 70 - :header-rows: 1 + :widths: 27 73 - * - Detector - - * - devices.Camera - Supports all GenICam/GenTL cameras. * - devices.ScanningMicroscope @@ -93,22 +90,19 @@ Processors A :class:`~.Processor` is an object that takes input from one or more other detectors, and combines/processes this data. By itself, a processor is a :class:`~.Detector`, enabling multiple processors to be chained together to combine their functionality. We already encountered an example in :numref:`Getting started`, where the :class:`~.SingleRoiProcessor` was used to average the data from a camera over a region of interest. A block diagram of the data flow of this code is shown in :numref:`hellowfsdiagram`. The OpenWFS currently includes the following processors: .. list-table:: - :widths: 30 70 - :header-rows: 1 + :widths: 27 73 - * - Processor - - - * - :class:`processors.SingleRoi` + * - processors.SingleRoi - Averages signal over a single ROI. - * - :class:`processors.MultipleRoi` + * - processors.MultipleRoi - Averages signals over multiple regions of interest (ROIs). - * - :class:`processors.CropProcessor` + * - processors.CropProcessor - Crops data from the source to a region of interest. - * - :class:`processors.TransformProcessor` + * - processors.TransformProcessor - Performs affine transformations on the source data. - * - :class:`simulation.GaussianNoise` + * - simulation.GaussianNoise - Adds Gaussian noise to the source data. - * - :class:`simulation.ADCProcessor` + * - simulation.ADCProcessor - Simulates an analog-digital converter, including optional shot-noise and readout noise. @@ -118,30 +112,23 @@ Actuators Actuators are devices that *move* things in the setup. This can be literal, such as moving a translation stage, or a virtual movement, like an SLM that takes time to switch to a different phase pattern. All actuators are derived from the common :class:`.Actuator` base class. Actuators have no additional methods or properties other than those in the :class:`.Device` base class. A list of actuators currently supported by OpenWFS can be found in the table below. .. list-table:: - :widths: 30 70 - :header-rows: 1 - :name: supported-actuators + :widths: 27 73 - * - :class:`devices.SLM` + * - devices.SLM - Controls and renders patterns on a Spatial Light Modulator (SLM) using OpenGL - * - :class:`simulation.SLM` + * - simulation.SLM - Simulates a phase-only spatial light modulator, including timing and non-linear phase response. - * - :class:`simulation.XYStage` + * - simulation.XYStage - Simulates a translation stage, used in :class:`~Microscope`. Algorithms ------------ -OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could be implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the the Micro-Manager graphical user interface (GUI), see :ref:`section-micromanager`. - -All algorithms are designed to be completely hardware-agnostic, so that they can be used with any type of feedback signal and either use real hardware or simulated hardware without the need to change a single line of code in the algorithm implementation. The :class:`~.FourierDualReference`, :class:`~.DualReference` and :class:`~.StepwiseSequential` algorithms provide support for optimizing multiple targets simulaneously in a single run of the algorithm. +OpenWFS comes with a number of wavefront shaping algorithms already implemented, as listed in the table below. Although these algorithms could have been implemented as functions, we chose to implement them as objects, so that the parameters of the algorithm can be stored as attributes of the object. This simplifies keeping the parameters together in one place in the code, and also allows the algorithm parameters to be accessible in the Micro-Manager graphical user interface, see :ref:`section-micromanager`. .. list-table:: :widths: 30 70 - :header-rows: 1 - * - Algorithm - - * - :class:`algorithms.FourierDualReference` - A dual reference algorithm that uses plane waves from a disk in k-space for wavefront shaping :cite:`Mastiani2022`. * - :class:`algorithms.DualReference` @@ -151,6 +138,7 @@ All algorithms are designed to be completely hardware-agnostic, so that they can * - :class:`algorithms.StepwiseSequential` - A simplified version of the original wavefront shaping algorithm :cite:`Vellekoop2007`, with pre-optimization omitted. +All algorithms are designed to be completely hardware-agnostic, so that the exact same code can be used with any type of feedback signal on real or simulated hardware. All algorithms except the :class:`~.SimpleGenetic` algorithm provide support for optimizing multiple targets simulaneously in a single run of the algorithm. Units and metadata ---------------------------------- diff --git a/docs/source/development.rst b/docs/source/development.rst index ea0add9..6474a6b 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -48,11 +48,6 @@ Reporting bugs and contributing -------------------------------------------------- Bugs can be reported through the GitHub issue tracking system. Better than reporting bugs, we encourage users to *contribute bug fixes, new algorithms, device drivers, and other improvements*. These contributions can be made in the form of a pull request :cite:`zandonellaMassiddaOpenScience2022`, which will be reviewed by the development team and integrated into the package when appropriate. Please contact the current development team through GitHub :cite:`openwfsgithub` to coordinate such contributions. -Implementing new algorithms --------------------------------------------------- -To implement a new algorithm, the currently existing algorithms can be consulted for a few examples. -Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away in the calls to ``slm.set_phases`` and ``feedback.trigger``. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. - Implementing new devices -------------------------------------------------- @@ -66,7 +61,7 @@ When an actuator is started, or when a detector is triggered, it calls ``self._s Implementing a detector ++++++++++++++++++++++++++++++++++ -To implement a detector, the user should subclass the :meth:`~.Detector` base class, and implement properties and logic to control the detector hardware. In particular, the user should implement the :meth:`~Detector._do_trigger` method to start the measurement process, and the :meth:`~Detector._fetch()` method to fetch the data from the hardware, optionally process it, and return it as a numpy array. +To implement a detector, the user should subclass the :meth:`~.Detector` base class, and implement properties and logic to control the detector hardware. In particular, the user should implement the :meth:`~Detector._do_trigger` method to start the measurement process in the hardware if needed, and the :meth:`~Detector._fetch()` method to fetch the data from the hardware, optionally process it, and return it as a numpy array. A simple example of a detector that can be used as a starting point is the :class:`mockdevices.NoiseDetector`, which generates random noise with a given shape and pixel size. If ``duration``, ``pixel_size`` and ``data_shape`` are constants, they should be passed to the base class constructor. If these properties may change during operation, the user should override the ``duration``, ``pixel_size`` and ``data_shape`` properties to provide the correct values dynamically. If the ``duration`` is not known in advance (for example, when waiting for a hardware trigger), the Detector should implement the ``busy`` function to poll the hardware for the busy state. @@ -75,12 +70,18 @@ If the detector is created with the flag ``multi_threaded = True``, then :meth:` Implementing a processor ++++++++++++++++++++++++++++++++++ -To implement a data processing step that dynamically processes data from one or more input detectors, implement a custom processor. This is done by deriving from the :class:`~.Processor` base class and implementing the ``__init__`` function. This function should pass a list of all upstream nodes, i.e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth:`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls :meth:`~.Detector._fetch()` with this data as input. See the implementation of :class:`~.Shutter` or any other processor for an example of how to implement this function. +To implement a data processing step that dynamically processes data from one or more input detectors, implement a custom processor. This is done by deriving from the :class:`~.Processor` base class and implementing the ``__init__`` function. This function should pass a list of all upstream nodes, i.e. all detectors which provide the input signals to the processor, the base class constructor. In addition, the :meth:`~Detector._fetch()` method should be implemented to process the data. The framework will wait until the data from all sources is available, and calls :meth:`~.Detector._fetch()` with this data as input. See the implementation of :class:`~.GaussianNoise` or any other processor for an example of how to implement this function. Implementing an actuator +++++++++++++++++++++++++++++++ To implement an actuator, the user should subclass the :class:`~Actuator` base class, and implement whatever properties and logic appropriate to the device. All methods that start the actuator (e.g. ``update()`` or ``move()``), should first call ``self._start()`` to request a state switch to the ``moving`` state. As for detectors, actuators should either specify a static ``duration` and ``latency`` if known, or override these properties to return run-time values for the duration and latency. Similarly, if the duration of an action of the actuator is not known in advance, the class should override ``busy`` to poll for the action to complete. +Implementing new algorithms +-------------------------------------------------- +Algorithms in OpenWFS do not necessarily need to be implemented as classes. However, the included algorithms are wrapped in a class so that the parameters of the algorithm can be viewed and changed from the Micro-Manager GUI. Moreover, wrapping are implemented as classes with an ``execute()`` method. +that inherit from the :class:`~.Algorithm` base class. +To implement a new algorithm, the currently existing algorithms can be consulted for a few examples. +Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away in the calls to ``slm.set_phases`` and ``feedback.trigger``. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. diff --git a/docs/source/index.rst b/docs/source/index.rst index c7886ca..017b2dc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,8 +9,8 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments core slms simulations - micromanager troubleshooting + micromanager development api auto_examples/index diff --git a/docs/source/index_latex.rst b/docs/source/index_latex.rst index aad09ab..e32f627 100644 --- a/docs/source/index_latex.rst +++ b/docs/source/index_latex.rst @@ -9,7 +9,7 @@ OpenWFS - a library for conducting and simulating wavefront shaping experiments core slms simulations - micromanager troubleshooting + micromanager development conclusion diff --git a/docs/source/readme.rst b/docs/source/readme.rst index c26e055..76e0964 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -33,7 +33,7 @@ OpenWFS is a Python package that is primarily designed for performing and for si * **GenICam cameras**. The :class:`~.devices.Camera` object uses the ``harvesters`` backend :cite:`harvesters` to access any camera supporting the GenICam standard :cite:`genicam`. * **Automatic synchronization**. OpenWFS provides tools for automatic synchronization of actuators (e.g. an SLM) and detectors (e.g. a camera). The automatic synchronization makes it trivial to perform pipelined measurements that avoid the delay normally caused by the latency of the video card and SLM. -* **Simulation**. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms without the need for physical hardware. +* **Simulation**. The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. OpenWFS provides an extensive framework for testing and simulating wavefront shaping algorithms, including the effect of measurement noise, stage drift, and user-defined aberrations. This allows for rapid prototyping and testing of new algorithms without the need for physical hardware. * **Wavefront shaping algorithms**. A growing collection of wavefront shaping algorithms. OpenWFS abstracts the hardware control, synchronization, and signal processing so that the user can focus on the algorithm itself. As a result, even advanced algorithms can be implemented in a few dozens of lines of code, and automatically work with any combination of hardware and simulation tools that OpenWFS supports. @@ -47,12 +47,9 @@ OpenWFS is a Python package that is primarily designed for performing and for si .. only:: latex - Here, we first show how to get started using OpenWFS for simulating and controlling wavefront shaping experiments. An in-depth discussion of the core design of OpenWFS is given in :numref:`Key concepts`. Key to any wavefront shaping experiment is the SLM. The support for advanced options like texture mapping and the use of a software lookup table are explained in :numref:`section-slms`. - - The ability to simulate optical experiments is essential for the rapid development and debugging of wavefront shaping algorithms. The built-in options for realistically simulating experiments are be discussed in :numref:`section-simulations`. The automatic troubleshooter and Micro-Manager integration are discussed in :numref:`section-troubleshooting` and :numref:`section-micromanager`, respectively. - - Finally, OpenWFS is designed to be modular and easy to extend. In :numref:`section-development`, we show how to write custom hardware control modules. Note that not all functionality of the package is covered in this document, and we refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. + Here, we first show how to get started using OpenWFS for simulating and controlling wavefront shaping experiments. An in-depth discussion of the core design of OpenWFS is given in :numref:`Key concepts`. Key to any wavefront shaping experiment is the spatial light modulator. The support for advanced options like texture mapping and the use of a software lookup table are explained in :numref:`section-slms`. The tools for realistically simulating experiments, automatic troubleshooting of experiments, and Micro-Manager integration are discussed in :numref:`section-simulations`, :numref:`section-troubleshooting`, and :numref:`section-micromanager`, respectively. Finally, in :numref:`section-development`, we show how to write custom hardware control modules in order to extend the functionality of OpenWFS. + Note that not all functionality of the package is covered in this document. We refer to the API documentation :cite:`openwfsdocumentation` for a complete overview of most recent version of the package. Getting started ---------------------- diff --git a/docs/source/slms.rst b/docs/source/slms.rst index 5d56689..82d4adb 100644 --- a/docs/source/slms.rst +++ b/docs/source/slms.rst @@ -26,19 +26,16 @@ Texture mapping and blending Sample output of the SLM object, generated by the script ``examples/slm_disk.py``. Here, two patches were used: a circular one with large segments in concentric rings, and a second one showing a superposed phase gradient. -On top of the basic functionality, the :class:`hardware.SLM` object provides advanced functionality for controlling how the pixels in the phase array are mapped to the screen. This functionality uses the texture mapping capabilities of the graphics card (:cite:`neider1993opengl`) to allow for arbitrary transformations of phase maps to the screen. +On top of the basic functionality, the :class:`hardware.SLM` object provides advanced functionality for controlling how the pixels in the phase array are mapped to the screen. This functionality uses the texture mapping capabilities of the graphics card (see, e.g. :cite:`neider1993opengl`) to allow for arbitrary transformations of phase maps to the screen. -Texture mapping involves two components: a texture and a geometry, which are stored together in a :class:`hardware.SLM.Patch` object. The *texture* is a 2-D array holding phase values. Values in the texture are referenced by texture coordinates ranging from 0 to 1. +Texture mapping involves two components: a texture and a geometry, which are stored together in a :class:`~.hardware.SLM.Patch` object. The *texture* is a 2-D array holding phase values in radians. Values in the texture are referenced by texture coordinates ranging from 0 to 1. The *geometry* describes a set of triangles that is drawn to the screen, with each triangle holding a 2-D screen coordinate and a 2-D texture coordinate. The screen coordinate determines where the vertex is drawn on the screen, and the texture coordinate determines which pixel in the texture is used to color the vertex. When drawing the triangles, OpenGL automatically interpolates the texture coordinates between the vertices, and looks up the nearest value in the phase texture. -The *geometry* describes a set of triangles that is drawn to the screen, with each triangle holding a 2-D screen coordinate and a 2-D texture coordinate. The screen coordinate determines where the vertex is drawn on the screen, and the texture coordinate determines which pixel in the texture is used to color the vertex. - -If an SLM holds multiple patches, the patches are drawn in the order they are present in the :attr:`~.slm.SLM.patches` list. If patches overlap, the pixels of the previous patch are either overwritten (when the :attr:`~.Patch.additive_blend` property of the patch is ``False``), or added to the phase values of the previous patch (when :attr:`~.Patch.additive_blend` is ``True``). - -In the simplest form, square texture is mapped to a square region on the screen. This region is comprised of two triangles, with the screen coordinates corresponding to the vertices of the square. The vertices hold texture coordinates ranging from (0,0) to (1,1). The graphics card then interpolates the texture coordinates between the vertices, and for each screen pixel looks up the nearest value in the texture. This way, the texture is scaled to fit the region, regardless of how many elements the texture map has. +In the simplest form, a square texture is mapped to a square region on the screen. This region is comprised of two triangles, with the screen coordinates corresponding to the vertices of the square. The vertices hold texture coordinates ranging from (0,0) to (1,1). The graphics card then interpolates the texture coordinates between the vertices, and for each screen pixel looks up the nearest value in the texture. This way, the texture is scaled to fit the region, regardless of how many elements the texture map has. A more advanced example is shown in :numref:`slmdemo`, where the texture is mapped to a disk. The disk is drawn as a set of triangles, with the screen coordinates corresponding to points on the concentric rings that form the disk. In this example, the texture was a 1 × 18 element array with random values. The texture coordinates were defined such that the elements of this array are mapped to three concentric rings, consisting of 4, 6, and 8 segments, respectively (see :numref:`slmcode`). Such an approach can be useful for equalizing the contribution of different segments on the SLM :cite:`mastiani2021noise`. -The example in :numref:`slmdemo` also demonstrates a second capability of the SLM object: blending. In addition to the patch that describes the disk, a second patch was drawn that holds a linear gradient mapped to a square. The graphics card draws both patches and adds the phase values at each pixel, wrapping the values at 2π. This way, the gradient steers the light coming from the SLM, while the disk texture determines the shape of the wavefront. +The example in :numref:`slmdemo` also demonstrates a second capability of the SLM object, namely using multiple patches simultaneously on a single SLM. The patches are drawn in the order they are present in the :attr:`~.slm.SLM.patches` list. At the pixels where the patches overlap, the phase values for the two patches are added and wrapped to the interval [0, 2π). In the example, the first patch describes the disk, and a second square patch was drawn on top of it. This second patch holds a linear gradient, which may be used to steer the light coming from the SLM.LM, while the disk texture determines the shape of the wavefront. This blending behavior can be disabled by setting :attr:`~.Patch.additive_blend` ``= False``, in which case each patch just overwrites the pixels drawn by previous patches. + .. _slmcode: .. literalinclude:: ../../examples/slm_disk.py @@ -56,7 +53,7 @@ The combination of texture mapping and blending allows for a wide range of use c All of these corrections can be done in real time using OpenGL acceleration, making the SLM object a versatile tool for wavefront shaping experiments. -A final aspect of the SLM that is demonstrated in the example is the use of the :attr:`~.slm.SLM.pixels` attribute. This attribute holds a virtual camera that reads the gray values of the pixels currently displayed on the SLM. This virtual camera implements the :class:`~.Detector` interface, meaning that it can be used just like an actual camera. This feature is useful for storing the images displayed on the SLM. To retrieve the electrical field instead of the phase, use the :attr:`~.slm.SLM.fields` attribute. +A final aspect of the SLM that is demonstrated in the example is the use of the :attr:`~.slm.SLM.pixels` attribute. This attribute holds a virtual camera that reads the gray values of the pixels currently displayed on the SLM. This virtual camera implements the :class:`~.Detector` interface, meaning that it can be used just like an actual camera. This feature is useful, e.g., for storing or checking the images displayed on the SLM. For debugging or demonstration purposes, it is often useful to receive feedback on the image displayed on the SLM. In Windows, this image can be see by hovering over the program icon in the task bar. Alternatively, the combination Ctrl + PrtScn can be used to grab the image on all active monitors. For demonstration purposes, the :func:`~.slm.SLM.clone` function can be used to create a second SLM window (typically placed in a corner of the primary screen), which shows the same image as the original SLM. This technique is used in the ``wfs_demonstration_experimental.py`` code available in the online example gallery :cite:`readthedocsOpenWFS`. diff --git a/openwfs/devices/__init__.py b/openwfs/devices/__init__.py index 924c169..f14b546 100644 --- a/openwfs/devices/__init__.py +++ b/openwfs/devices/__init__.py @@ -1,3 +1,24 @@ +import warnings + +try: + import glfw + import OpenGL +except ImportError: + warnings.warn( + """Could not initialize OpenGL because the glfw or PyOpenGL package is missing. + To install, make sure to install the required packages: + ```pip install glfw``` + ```pip install PyOpenGL``` + Alternatively, specify the opengl extra when installing openwfs: + ```pip install openwfs[opengl]``` + + Note that these installs will fail if no suitable *OpenGL driver* is found on the system. + Please make sure you have the latest video drivers installed. + """ + ) +from . import slm +from .slm import SLM + try: from .camera import Camera except ImportError: @@ -9,6 +30,3 @@ from .nidaq_gain import Gain except ImportError: pass # ok, we don't have nidaqmx installed - -from . import slm -from .slm import SLM diff --git a/openwfs/devices/slm/__init__.py b/openwfs/devices/slm/__init__.py index 076fb93..31142aa 100644 --- a/openwfs/devices/slm/__init__.py +++ b/openwfs/devices/slm/__init__.py @@ -1,22 +1,3 @@ -import warnings - -try: - import glfw - import OpenGL -except ImportError: - raise ImportError( - """Could not initialize OpenGL because the glfw or PyOpenGL package is missing. - To install, make sure to install the required packages: - ```pip install glfw``` - ```pip install PyOpenGL``` - Alternatively, specify the opengl extra when installing openwfs: - ```pip install openwfs[opengl]``` - - Note that these installs will fail if no suitable *OpenGL driver* is found on the system. - Please make sure you have the latest video drivers installed. - """ - ) - from . import geometry from . import patch from . import shaders diff --git a/openwfs/simulation/mockdevices.py b/openwfs/simulation/mockdevices.py index 7e8aeaa..d31d634 100644 --- a/openwfs/simulation/mockdevices.py +++ b/openwfs/simulation/mockdevices.py @@ -108,6 +108,9 @@ def __init__( multi_threaded=multi_threaded, ) + def _do_trigger(self) -> None: + pass # no hardware triggering is needed for this mock device + def _fetch(self) -> np.ndarray: # noqa if self._noise_type == "uniform": return self._rng.uniform(**self._noise_arguments, size=self.data_shape) @@ -407,7 +410,7 @@ class GaussianNoise(Processor): multi_threaded: Whether to perform processing in a worker thread. """ - def __init__(self, source: Detector, std: float, multi_threaded: bool = True): + def __init__(self, source: Detector, std: float, multi_threaded: bool = False): super().__init__(source, multi_threaded=multi_threaded) self._std = std From 452057aa07b9aabf9b53a3c53e53f225d3134506 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Tue, 8 Oct 2024 20:46:39 +0200 Subject: [PATCH 27/37] enabled opengl for debugging readthedocs --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e1f88da..bb07e7d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,7 +11,7 @@ build: - pip install poetry - pip install poetry-plugin-export - poetry config warnings.export false - - poetry export -f requirements.txt -o requirements.txt --with docs --extras "nidaq" --extras "genicam" + - poetry export -f requirements.txt -o requirements.txt --with docs --extras "nidaq" --extras "genicam" --extras "opengl" - cat requirements.txt python: From cebfaa22a0d51d82c8a67413cf4c599c42a5188e Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Wed, 9 Oct 2024 11:26:39 +0200 Subject: [PATCH 28/37] cleaning up algorithm and WFScontroller --- STYLEGUIDE.md | 10 +- docs/source/conf.py | 2 +- openwfs/algorithms/basic_fourier.py | 4 - openwfs/algorithms/dual_reference.py | 12 +- openwfs/algorithms/utilities.py | 176 ++++++++++++++------------- tests/test_wfs.py | 23 ++-- 6 files changed, 118 insertions(+), 109 deletions(-) diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 29236a3..1a0aa7a 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -13,6 +13,11 @@ line limit can be very cumbersome. - PEP 8:E203 whitespace before ':'. May be disabled. This is already checked by (and conflicts with) black. +# Layout + +- Line length of code is limited to 120 characters by `black`. +- Use soft wrapping for Markdown (`.md`) and reStructuredTest (`.rst`) files. + # Tests - Tests must *not* plot figures. @@ -39,4 +44,7 @@ Common warnings: images, use ``len(images)`` instead of ``images.shape(0)``. But to access the number of rows in an image, use ``image.shape(0)``. - + # Properties +- Document properties in the getter method only, not in the setter method. Also describe what happens if the property is + set. +- \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 6a05b01..16a13ec 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -137,7 +137,7 @@ # Hide some classes that are not production ready yet def skip(_app, _what, name, _obj, do_skip, _options): - if name in ("WFSController", "Gain"): + if name in ("Gain"): return True return do_skip diff --git a/openwfs/algorithms/basic_fourier.py b/openwfs/algorithms/basic_fourier.py index 53d302d..3c26a41 100644 --- a/openwfs/algorithms/basic_fourier.py +++ b/openwfs/algorithms/basic_fourier.py @@ -37,7 +37,6 @@ def __init__( k_step: float = 1.0, iterations: int = 2, amplitude: np.ndarray = 1.0, - analyzer: Optional[callable] = analyze_phase_stepping, optimized_reference: Optional[bool] = None ): """ @@ -51,8 +50,6 @@ def __init__( k_step: Make steps in k-space of this value. 1 corresponds to diffraction limited tilt. iterations: Number of ping-pong iterations. Defaults to 2. amplitude: Amplitude profile over the SLM. Defaults to 1.0 (flat) - analyzer: The function used to analyze the phase stepping data. - Must return a WFSResult object. Defaults to `analyze_phase_stepping` optimized_reference: When `True`, during each iteration the other half of the SLM displays the optimized pattern so far (as in [1]). When `False`, the algorithm optimizes A with a flat wavefront on B, @@ -77,7 +74,6 @@ def __init__( iterations=iterations, amplitude=amplitude, optimized_reference=optimized_reference, - analyzer=analyzer, ) def _construct_modes(self) -> tuple[np.ndarray, np.ndarray]: diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index 0eb882d..05c21ca 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -46,7 +46,6 @@ def __init__( amplitude: nd = 1.0, phase_steps: int = 4, iterations: int = 2, - analyzer: Optional[callable] = analyze_phase_stepping, optimized_reference: Optional[bool] = None ): """ @@ -79,19 +78,13 @@ def __init__( When set to `None` (default), the algorithm uses True if there is a single target, and False if there are multiple targets. - analyzer: The function used to analyze the phase stepping data. - Must return a WFSResult object. Defaults to `analyze_phase_stepping` - [1]: X. Tao, T. Lam, B. Zhu, et al., “Three-dimensional focusing through scattering media using conjugate adaptive optics with remote focusing (CAORF),” Opt. Express 25, 10368–10383 (2017). """ if optimized_reference is None: # 'auto' mode optimized_reference = np.prod(feedback.data_shape) == 1 elif optimized_reference and np.prod(feedback.data_shape) != 1: - raise ValueError( - "When using an optimized reference, the feedback detector should return a single scalar value." - ) - + raise ValueError("In optimized_reference mode, only scalar (single target) feedback signals can be used.") if iterations < 2: raise ValueError("The number of iterations must be at least 2.") if not optimized_reference and iterations != 2: @@ -102,7 +95,6 @@ def __init__( self.phase_steps = phase_steps self.optimized_reference = optimized_reference self.iterations = iterations - self._analyzer = analyzer self._phase_patterns = None self._gram = None self._shape = group_mask.shape @@ -298,7 +290,7 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, progress_bar.update() self.feedback.wait() - return self._analyzer(measurements, axis=1) + return analyze_phase_stepping(measurements, axis=1) def compute_t_set(self, t, side) -> nd: """ diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 86e46c1..5908135 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -143,6 +143,10 @@ def weighted_average(attribute): fidelity_calibration=weighted_average("fidelity_calibration"), ) + @property + def snr(self): + return 1.0 / (1.0 / self.fidelity_noise - 1.0) + def analyze_phase_stepping(measurements: np.ndarray, axis: int): """Analyzes the result of phase stepping measurements, returning matrix `t` and noise statistics @@ -240,88 +244,93 @@ def analyze_phase_stepping(measurements: np.ndarray, axis: int): class WFSController: """ - Controller for Wavefront Shaping (WFS) operations using a specified algorithm in the MicroManager environment. + EXPERIMENTAL - Controller for Wavefront Shaping (WFS) operations using a specified algorithm in the Micro-Manager environment. + + Usage: + + .. code-block:: python + + # not wrapped: + alg = FourierDualReference(feedback, slm) + + # wrapped + alg = WFSController(FourierDualReference, feedback, slm) + + Under the hood, a dynamic class is created that inherits both ``WFSController`` and ``FourierDualReference)``. + Effectively this is similar to having ``class WFSController(FourierDualReference)`` inheritance. + + Since Micro-Manager / PyDevice does not yet support buttons to activate actions, a WFS experiment is started by setting + the trigger attribute :attr:`wavefront` to the value State.OPTIMIZED + It adds attributes for inspecting the statistics of the last WFS optimization. Manages the state of the wavefront and executes the algorithm to optimize and apply wavefront corrections, while exposing all these parameters to MicroManager. """ class State(Enum): - FLAT_WAVEFRONT = 0 - SHAPED_WAVEFRONT = 1 + FLAT = 0 + OPTIMIZED = 1 + REOPTIMIZE = 2 - def __init__(self, algorithm): + def __init__(self, _algorithm_class, *args, **kwargs): """ Args: algorithm: An instance of a wavefront shaping algorithm. """ - self.algorithm = algorithm - self._wavefront = WFSController.State.FLAT_WAVEFRONT - self._result = None - self._noise_factor = None - self._amplitude_factor = None - self._estimated_enhancement = None - self._calibration_fidelity = None - self._estimated_optimized_intensity = None - self._snr = None # Average SNR. Computed when wavefront is computed. - self._optimized_wavefront = None - self._recompute_wavefront = False - self._feedback_enhancement = None + super().__init__(*args, **kwargs) + self._wavefront = WFSController.State.FLAT + self._result: Optional[WFSResult] = None + self._feedback_ratio = 0.0 self._test_wavefront = False # Trigger to test the optimized wavefront - self._run_troubleshooter = False # Trigger troubleshooter - self.dark_frame = None - self.before_frame = None + + def __new__(cls, algorithm_class, *args, **kwargs): + """Dynamically creates a class of type `class X(WFSController, algorithm_class` and returns an instance of that class""" + + # Dynamically create the class using type() + class_name = "WFSController_" + algorithm_class.__name__ + DynamicClass = type(class_name, (cls, algorithm_class), {}) + instance = super(WFSController, cls).__new__(DynamicClass) + return instance @property def wavefront(self) -> State: """ - Gets the current wavefront state. - - Returns: - State: The current state of the wavefront, either FLAT_WAVEFRONT or SHAPED_WAVEFRONT. + Enables switching between FLAT or OPTIMIZED wavefront on the SLM. + Setting this state to OPTIMIZED causes the algorithm execute if the optimized wavefront is not yet computed. + Setting this state to REOPTIMIZE always causes the algorithm to recompute the wavefront. The state switches to OPTIMIZED after executioin of the algorithm. + For multi-target optimizations, OPTIMIZED shows the wavefront for the first target. """ return self._wavefront @wavefront.setter def wavefront(self, value): - """ - Sets the wavefront state and applies the corresponding phases to the SLM. - - Args: - value (State): The desired state of the wavefront to set. - """ - self._wavefront = value - if value == WFSController.State.FLAT_WAVEFRONT: - self.algorithm.slm.set_phases(0.0) - else: - if self._recompute_wavefront or self._optimized_wavefront is None: - # select only the wavefront and statistics for the first target - result = self.algorithm.execute().select_target(0) - self._optimized_wavefront = -np.angle(result.t) - self._noise_factor = result.fidelity_noise - self._amplitude_factor = result.fidelity_amplitude - self._estimated_enhancement = result.estimated_enhancement - self._calibration_fidelity = result.fidelity_calibration - self._estimated_optimized_intensity = result.estimated_optimized_intensity - self._snr = 1.0 / (1.0 / result.fidelity_noise - 1.0) - self._result = result - self.algorithm.slm.set_phases(self._optimized_wavefront) + self._wavefront = WFSController.State(value) + if value == WFSController.State.FLAT: + self.slm.set_phases(0.0) + elif value == WFSController.State.OPTIMIZED: + if self._result is None: + # run the algorithm + self._result = self.execute().select_target(0) + self.slm.set_phases(self.optimized_wavefront) + else: # value == WFSController.State.REOPTIMIZE: + self._result = None # remove stored result + self.wavefront = WFSController.State.OPTIMIZED # recompute the wavefront @property - def noise_factor(self) -> float: + def fidelity_noise(self) -> float: """ Returns: - float: noise factor: the estimated loss in fidelity caused by the limited snr. + float: the estimated loss in fidelity caused by the limited snr. """ - return self._noise_factor + return self._result.fidelity_noise if self._result is not None else 0.0 @property - def amplitude_factor(self) -> float: + def fidelity_amplitude(self) -> float: """ Returns: - float: amplitude factor: estimated reduction of the fidelity due to phase-only + float: estimated reduction of the fidelity due to phase-only modulation (≈ π/4 for fully developed speckle) """ - return self._amplitude_factor + return self._result.fidelity_amplitude if self._result is not None else 0.0 @property def estimated_enhancement(self) -> float: @@ -330,15 +339,15 @@ def estimated_enhancement(self) -> float: float: estimated enhancement: estimated ratio / (with <> denoting ensemble average) """ - return self._estimated_enhancement + return self._result.estimated_enhancement if self._result is not None else 0.0 @property - def calibration_fidelity(self) -> float: + def fidelity_calibration(self) -> float: """ Returns: float: non-linearity. """ - return self._calibration_fidelity + return self._result.fidelity_calibration if self._result is not None else 0.0 @property def estimated_optimized_intensity(self) -> float: @@ -346,52 +355,47 @@ def estimated_optimized_intensity(self) -> float: Returns: float: estimated optimized intensity. """ - return self._estimated_optimized_intensity + return self._estimated_optimized_intensity if self._result is not None else 0.0 @property def snr(self) -> float: """ - Gets the signal-to-noise ratio (SNR) of the optimized wavefront. - Returns: - float: The average SNR computed during wavefront optimization. + float: The average signal-to-noise ratio (SNR) of the wavefront optimization measurements. """ - return self._snr + return self._result.snr if self._result is not None else 0.0 @property - def recompute_wavefront(self) -> bool: - """Returns: bool that indicates whether the wavefront needs to be recomputed.""" - return self._recompute_wavefront - - @recompute_wavefront.setter - def recompute_wavefront(self, value): - """Sets the bool that indicates whether the wavefront needs to be recomputed.""" - self._recompute_wavefront = value + def optimized_wavefront(self) -> np.ndarray: + return -np.angle(self._result.t) if self._result is not None else 0.0 @property - def feedback_enhancement(self) -> float: - """Returns: the average enhancement of the feedback, returns none if no such enhancement was measured.""" - return self._feedback_enhancement + def feedback_ratio(self) -> float: + """The ratio of average feedback signals after and before optimization. + + This value is calculated when the :attr:`test_wavefront` trigger is set to True. + + Note: this is *not* the enhancement factor, because the 'before' signal is not ensemble averaged. + Therefore, this value should be used with caution. + + Returns: + float: average enhancement of the feedback, 0.0 none if no such enhancement was measured.""" + return self._feedback_ratio @property def test_wavefront(self) -> bool: - """Returns: bool that indicates whether test_wavefront will be performed if set.""" - return self._test_wavefront + """Trigger to test the wavefront. - @test_wavefront.setter - def test_wavefront(self, value): + Set this value `True` to measure feedback signals with a flat and an optimized wavefront and compute the :attr:`feedback_ratio`. + This value is reset to `False` after the test is performed. """ - Calculates the feedback enhancement between the flat and shaped wavefronts by measuring feedback for both - cases. + return False - Args: - value (bool): True to enable test mode, False to disable. - """ + @test_wavefront.setter + def test_wavefront(self, value): if value: - self.wavefront = WFSController.State.FLAT_WAVEFRONT - feedback_flat = self.algorithm.feedback.read().copy() - self.wavefront = WFSController.State.SHAPED_WAVEFRONT - feedback_shaped = self.algorithm.feedback.read().copy() - self._feedback_enhancement = float(feedback_shaped.sum() / feedback_flat.sum()) - - self._test_wavefront = value + self.wavefront = WFSController.State.FLAT + feedback_flat = self.feedback.read().sum() + self.wavefront = WFSController.State.OPTIMIZED + feedback_shaped = self.feedback.read().sum() + self._feedback_ratio = float(feedback_shaped / feedback_flat) diff --git a/tests/test_wfs.py b/tests/test_wfs.py index d0fe69f..999ffea 100644 --- a/tests/test_wfs.py +++ b/tests/test_wfs.py @@ -192,17 +192,26 @@ def t_fidelity( return fidelity / norm -@pytest.mark.skip("Not implemented") def test_fourier2(): """Test the Fourier dual reference algorithm using WFSController.""" - slm_shape = (1000, 1000) + slm_shape = (10, 10) aberrations = skimage.data.camera() * ((2 * np.pi) / 255.0) sim = SimulatedWFS(aberrations=aberrations) - alg = FourierDualReference(feedback=sim, slm=sim.slm, slm_shape=slm_shape, k_radius=7.5, phase_steps=3) - controller = WFSController(alg) - controller.wavefront = WFSController.State.SHAPED_WAVEFRONT - scaled_aberration = zoom(aberrations, np.array(slm_shape) / aberrations.shape) - assert_enhancement(sim.slm, sim, controller._result, np.exp(1j * scaled_aberration)) + alg = WFSController( + FourierDualReference, feedback=sim, slm=sim.slm, slm_shape=slm_shape, k_radius=3.5, phase_steps=3 + ) + + # check if the attributes of the algorithm were passed through correctly + assert alg.k_radius == 3.5 + alg.k_radius = 2.5 + assert alg.k_radius == 2.5 + before = sim.read() + alg.wavefront = WFSController.State.OPTIMIZED # this will trigger the algorithm to optimize the wavefront + after = sim.read() + alg.wavefront = WFSController.State.FLAT # this set the wavefront back to flat + before2 = sim.read() + assert before == before2 + assert after / before > 3.0 @pytest.mark.skip("Not implemented") From a00d559e9762e9d067a4766b66cdcc025437566b Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Wed, 9 Oct 2024 12:04:48 +0200 Subject: [PATCH 29/37] harmonized optional imports --- openwfs/devices/__init__.py | 51 ++++++++--------- openwfs/devices/camera.py | 14 ++--- openwfs/devices/galvo_scanner.py | 15 ++--- openwfs/devices/nidaq_gain.py | 10 +++- openwfs/devices/slm/context.py | 4 +- openwfs/devices/slm/patch.py | 98 ++++++++++++-------------------- openwfs/devices/slm/slm.py | 47 ++++++--------- openwfs/devices/slm/texture.py | 55 +++++++----------- pyproject.toml | 45 +++++++-------- 9 files changed, 134 insertions(+), 205 deletions(-) diff --git a/openwfs/devices/__init__.py b/openwfs/devices/__init__.py index f14b546..a53b058 100644 --- a/openwfs/devices/__init__.py +++ b/openwfs/devices/__init__.py @@ -1,32 +1,29 @@ +import importlib import warnings -try: - import glfw - import OpenGL -except ImportError: - warnings.warn( - """Could not initialize OpenGL because the glfw or PyOpenGL package is missing. - To install, make sure to install the required packages: - ```pip install glfw``` - ```pip install PyOpenGL``` - Alternatively, specify the opengl extra when installing openwfs: - ```pip install openwfs[opengl]``` - Note that these installs will fail if no suitable *OpenGL driver* is found on the system. - Please make sure you have the latest video drivers installed. - """ - ) -from . import slm -from .slm import SLM +def safe_import(module_name): + try: + return importlib.import_module(module_name) + except ModuleNotFoundError: + warnings.warn( + f"""Could not import {module_name}, because the package is not installed. + To install, use: + pip install {module_name} + + Alternatively, specify to install the required extras when installing openwfs, using one of: + pip install openwfs[all] + pip install openwfs[opengl] + pip install openwfs[genicam] + pip install openwfs[nidaq] + """ + ) + return None -try: - from .camera import Camera -except ImportError: - pass # ok, we don't have harvesters installed -try: - from . import galvo_scanner - from .galvo_scanner import ScanningMicroscope, Axis - from .nidaq_gain import Gain -except ImportError: - pass # ok, we don't have nidaqmx installed +from . import slm +from .slm import SLM +from .camera import Camera +from . import galvo_scanner +from .galvo_scanner import ScanningMicroscope, Axis +from .nidaq_gain import Gain diff --git a/openwfs/devices/camera.py b/openwfs/devices/camera.py index 66126f8..f3a4e6d 100644 --- a/openwfs/devices/camera.py +++ b/openwfs/devices/camera.py @@ -4,17 +4,11 @@ import numpy as np from astropy.units import Quantity -try: +from . import safe_import + +hc = safe_import("harvesters.core") +if hc is not None: from harvesters.core import Harvester -except ImportError: - raise ImportError( - """The harvesters package is required for the Camera class. - To install: - ```pip install harvesters``` - Alternatively, specify the genicam dependency when installing openwfs: - ```pip install openwfs[genicam]``` - """ - ) from ..core import Detector diff --git a/openwfs/devices/galvo_scanner.py b/openwfs/devices/galvo_scanner.py index 23f8811..d503c71 100644 --- a/openwfs/devices/galvo_scanner.py +++ b/openwfs/devices/galvo_scanner.py @@ -7,20 +7,13 @@ from annotated_types import Ge, Le from astropy.units import Quantity -try: - import nidaqmx as ni +from . import safe_import + +ni = safe_import("nidaqmx") +if ni is not None: import nidaqmx.system from nidaqmx.constants import TerminalConfiguration, DigitalWidthUnits from nidaqmx.stream_writers import AnalogMultiChannelWriter -except ImportError: - raise ImportError( - """The nidaqmx package is required for the ScanningMicroscope class, even when only using test data. - To install: - ```pip install nidaqmx``` - Alternatively, specify the genicam dependency when installing openwfs: - ```pip install openwfs[nidaq]``` - """ - ) from ..core import Detector from ..utilities import unitless diff --git a/openwfs/devices/nidaq_gain.py b/openwfs/devices/nidaq_gain.py index 64c7c51..b359029 100644 --- a/openwfs/devices/nidaq_gain.py +++ b/openwfs/devices/nidaq_gain.py @@ -1,9 +1,11 @@ import time import astropy.units as u -import nidaqmx as ni from astropy.units import Quantity -from nidaqmx.constants import LineGrouping + +from . import safe_import + +ni = safe_import("nidaqmx") class Gain: @@ -55,7 +57,9 @@ def check_overload(self): def on_reset(self, value): if value: with ni.Task() as task: - task.do_channels.add_do_chan(self.port_do, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES) + task.do_channels.add_do_chan( + self.port_do, line_grouping=nidaqmx.constants.LineGroupingLineGrouping.CHAN_FOR_ALL_LINES + ) task.write([True]) time.sleep(1) task.write([False]) diff --git a/openwfs/devices/slm/context.py b/openwfs/devices/slm/context.py index 0e33cf7..2f16f2b 100644 --- a/openwfs/devices/slm/context.py +++ b/openwfs/devices/slm/context.py @@ -1,7 +1,9 @@ import threading import weakref -import glfw +from .. import safe_import + +glfw = safe_import("glfw") SLM = "slm.SLM" diff --git a/openwfs/devices/slm/patch.py b/openwfs/devices/slm/patch.py index 66d2c8e..fd15ef8 100644 --- a/openwfs/devices/slm/patch.py +++ b/openwfs/devices/slm/patch.py @@ -5,40 +5,12 @@ from numpy.typing import ArrayLike from .context import Context +from .. import safe_import -try: - import OpenGL.GL as GL - from OpenGL.GL import ( - glGenBuffers, - glBindBuffer, - glBufferData, - glDeleteBuffers, - glEnable, - glBlendFunc, - glBlendEquation, - glDisable, - glUseProgram, - glBindVertexBuffer, - glDrawElements, - glGenFramebuffers, - glBindFramebuffer, - glFramebufferTexture2D, - glCheckFramebufferStatus, - glDeleteFramebuffers, - glEnableVertexAttribArray, - glVertexAttribFormat, - glVertexAttribBinding, - glEnableVertexAttribArray, - glPrimitiveRestartIndex, - glActiveTexture, - glBindTexture, - glGenVertexArrays, - glBindVertexArray, - ) - +GL = safe_import("OpenGL.GL") +if GL is not None: from OpenGL.GL import shaders -except AttributeError: - warnings.warn("OpenGL not found, SLM will not work") + from .geometry import rectangle, Geometry from .shaders import ( default_vertex_shader, @@ -96,23 +68,23 @@ def _draw(self): if not self.enabled: return - glUseProgram(self._program) + GL.glUseProgram(self._program) if self.additive_blend: - glEnable(GL.GL_BLEND) - glBlendFunc(GL.GL_ONE, GL.GL_ONE) # (1 * rgb, 1 * alpha) - glBlendEquation(GL.GL_FUNC_ADD) + GL.glEnable(GL.GL_BLEND) + GL.glBlendFunc(GL.GL_ONE, GL.GL_ONE) # (1 * rgb, 1 * alpha) + GL.glBlendEquation(GL.GL_FUNC_ADD) else: - glDisable(GL.GL_BLEND) + GL.glDisable(GL.GL_BLEND) for idx, texture in enumerate(self._textures): # activate texture as texture unit idx texture._bind(idx) # noqa: ok to use _bind in friend class # perform the actual drawing - glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._indices) - glBindVertexBuffer(0, self._vertices, 0, 16) - glDrawElements(GL.GL_TRIANGLE_STRIP, self._index_count, GL.GL_UNSIGNED_SHORT, None) + GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._indices) + GL.glBindVertexBuffer(0, self._vertices, 0, 16) + GL.glDrawElements(GL.GL_TRIANGLE_STRIP, self._index_count, GL.GL_UNSIGNED_SHORT, None) def set_phases(self, values: ArrayLike, update=True): """ @@ -133,7 +105,7 @@ def update(self): def _delete_buffers(self): with self.context as slm: if slm: - glDeleteBuffers(2, [self._vertices, self._indices]) + GL.glDeleteBuffers(2, [self._vertices, self._indices]) @property def geometry(self): @@ -151,17 +123,17 @@ def geometry(self, value: Geometry): # store the data on the GPU with self.context: self._geometry = value - (self._vertices, self._indices) = glGenBuffers(2) + (self._vertices, self._indices) = GL.glGenBuffers(2) self._index_count = value.indices.size - glBindBuffer(GL.GL_ARRAY_BUFFER, self._vertices) - glBufferData( + GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._vertices) + GL.glBufferData( GL.GL_ARRAY_BUFFER, value.vertices.size * 4, value.vertices, GL.GL_DYNAMIC_DRAW, ) - glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._indices) - glBufferData( + GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._indices) + GL.glBufferData( GL.GL_ELEMENT_ARRAY_BUFFER, value.indices.size * 2, value.indices, @@ -197,20 +169,20 @@ def __init__(self, slm, lookup_table: Optional[Sequence[int]], bit_depth: int): # Create a frame buffer object to render to. The frame buffer holds a texture that is the same size as the # window. All patches are first rendered to this texture. The texture # is then processed as a whole (applying the software lookup table) and displayed on the screen. - self._frame_buffer = glGenFramebuffers(1) + self._frame_buffer = GL.glGenFramebuffers(1) self.set_phases(np.zeros(self.context.slm.shape, dtype=np.float32), update=False) - glBindFramebuffer(GL.GL_FRAMEBUFFER, self._frame_buffer) - glFramebufferTexture2D( + GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self._frame_buffer) + GL.glFramebufferTexture2D( GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D, self._textures[Patch._PHASES_TEXTURE].handle, 0, ) - if glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE: + if GL.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE: raise Exception("Could not construct frame buffer") - glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) + GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) self._bit_depth = bit_depth self._textures.append(Texture(self.context, GL.GL_TEXTURE_1D)) # create texture for lookup table @@ -221,7 +193,7 @@ def __init__(self, slm, lookup_table: Optional[Sequence[int]], bit_depth: int): def __del__(self): with self.context as slm: if slm: - glDeleteFramebuffers(1, [self._frame_buffer]) + GL.glDeleteFramebuffers(1, [self._frame_buffer]) @property def lookup_table(self): @@ -264,15 +236,17 @@ class VertexArray: # Since we have a fixed vertex format, we only need to bind the VertexArray once, and not bother with # updating, binding, or even deleting it def __init__(self): - self._vertex_array = glGenVertexArrays(1) # no need to destroy explicitly, destroyed when window is destroyed - glBindVertexArray(self._vertex_array) - glEnableVertexAttribArray(0) - glEnableVertexAttribArray(1) - glVertexAttribFormat(0, 2, GL.GL_FLOAT, GL.GL_FALSE, 0) # first two float32 are screen coordinates - glVertexAttribFormat(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 8) # second two are texture coordinates - glVertexAttribBinding(0, 0) # use binding index 0 for both attributes - glVertexAttribBinding(1, 0) # the attribute format can now be used with glBindVertexBuffer + self._vertex_array = GL.glGenVertexArrays( + 1 + ) # no need to destroy explicitly, destroyed when window is destroyed + GL.glBindVertexArray(self._vertex_array) + GL.glEnableVertexAttribArray(0) + GL.glEnableVertexAttribArray(1) + GL.glVertexAttribFormat(0, 2, GL.GL_FLOAT, GL.GL_FALSE, 0) # first two float32 are screen coordinates + GL.glVertexAttribFormat(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 8) # second two are texture coordinates + GL.glVertexAttribBinding(0, 0) # use binding index 0 for both attributes + GL.glVertexAttribBinding(1, 0) # the attribute format can now be used with glBindVertexBuffer # enable primitive restart, so that we can draw multiple triangle strips with a single draw call - glEnable(GL.GL_PRIMITIVE_RESTART) - glPrimitiveRestartIndex(0xFFFF) # this is the index we use to separate individual triangle strips + GL.glEnable(GL.GL_PRIMITIVE_RESTART) + GL.glPrimitiveRestartIndex(0xFFFF) # this is the index we use to separate individual triangle strips diff --git a/openwfs/devices/slm/slm.py b/openwfs/devices/slm/slm.py index e7bdb62..4e83f47 100644 --- a/openwfs/devices/slm/slm.py +++ b/openwfs/devices/slm/slm.py @@ -3,31 +3,16 @@ from weakref import WeakSet import astropy.units as u -import glfw import numpy as np from astropy.units import Quantity from numpy.typing import ArrayLike from .context import Context +from .. import safe_import from ...simulation import PhaseToField -try: - import OpenGL.GL as GL - from OpenGL.GL import ( - glViewport, - glClearColor, - glClear, - glGenBuffers, - glReadBuffer, - glReadPixels, - glFinish, - glBindBuffer, - glBufferData, - glBindBufferBase, - glBindFramebuffer, - ) -except AttributeError: - warnings.warn("OpenGL not found, SLM will not work") +GL = safe_import("OpenGL.GL") +glfw = safe_import("glfw") from .patch import FrameBufferPatch, Patch, VertexArray from ...core import PhaseSLM, Actuator, Device, Detector from ...utilities import Transform @@ -234,7 +219,7 @@ def _on_resize(self): # re-use the lookup table if possible, otherwise create a default one ranging from 0 to 2 ** bit_depth-1. old_lut = self._frame_buffer.lookup_table if self._frame_buffer is not None else None self._frame_buffer = FrameBufferPatch(self, old_lut, current_bit_depth) - glViewport(0, 0, self._shape[1], self._shape[0]) + GL.glViewport(0, 0, self._shape[1], self._shape[0]) # tell openGL to wait for the vertical retrace when swapping buffers (it appears need to do this # after creating the frame buffer) glfw.swap_interval(1) @@ -290,8 +275,8 @@ def _create_window(self): glfw.set_window_pos(self._window, self._position[1], self._position[0]) with self._context: - self._globals = glGenBuffers(1) # create buffer for storing globals - glClearColor(0.0, 0.0, 0.0, 1.0) # set clear color to black + self._globals = GL.glGenBuffers(1) # create buffer for storing globals + GL.glClearColor(0.0, 0.0, 0.0, 1.0) # set clear color to black self._on_resize() @property @@ -414,13 +399,15 @@ def update(self): """ with self._context: # first draw all patches into the frame buffer - glBindFramebuffer(GL.GL_FRAMEBUFFER, self._frame_buffer._frame_buffer) # noqa - ok to access 'friend class' - glClear(GL.GL_COLOR_BUFFER_BIT) + GL.glBindFramebuffer( + GL.GL_FRAMEBUFFER, self._frame_buffer._frame_buffer + ) # noqa - ok to access 'friend class' + GL.glClear(GL.GL_COLOR_BUFFER_BIT) for patch in self.patches: patch._draw() # noqa - ok to access 'friend class' # then draw the frame buffer to the screen - glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) + GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) self._frame_buffer._draw() # noqa - ok to access 'friend class' glfw.poll_events() # process window messages @@ -439,7 +426,7 @@ def update(self): # wait for buffer swap to complete (this should be directly after a vsync, so returning from this # function _should_ be synced with the vsync) - glFinish() + GL.glFinish() # call _start again to update the _end_time_ns property, # since some time has passed waiting for the vsync @@ -531,9 +518,9 @@ def transform(self, value: Transform): padded = transform.opencl_matrix() with self._context: - glBindBuffer(GL.GL_UNIFORM_BUFFER, self._globals) - glBufferData(GL.GL_UNIFORM_BUFFER, padded.size * 4, padded, GL.GL_STATIC_DRAW) - glBindBufferBase(GL.GL_UNIFORM_BUFFER, 1, self._globals) # connect buffer to binding point 1 + GL.glBindBuffer(GL.GL_UNIFORM_BUFFER, self._globals) + GL.glBufferData(GL.GL_UNIFORM_BUFFER, padded.size * 4, padded, GL.GL_STATIC_DRAW) + GL.glBindBufferBase(GL.GL_UNIFORM_BUFFER, 1, self._globals) # connect buffer to binding point 1 @property def lookup_table(self) -> Sequence[int]: @@ -634,10 +621,10 @@ def data_shape(self): def _fetch(self, *args, **kwargs) -> np.ndarray: with self._context: - glReadBuffer(GL.GL_FRONT) + GL.glReadBuffer(GL.GL_FRONT) shape = self.data_shape data = np.empty(shape, dtype="uint8") - glReadPixels(0, 0, shape[1], shape[0], GL.GL_RED, GL.GL_UNSIGNED_BYTE, data) + GL.glReadPixels(0, 0, shape[1], shape[0], GL.GL_RED, GL.GL_UNSIGNED_BYTE, data) # flip data upside down, because the OpenGL convention is to have the origin at the bottom left, # but we want it at the top left (like in numpy) return data[::-1, :] diff --git a/openwfs/devices/slm/texture.py b/openwfs/devices/slm/texture.py index a05b674..f07e699 100644 --- a/openwfs/devices/slm/texture.py +++ b/openwfs/devices/slm/texture.py @@ -3,31 +3,16 @@ import numpy as np from .context import Context +from .. import safe_import -try: - import OpenGL.GL as GL - from OpenGL.GL import ( - glGenTextures, - glBindTexture, - glTexImage2D, - glTexSubImage2D, - glTexImage1D, - glTexSubImage1D, - glTexParameteri, - glActiveTexture, - glDeleteTextures, - glGetTextureImage, - glPixelStorei, - ) -except AttributeError: - warnings.warn("OpenGL not found, SLM will not work"), +GL = safe_import("OpenGL.GL") class Texture: - def __init__(self, slm, texture_type=GL.GL_TEXTURE_2D): + def __init__(self, slm, texture_type=None): self.context = Context(slm) - self.handle = glGenTextures(1) - self.type = texture_type + self.handle = GL.glGenTextures(1) + self.type = texture_type if texture_type is not None else GL.GL_TEXTURE_2D self.synchronized = False # self.data is not yet synchronized with texture in GPU memory self._data_shape = None # current size of the texture, to see if we need to make a new texture or # overwrite the exiting one @@ -36,21 +21,21 @@ def __init__(self, slm, texture_type=GL.GL_TEXTURE_2D): self.set_data(0) # set wrapping and interpolation options - glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT) - glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT) - glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_R, GL.GL_REPEAT) - glTexParameteri(self.type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST) - glTexParameteri(self.type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST) + GL.glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT) + GL.glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT) + GL.glTexParameteri(self.type, GL.GL_TEXTURE_WRAP_R, GL.GL_REPEAT) + GL.glTexParameteri(self.type, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST) + GL.glTexParameteri(self.type, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST) def __del__(self): with self.context as slm: if slm: - glDeleteTextures(1, [self.handle]) + GL.glDeleteTextures(1, [self.handle]) def _bind(self, idx): """Bind texture to texture unit idx. Assumes that the OpenGL context is already active.""" - glActiveTexture(GL.GL_TEXTURE0 + idx) - glBindTexture(self.type, self.handle) + GL.glActiveTexture(GL.GL_TEXTURE0 + idx) + GL.glBindTexture(self.type, self.handle) def set_data(self, value): """Set texture data. @@ -61,8 +46,8 @@ def set_data(self, value): value = np.array(value, dtype=np.float32, order="C", copy=False) with self.context: - glBindTexture(self.type, self.handle) - glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 4) # alignment is at least four bytes since we use float32 + GL.glBindTexture(self.type, self.handle) + GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 4) # alignment is at least four bytes since we use float32 (internal_format, data_format, data_type) = ( GL.GL_R32F, GL.GL_RED, @@ -77,7 +62,7 @@ def set_data(self, value): raise ValueError("Data should be a 1-d array or a scalar") if value.shape != self._data_shape: # create a new texture - glTexImage1D( + GL.glTexImage1D( GL.GL_TEXTURE_1D, 0, internal_format, @@ -90,7 +75,7 @@ def set_data(self, value): self._data_shape = value.shape else: # overwrite existing texture - glTexSubImage1D( + GL.glTexSubImage1D( GL.GL_TEXTURE_1D, 0, 0, @@ -106,7 +91,7 @@ def set_data(self, value): elif value.ndim != 2: raise ValueError("Data should be a 2-D array or a scalar") if value.shape != self._data_shape: - glTexImage2D( + GL.glTexImage2D( GL.GL_TEXTURE_2D, 0, internal_format, @@ -119,7 +104,7 @@ def set_data(self, value): ) self._data_shape = value.shape else: - glTexSubImage2D( + GL.glTexSubImage2D( GL.GL_TEXTURE_2D, 0, 0, @@ -136,5 +121,5 @@ def set_data(self, value): def get_data(self): with self.context: data = np.empty(self._data_shape, dtype="float32") - glGetTextureImage(self.handle, 0, GL.GL_RED, GL.GL_FLOAT, data.size * 4, data) + GL.glGetTextureImage(self.handle, 0, GL.GL_RED, GL.GL_FLOAT, data.size * 4, data) return data diff --git a/pyproject.toml b/pyproject.toml index 6f1304e..1f5579c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,22 @@ harvesters = { version = "^1.4.2", optional = true } PyOpenGL = { version = "^3.1.7", optional = true } glfw = { version = "^2.5.9", optional = true } +# optional dependencies for development and testing +scikit-image = { version = ">=0.21.0", optional = true } +pytest = { version = ">=7.0.0", optional = true } +black = { version = ">=24.0.0", optional = true } # code formatter +poetry = { version = ">=1.2.0", optional = true } # package manager + +# optional dependenies for documentation building +sphinx = { verstion = ">=4.1.2", optional = true } +sphinx_mdinclude = { verstion = ">=0.5.0", optional = true } +sphinx-rtd-theme = { verstion = ">=2.0.0", optional = true } +sphinx-autodoc-typehints = { verstion = ">=2.2.0", optional = true } +sphinxcontrib-bibtex = { verstion = ">=2.6.0", optional = true } +sphinx-markdown-builder = { verstion = ">=0.6.6", optional = true } +sphinx-gallery = { verstion = ">=0.15.0", optional = true } + + [tool.poetry.extras] # optional dependencies for hardware components # these are not required for the core functionality of the library @@ -42,34 +58,11 @@ glfw = { version = "^2.5.9", optional = true } nidaq = ["nidaqmx"] genicam = ["harvesters"] opengl = ["PyOpenGL", "glfw"] +dev = ["scikit-image", "pytest", "black", "poetry"] +doc = ["sphinx", "sphinx_mdinclude", "sphinx-rtd-theme", "sphinx-autodoc-typehints", "sphinxcontrib-bibtex", "sphinx-markdown-builder", "sphinx-gallery"] + [tool.black] line-length = 120 -[tool.poetry.group.dev] -optional = true - -[tool.poetry.group.dev.dependencies] -# development dependencies, used for testing only, not needed for normal use -# to install, use pip install openwfs[dev] -scikit-image = ">=0.21.0" -pytest = ">=7.0.0" -nidaqmx = "^1.0.1" # we can test without the hardware, but still need the package -black = ">=24.0.0" # code formatter -poetry = ">=1.2.0" # package manager - -[tool.poetry.group.docs] -optional = true - -[tool.poetry.group.docs.dependencies] -# documentation dependencies, used for building the sphinx documentation only -# to install, use pip install openwfs[docs] -sphinx = ">=4.1.2" -sphinx_mdinclude = ">=0.5.0" -sphinx-rtd-theme = ">=2.0.0" -sphinx-autodoc-typehints = ">=2.2.0" -sphinxcontrib-bibtex = ">=2.6.0" -sphinx-markdown-builder = ">=0.6.6" -sphinx-gallery = ">=0.15.0" - From dace931778f953cef4cf25173af503e5b48f88c8 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Wed, 9 Oct 2024 12:16:30 +0200 Subject: [PATCH 30/37] again separated install groups (for development) and extras (for use) --- .readthedocs.yaml | 2 +- pyproject.toml | 52 +++++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index bb07e7d..e4f96f8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,7 +11,7 @@ build: - pip install poetry - pip install poetry-plugin-export - poetry config warnings.export false - - poetry export -f requirements.txt -o requirements.txt --with docs --extras "nidaq" --extras "genicam" --extras "opengl" + - poetry export -f requirements.txt -o requirements.txt --with docs --extras "all" - cat requirements.txt python: diff --git a/pyproject.toml b/pyproject.toml index 1f5579c..bd330c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,35 +34,47 @@ harvesters = { version = "^1.4.2", optional = true } PyOpenGL = { version = "^3.1.7", optional = true } glfw = { version = "^2.5.9", optional = true } -# optional dependencies for development and testing -scikit-image = { version = ">=0.21.0", optional = true } -pytest = { version = ">=7.0.0", optional = true } -black = { version = ">=24.0.0", optional = true } # code formatter -poetry = { version = ">=1.2.0", optional = true } # package manager - -# optional dependenies for documentation building -sphinx = { verstion = ">=4.1.2", optional = true } -sphinx_mdinclude = { verstion = ">=0.5.0", optional = true } -sphinx-rtd-theme = { verstion = ">=2.0.0", optional = true } -sphinx-autodoc-typehints = { verstion = ">=2.2.0", optional = true } -sphinxcontrib-bibtex = { verstion = ">=2.6.0", optional = true } -sphinx-markdown-builder = { verstion = ">=0.6.6", optional = true } -sphinx-gallery = { verstion = ">=0.15.0", optional = true } - - [tool.poetry.extras] # optional dependencies for hardware components # these are not required for the core functionality of the library -# to install, use pip install openwfs[nidaq, genicam] -# or poetry install --extras "nidaq" --extras "genicam" +# to install, use pip install openwfs[nidaq, genicam, opengl] +# or poetry install --extras "nidaq" --extras "genicam" -- extras "opengl" nidaq = ["nidaqmx"] genicam = ["harvesters"] opengl = ["PyOpenGL", "glfw"] -dev = ["scikit-image", "pytest", "black", "poetry"] -doc = ["sphinx", "sphinx_mdinclude", "sphinx-rtd-theme", "sphinx-autodoc-typehints", "sphinxcontrib-bibtex", "sphinx-markdown-builder", "sphinx-gallery"] +all = ["dev", "doc", "harvesters", "opengl", "nidaq"] [tool.black] line-length = 120 +# for the development process, we also want the docs and dev dependencies +# these can only be installed through poetry using --with dev and --with docs + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +# development dependencies, used for testing only, not needed for normal use +# to install, use pip install openwfs[dev] +scikit-image = ">=0.21.0" +pytest = ">=7.0.0" +nidaqmx = "^1.0.1" # we can test without the hardware, but still need the package +black = ">=24.0.0" # code formatter +poetry = ">=1.2.0" # package manager + +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] +# documentation dependencies, used for building the sphinx documentation only +# to install, use pip install openwfs[docs] +sphinx = ">=4.1.2" +sphinx_mdinclude = ">=0.5.0" +sphinx-rtd-theme = ">=2.0.0" +sphinx-autodoc-typehints = ">=2.2.0" +sphinxcontrib-bibtex = ">=2.6.0" +sphinx-markdown-builder = ">=0.6.6" +sphinx-gallery = ">=0.15.0" + From 7450f42a47274d05f1e79278facca8258f8dbb35 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Wed, 9 Oct 2024 15:35:53 +0200 Subject: [PATCH 31/37] unified progress bar argument --- docs/source/development.rst | 9 ++++----- openwfs/algorithms/dual_reference.py | 26 +++++++++++--------------- openwfs/algorithms/genetic.py | 10 ++++------ openwfs/algorithms/ssa.py | 6 ++++-- openwfs/algorithms/utilities.py | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 28 deletions(-) diff --git a/docs/source/development.rst b/docs/source/development.rst index 6474a6b..19207ca 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -78,11 +78,10 @@ To implement an actuator, the user should subclass the :class:`~Actuator` base c Implementing new algorithms -------------------------------------------------- -Algorithms in OpenWFS do not necessarily need to be implemented as classes. However, the included algorithms are wrapped in a class so that the parameters of the algorithm can be viewed and changed from the Micro-Manager GUI. Moreover, wrapping are implemented as classes with an ``execute()`` method. -that inherit from the :class:`~.Algorithm` base class. -To implement a new algorithm, the currently existing algorithms can be consulted for a few examples. -Essentially, the algorithm needs to have an execute method, which needs to produce a WFSResult. With OpenWFS, all hardware interactions are abstracted away in the calls to ``slm.set_phases`` and ``feedback.trigger``. During the execution, different modes are measured, and a transmission matrix is calculated or approached. For most of our algorithms, the same algorithm can be used to analyze a phase stepping experiment. In order to show the versatility of this platform, we implemented the genetic algorithm described in :cite:`Piestun2012` and more recently adapted for a GUI in :cite:`Anderson2024`. - +The algorithms that are included in OpenWFS are wrapped in classes with two common attribute: ``slm``, ``feedback``, which respectively hold a :class:`~.PhaseSLM` object to control the SLM and a :class:`~Detector` object that returns the feedback signals used in the optimization. For algorithms that support optimizing multiple targets simulaneously, the ``feedback`` detector may return an array of values. +In addition, all algorithms have an ``execute()`` method that executes the algoritm and returns the measured transmission matrix, along with statistics about the measurements in a :class:`WFSResults` structure (see :numref:`section-troubleshooting). +When implementing a new algorithm, it is perfectly acceptable to deviate from this convention. However, if an algorithm follows the convention described above, it can directly be wrapped in a `WFSController` so that it can be used in Micro-Manager (see :numref:`section-micromanager`) +As can be seen in the example in :numref:`hello-wfs`, OpenWFS abstracts all hardware interactions in the calls to ``slm.set_phases`` and ``feedback.trigger``. diff --git a/openwfs/algorithms/dual_reference.py b/openwfs/algorithms/dual_reference.py index 05c21ca..af5916b 100644 --- a/openwfs/algorithms/dual_reference.py +++ b/openwfs/algorithms/dual_reference.py @@ -3,7 +3,7 @@ import numpy as np from numpy import ndarray as nd -from .utilities import analyze_phase_stepping, WFSResult +from .utilities import analyze_phase_stepping, WFSResult, DummyProgressBar from ..core import Detector, PhaseSLM @@ -173,7 +173,7 @@ def gram(self) -> tuple[nd, nd]: """ return tuple(self._gram) - def execute(self, *, capture_intermediate_results: bool = False, progress_bar=None) -> WFSResult: + def execute(self, *, capture_intermediate_results: bool = False, progress_bar=DummyProgressBar()) -> WFSResult: """ Executes the blind focusing dual reference algorithm and compute the SLM transmission matrix. capture_intermediate_results: When True, measures the feedback from the optimized wavefront after each iteration. @@ -196,12 +196,10 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No intermediate_results = np.zeros(self.iterations) # List to store feedback from full patterns # Prepare progress bar - if progress_bar: - num_measurements = ( - np.ceil(self.iterations / 2) * self.phase_patterns[0].shape[2] - + np.floor(self.iterations / 2) * self.phase_patterns[1].shape[2] - ) - progress_bar.total = num_measurements + progress_bar.total = ( + np.ceil(self.iterations / 2) * self.phase_patterns[0].shape[2] + + np.floor(self.iterations / 2) * self.phase_patterns[1].shape[2] + ) # Switch the phase sets back and forth multiple times for it in range(self.iterations): @@ -221,7 +219,7 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No if self.optimized_reference: # use the best estimate so far to construct an optimized reference # TODO: see if the squeeze can be removed - t_this_side = self.compute_t_set(results_all[it].t, side).squeeze() + t_this_side = self._compute_t_set(results_all[it].t, side).squeeze() ref_phases[self.masks[side]] = -np.angle(t_this_side[self.masks[side]]) # Try full pattern @@ -246,7 +244,7 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No relative = t_side_0[..., self._zero_indices[0]] + np.conjugate(t_side_1[..., self._zero_indices[1]]) factor = np.expand_dims(relative / np.abs(relative), -1) - t_full = self.compute_t_set(t_side_0, 0) + self.compute_t_set(factor * t_side_1, 1) + t_full = self._compute_t_set(t_side_0, 0) + self._compute_t_set(factor * t_side_1, 1) # Compute average fidelity factors # subtract 1 from n, because both sets (usually) contain a flat wavefront, @@ -260,7 +258,7 @@ def execute(self, *, capture_intermediate_results: bool = False, progress_bar=No result.intermediate_results = intermediate_results return result - def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, progress_bar=None) -> WFSResult: + def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, progress_bar) -> WFSResult: """ Conducts experiments on one part of the SLM. @@ -269,7 +267,7 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, ``shape = mode_count × height × width`` ref_phases: 2D array containing the reference phase pattern. mod_mask: 2D array containing a boolean mask, where True indicates the modulated part of the SLM. - progress_bar: Optional progress bar object. Following the convention for tqdm progress bars, + progress_bar: Progress bar object. Following the convention for tqdm progress bars, this object should have a `total` attribute and an `update()` function. Returns: @@ -285,14 +283,12 @@ def _single_side_experiment(self, mod_phases: nd, ref_phases: nd, mod_mask: nd, phases[mod_mask] = modulated[mod_mask] + phi self.slm.set_phases(phases) self.feedback.trigger(out=measurements[m, p, ...]) - - if progress_bar is not None: progress_bar.update() self.feedback.wait() return analyze_phase_stepping(measurements, axis=1) - def compute_t_set(self, t, side) -> nd: + def _compute_t_set(self, t, side) -> nd: """ Compute the transmission matrix in SLM space from transmission matrix in input mode space. diff --git a/openwfs/algorithms/genetic.py b/openwfs/algorithms/genetic.py index 10426bb..25a750c 100644 --- a/openwfs/algorithms/genetic.py +++ b/openwfs/algorithms/genetic.py @@ -2,7 +2,7 @@ import numpy as np -from .utilities import WFSResult +from .utilities import WFSResult, DummyProgressBar from ..core import Detector, PhaseSLM @@ -74,7 +74,7 @@ def __init__( def _generate_random_phases(self, shape): return self.generator.random(size=shape, dtype=np.float32) * (2 * np.pi) - def execute(self, *, progress_bar=None) -> WFSResult: + def execute(self, *, progress_bar=DummyProgressBar()) -> WFSResult: """Executes the algorithm. Args: progress_bar: Optional tqdm-like progress bar for displaying progress @@ -84,8 +84,7 @@ def execute(self, *, progress_bar=None) -> WFSResult: population = self._generate_random_phases((self.population_size, *self.shape)) # initialize the progress bar if available - if progress_bar is not None: - progress_bar.total = self.generations * self.population_size + progress_bar.total = self.generations * self.population_size for i in itertools.count(): # Try all phase patterns @@ -93,8 +92,7 @@ def execute(self, *, progress_bar=None) -> WFSResult: for p in range(self.population_size): self.slm.set_phases(population[p]) self.feedback.trigger(out=measurements[p, ...]) - if progress_bar is not None: - progress_bar.update(1) + progress_bar.update() self.feedback.wait() diff --git a/openwfs/algorithms/ssa.py b/openwfs/algorithms/ssa.py index c2cebf0..aabb08c 100644 --- a/openwfs/algorithms/ssa.py +++ b/openwfs/algorithms/ssa.py @@ -1,6 +1,6 @@ import numpy as np -from .utilities import analyze_phase_stepping, WFSResult +from .utilities import analyze_phase_stepping, WFSResult, DummyProgressBar from ..core import Detector, PhaseSLM @@ -41,7 +41,7 @@ def __init__( self.slm = slm self.feedback = feedback - def execute(self) -> WFSResult: + def execute(self, progress_bar=DummyProgressBar()) -> WFSResult: """Executes the StepwiseSequential algorithm, computing the transmission matrix of the sample Returns: @@ -49,6 +49,7 @@ def execute(self) -> WFSResult: """ phase_pattern = np.zeros((self.n_y, self.n_x), "float32") measurements = np.zeros((self.n_y, self.n_x, self.phase_steps, *self.feedback.data_shape)) + progress_bar.count = self.n_x * self.n_y for y in range(self.n_y): for x in range(self.n_x): @@ -57,6 +58,7 @@ def execute(self) -> WFSResult: self.slm.set_phases(phase_pattern) self.feedback.trigger(out=measurements[y, x, p, ...]) phase_pattern[y, x] = 0 + progress_bar.update() self.feedback.wait() return analyze_phase_stepping(measurements, axis=2) diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 5908135..43a86a9 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -399,3 +399,18 @@ def test_wavefront(self, value): self.wavefront = WFSController.State.OPTIMIZED feedback_shaped = self.feedback.read().sum() self._feedback_ratio = float(feedback_shaped / feedback_flat) + + +class DummyProgressBar: + """Placeholder for a progress bar object. + + Some functions take an optional tdqm-style progress bar as input. + This class serves as a placeholder iif no progress bar is given. + It does nothing. + """ + + def __init__(self): + self.count = 0 + + def update(self): + pass From 3cff35801799bbb4dfca0f6f86219e8669a5ad58 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 12:56:35 +0200 Subject: [PATCH 32/37] added and fixed Micro-Manager samples --- docs/source/core.rst | 8 +- examples/micro_manager_microscope.cfg | 10 +- examples/micro_manager_microscope.py | 27 ++++- .../micro_manager_scanning_microscope.cfg | 6 +- examples/micro_wfs_demo.py | 112 ++++++++++++++++++ examples/sample_microscope.py | 1 + openwfs/algorithms/utilities.py | 2 +- openwfs/processors/processors.py | 2 +- openwfs/simulation/mockdevices.py | 29 +++-- tests/test_simulation.py | 6 +- 10 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 examples/micro_wfs_demo.py diff --git a/docs/source/core.rst b/docs/source/core.rst index be45186..84488f1 100644 --- a/docs/source/core.rst +++ b/docs/source/core.rst @@ -147,10 +147,10 @@ OpenWFS consistently uses ``astropy.units`` :cite:`astropy` for quantities with .. code-block:: python import astropy.units as u - c = Camera() - c.shutter_time = 10 * u.ms - c.shutter_time = 0.01 * u.s # equivalent to the previous line - c.shutter_time = 10 # raises an error, since the unit is missing + c = Camera(...) + c.exposure = 10 * u.ms + c.exposure = 0.01 * u.s # equivalent to the previous line + c.exposure = 10 # raises an error, since the unit is missing In addition, OpenWFS allows attaching pixel-size metadata to data arrays using the functions :func:`~.set_pixel_size()`. Pixel sizes can represent a physical length (e.g. as in the size pixels on an image sensor), or other units such as time (e.g. as the sampling period in a time series). OpenWFS fully supports anisotropic pixels, where the pixel sizes in the x and y directions are different. diff --git a/examples/micro_manager_microscope.cfg b/examples/micro_manager_microscope.cfg index 32fbe4e..fed5471 100644 --- a/examples/micro_manager_microscope.cfg +++ b/examples/micro_manager_microscope.cfg @@ -5,8 +5,8 @@ Property,Core,Initialize,0 # Devices Device,PyHub,PyDevice,PyHub -Device,Camera : camera,PyDevice,Camera:camera -Device,XYStage : stage,PyDevice,XYStage:stage +Device,camera,PyDevice,Camera:camera +Device,stage,PyDevice,XYStage:stage # Pre-init settings for devices Property,PyHub,PythonEnvironment,(auto) @@ -15,8 +15,8 @@ Property,PyHub,ScriptPath,./micro_manager_microscope.py # Pre-init settings for COM ports # Hub (parent) references -Parent,Camera : camera,PyHub -Parent,XYStage : stage,PyHub +Parent,camera,PyHub +Parent,stage,PyHub # Initialize Property,Core,Initialize,1 @@ -26,7 +26,7 @@ Property,Core,Initialize,1 # Focus directions # Roles -Property,Core,Camera,Camera : camera +Property,Core,Camera,camera Property,Core,AutoShutter,1 # Camera-synchronized devices diff --git a/examples/micro_manager_microscope.py b/examples/micro_manager_microscope.py index aecc93c..231e360 100644 --- a/examples/micro_manager_microscope.py +++ b/examples/micro_manager_microscope.py @@ -10,11 +10,12 @@ import astropy.units as u import numpy as np +from openwfs.processors import SingleRoi +from openwfs.simulation import Microscope, StaticSource, Camera, SLM +from openwfs.utilities.patterns import gaussian -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 +specimen_resolution = (512, 512) # height × width in pixels of the specimen image +specimen_pixel_size = 120 * u.nm # resolution (pixel size) of the specimen image magnification = 40 # magnification from object plane to camera. numerical_aperture = 0.85 # numerical aperture of the microscope objective wavelength = 532.8 * u.nm # wavelength of the light, for computing diffraction. @@ -22,10 +23,15 @@ camera_pixel_size = 6.45 * u.um # Size of the pixels on the camera # Create a random noise image with a few bright spots +image_data = np.maximum(np.random.randint(-10000, 100, specimen_resolution, dtype=np.int16), 0) +image_data[256, 256] = 100 src = StaticSource( - data=np.maximum(np.random.randint(-10000, 100, specimen_resolution, dtype=np.int16), 0), + data=image_data, pixel_size=specimen_pixel_size, ) +aberrations = StaticSource(extent=2 * numerical_aperture) + +slm = SLM(shape=(100, 100), field_amplitude=gaussian((100, 100), waist=1.0)) # Create a microscope with the given parameters mic = Microscope( @@ -33,16 +39,25 @@ magnification=magnification, numerical_aperture=numerical_aperture, wavelength=wavelength, + aberrations=aberrations, + incident_field=slm.field, ) # simulate shot noise in an 8-bit camera with auto-exposure: cam = Camera( mic, + analog_max=None, shot_noise=True, digital_max=255, data_shape=camera_resolution, pixel_size=camera_pixel_size, ) +feedback = SingleRoi(source=cam, pos=(256, 256), mask_type="gaussian", radius=3.0, waist=1.0) + +alg = WFSController( + FourierDualReference, feedback=feedback, slm=sim.slm, slm_shape=slm_shape, k_radius=3.5, phase_steps=5 +) + # construct dictionary of objects to expose to Micro-Manager -devices = {"camera": cam, "stage": mic.xy_stage} +devices = {"camera": cam, "stage": mic.xy_stage, "algorithm": alg} diff --git a/examples/micro_manager_scanning_microscope.cfg b/examples/micro_manager_scanning_microscope.cfg index ca821e5..f9d9c1e 100644 --- a/examples/micro_manager_scanning_microscope.cfg +++ b/examples/micro_manager_scanning_microscope.cfg @@ -5,7 +5,7 @@ Property,Core,Initialize,0 # Devices Device,PyHub,PyDevice,PyHub -Device,Camera : microscope,PyDevice,Camera:microscope +Device,microscope,PyDevice,Camera:microscope # Pre-init settings for devices Property,PyHub,PythonEnvironment,(auto) @@ -14,7 +14,7 @@ Property,PyHub,ScriptPath,./micro_manager_scanning_microscope.py # Pre-init settings for COM ports # Hub (parent) references -Parent,Camera : microscope,PyHub +Parent,microscope,PyHub # Initialize Property,Core,Initialize,1 @@ -24,7 +24,7 @@ Property,Core,Initialize,1 # Focus directions # Roles -Property,Core,Camera,Camera : microscope +Property,Core,Camera,microscope Property,Core,AutoShutter,1 # Camera-synchronized devices diff --git a/examples/micro_wfs_demo.py b/examples/micro_wfs_demo.py new file mode 100644 index 0000000..3dc4f47 --- /dev/null +++ b/examples/micro_wfs_demo.py @@ -0,0 +1,112 @@ +""" Micro-Manager simulated microscope +====================================================================== +This script simulates a microscope with a random noise image as a mock specimen. +The numerical aperture, stage position, and other parameters can be modified through the Micro-Manager GUI. +To use this script as a device in Micro-Manager, make sure you have the PyDevice adapter installed and +select this script in the hardware configuration wizard for the PyDevice component. + +See the 'Sample Microscope' example for a microscope simulation that runs from Python directly. +""" + +import astropy.units as u +import numpy as np +import skimage.data +from openwfs.algorithms import FourierDualReference +from openwfs.algorithms.utilities import WFSController +from openwfs.processors import SingleRoi +from openwfs.simulation import Microscope, StaticSource, Camera, SLM +from openwfs.utilities.patterns import gaussian + +specimen_resolution = (512, 512) # height × width in pixels of the specimen image +specimen_pixel_size = 60 * u.nm # resolution (pixel size) of the specimen image +magnification = 40 # magnification from object plane to camera. +numerical_aperture = 0.85 # numerical aperture of the microscope objective +wavelength = 532.8 * u.nm # wavelength of the light, for computing diffraction. +camera_resolution = (256, 256) # number of pixels on the camera +camera_pixel_size = 6.45 * u.um # Size of the pixels on the camera +slm_shape = (100, 100) + +# Create a random noise image with a few bright spots, one in the center +image_data = np.maximum(np.random.randint(-10000, 100, specimen_resolution, dtype=np.int16), 0) +image_data[256, 256] = 100 +src = StaticSource( + data=image_data, + pixel_size=specimen_pixel_size, +) + +# create an aberration pattern +aberrations = StaticSource(data=skimage.data.camera() * (np.pi / 128), extent=2 * numerical_aperture) + +# simulate a spatial light modulator illuminated by a gaussian beam +slm = SLM(slm_shape) # , field_amplitude=gaussian((100, 100), waist=1.0)) + +# Create a microscope +mic = Microscope( + source=src, + data_shape=specimen_resolution, + magnification=magnification, + numerical_aperture=numerical_aperture, + wavelength=wavelength, + aberrations=aberrations, + incident_field=slm.field, +) + +# simulate shot noise in an 8-bit camera: +saturation = mic.read().mean() * 20 +cam = Camera( + mic, + analog_max=saturation, + shot_noise=True, + digital_max=0xFFFF, +) + +# integrate the signal in a gaussian-weighted region of interest +feedback = SingleRoi(source=cam, pos=(256, 256), mask_type="gaussian", radius=3.0, waist=1.0) + +# the WFS algorithm +alg = WFSController(FourierDualReference, feedback=feedback, slm=slm, slm_shape=slm_shape, k_radius=3.5, phase_steps=5) + +# construct dictionary of objects to expose to Micro-Manager +devices = {"camera": cam, "stage": mic.xy_stage, "algorithm": alg} + +if __name__ == "__main__": + # run the algorithm + # NOTE: the _psf field will later be replaced by a .psf field that returns a camera + import matplotlib.pyplot as plt + + before = mic.read() + before_psf = mic._psf + plt.subplot(2, 3, 1) + plt.imshow(before) + plt.colorbar() + plt.title("Image - no WFS") + + plt.subplot(2, 3, 2) + plt.imshow(before_psf) + plt.colorbar() + plt.title("PSF - no WFS") + + plt.subplot(2, 3, 3) + plt.title("Aberrations") + plt.imshow(aberrations.read()) + plt.colorbar() + + alg.wavefront = WFSController.State.OPTIMIZED + after = mic.read() + after_psf = mic._psf + + plt.subplot(2, 3, 4) + plt.imshow(after) + plt.title("Image - WFS") + plt.colorbar() + + plt.subplot(2, 3, 5) + plt.imshow(after_psf) + plt.colorbar() + plt.title("PSF - WFS") + + plt.subplot(2, 3, 6) + plt.imshow(-slm.phases.read()) + plt.colorbar() + plt.title("Corrections × -1") + plt.show(block=True) diff --git a/examples/sample_microscope.py b/examples/sample_microscope.py index 074e564..b080b0d 100644 --- a/examples/sample_microscope.py +++ b/examples/sample_microscope.py @@ -38,6 +38,7 @@ # simulate shot noise in an 8-bit camera with auto-exposure: cam = Camera( mic, + analog_max=None, shot_noise=True, digital_max=255, data_shape=camera_resolution, diff --git a/openwfs/algorithms/utilities.py b/openwfs/algorithms/utilities.py index 43a86a9..84c8c1b 100644 --- a/openwfs/algorithms/utilities.py +++ b/openwfs/algorithms/utilities.py @@ -355,7 +355,7 @@ def estimated_optimized_intensity(self) -> float: Returns: float: estimated optimized intensity. """ - return self._estimated_optimized_intensity if self._result is not None else 0.0 + return self._result.estimated_optimized_intensity.mean() if self._result is not None else 0.0 @property def snr(self) -> float: diff --git a/openwfs/processors/processors.py b/openwfs/processors/processors.py index 217cf34..d8ca158 100644 --- a/openwfs/processors/processors.py +++ b/openwfs/processors/processors.py @@ -221,7 +221,7 @@ def __init__( pos (int, int): y,x coordinates of the center of the ROI, measured in pixels from the top-left corner. when omitted, the default value of source.data_shape // 2 is used. note: non-integer positions for the ROI are currently not supported. - radius (float): Radius of the ROI. Default is 0.1. + radius (float): Radius of the ROI in pixels. Default is 0.1. mask_type: Type of the mask. Options are 'disk', 'gaussian', or 'square'. Default is 'disk'. waist (float): Defines the width of the Gaussian distribution. Default is 0.5. """ diff --git a/openwfs/simulation/mockdevices.py b/openwfs/simulation/mockdevices.py index d31d634..8fbe040 100644 --- a/openwfs/simulation/mockdevices.py +++ b/openwfs/simulation/mockdevices.py @@ -133,7 +133,7 @@ class ADCProcessor(Processor): def __init__( self, source: Detector, - analog_max: float = 0.0, + analog_max: Optional[float], digital_max: int = 0xFFFF, shot_noise: bool = False, gaussian_noise_std: float = 0.0, @@ -144,20 +144,20 @@ def __init__( Initializes the ADCProcessor class, which mimics an analog-digital converter. Args: - source (Detector): The source detector providing analog data. - analog_max (float): The maximum analog value that can be handled by the ADC. - If set to 0.0, each measurement will be automatically scaled so that the maximum + source: The source detector providing analog data. + analog_max: The maximum analog value that can be handled by the ADC. + If set to None, each measurement will be automatically scaled so that the maximum value in the data set returned by `source` is converted to `digital_max`. Note that this means that the values from two different measurements cannot be compared quantitatively. - digital_max (int): + digital_max: The maximum digital value that the ADC can output, default is unsigned 16-bit maximum. - shot_noise (bool): + shot_noise: Flag to determine if Poisson noise should be applied instead of rounding. Useful for realistically simulating detectors. - gaussian_noise_std (float): + gaussian_noise_std: If >0, add gaussian noise with std of this value to the data. """ super().__init__(source, multi_threaded=multi_threaded) @@ -174,7 +174,7 @@ def __init__( def _fetch(self, data) -> np.ndarray: # noqa """Clips the data to the range of the ADC, and digitizes the values.""" - if self.analog_max == 0.0: # auto scaling + if self.analog_max is None: # auto scaling max_value = np.max(data) if max_value > 0.0: data = data * (self.digital_max / max_value) # auto-scale to maximum value @@ -200,10 +200,13 @@ def analog_max(self) -> Optional[float]: return self._analog_max @analog_max.setter - def analog_max(self, value): + def analog_max(self, value: Optional[float]): + if value is None: + self._analog_max = None + return if value < 0.0: raise ValueError("analog_max cannot be negative") - self._analog_max = value + self._analog_max = float(value) @property def digital_max(self) -> int: @@ -214,11 +217,11 @@ def digital_max(self) -> int: return self._digital_max @property - def conversion_factor(self) -> float: + def conversion_factor(self) -> Optional[float]: """Conversion factor between analog and digital values. - If analog_max is set to 0.0, each frame is auto-scaled, and this function returns 0. + If analog_max is set to None, each frame is auto-scaled, and this function returns None. """ - return self.digital_max / self.analog_max if self.analog_max > 0.0 else 0.0 + return self.digital_max / self.analog_max if self.analog_max is not None else None @digital_max.setter def digital_max(self, value): diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 2760cec..bd7001f 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -20,7 +20,7 @@ def test_mock_camera_and_single_roi(): """ img = np.zeros((1000, 1000), dtype=np.int16) img[200, 300] = 39.39 # some random float - src = Camera(StaticSource(img, pixel_size=450 * u.nm)) + src = Camera(StaticSource(img, pixel_size=450 * u.nm), analog_max=0xFFFF) roi_detector = SingleRoi(src, pos=(200, 300), radius=0) # Only measure that specific point assert roi_detector.read() == int(2**16 - 1) # it should cast the array into some int @@ -34,11 +34,11 @@ def test_microscope_without_magnification(shape): # construct input image img = np.zeros(shape, dtype=np.int16) img[256, 256] = 100 - src = Camera(StaticSource(img, pixel_size=400 * u.nm)) + src = Camera(StaticSource(img, pixel_size=400 * u.nm), analog_max=0xFFFF) # construct microscope sim = Microscope(source=src, magnification=1, numerical_aperture=1, wavelength=800 * u.nm) - cam = Camera(sim) + cam = Camera(sim, analog_max=None) img = cam.read() assert img[256, 256] == 2**16 - 1 From 2d2f31b1238dc3d1368de839bd169fb03af77f5d Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 13:01:29 +0200 Subject: [PATCH 33/37] fixed missing/incorrect analog_max --- examples/micro_wfs_demo.cfg | 47 +++++++++++++++++++++++++++++++++++++ tests/test_simulation.py | 10 ++++---- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 examples/micro_wfs_demo.cfg diff --git a/examples/micro_wfs_demo.cfg b/examples/micro_wfs_demo.cfg new file mode 100644 index 0000000..30e7b3a --- /dev/null +++ b/examples/micro_wfs_demo.cfg @@ -0,0 +1,47 @@ +# Generated by Configurator on Wed Oct 09 16:05:06 CEST 2024 + +# Reset +Property,Core,Initialize,0 + +# Devices +Device,PyHub,PyDevice,PyHub +Device,camera,PyDevice,Camera:camera +Device,algorithm,PyDevice,Device:algorithm +Device,stage,PyDevice,XYStage:stage + +# Pre-init settings for devices +Property,PyHub,PythonEnvironment,(auto) +Property,PyHub,ScriptPath,./micro_wfs_demo.py + +# Pre-init settings for COM ports + +# Hub (parent) references +Parent,camera,PyHub +Parent,algorithm,PyHub +Parent,stage,PyHub + +# Initialize +Property,Core,Initialize,1 + +# Delays + +# Focus directions + +# Roles +Property,Core,Camera,camera +Property,Core,AutoShutter,1 + +# Camera-synchronized devices + +# Labels + +# Configuration presets +# Group: Channel + +# Group: System +# Preset: Startup + + + +# PixelSize settings + diff --git a/tests/test_simulation.py b/tests/test_simulation.py index bd7001f..6cfd666 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -20,7 +20,7 @@ def test_mock_camera_and_single_roi(): """ img = np.zeros((1000, 1000), dtype=np.int16) img[200, 300] = 39.39 # some random float - src = Camera(StaticSource(img, pixel_size=450 * u.nm), analog_max=0xFFFF) + src = Camera(StaticSource(img, pixel_size=450 * u.nm), analog_max=None) roi_detector = SingleRoi(src, pos=(200, 300), radius=0) # Only measure that specific point assert roi_detector.read() == int(2**16 - 1) # it should cast the array into some int @@ -49,7 +49,7 @@ def test_microscope_and_aberration(): """ img = np.zeros((1000, 1000), dtype=np.int16) img[256, 256] = 100 - src = Camera(StaticSource(img, pixel_size=400 * u.nm)) + src = Camera(StaticSource(img, pixel_size=400 * u.nm), analog_max=None) slm = SLM(shape=(512, 512)) @@ -77,7 +77,7 @@ def test_slm_and_aberration(): """ img = np.zeros((1000, 1000), dtype=np.int16) img[256, 256] = 100 - src = Camera(StaticSource(img, pixel_size=400 * u.nm)) + src = Camera(StaticSource(img, pixel_size=400 * u.nm), analog_max=None) slm = SLM(shape=(512, 512)) @@ -116,7 +116,7 @@ def test_slm_tilt(): img[signal_location] = 100 pixel_size = 400 * u.nm wavelength = 750 * u.nm - src = Camera(StaticSource(img, pixel_size=pixel_size)) + src = Camera(StaticSource(img, pixel_size=pixel_size), analog_max=None) slm = SLM(shape=(1000, 1000)) @@ -137,7 +137,7 @@ def test_slm_tilt(): new_location = signal_location + shift - cam = Camera(sim) + cam = Camera(sim, analog_max=None) img = cam.read(immediate=True) max_pos = np.unravel_index(np.argmax(img), img.shape) assert np.all(max_pos == new_location) From 05eb2a85b7d97cb191bee31ef590cb7737949752 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 13:07:14 +0200 Subject: [PATCH 34/37] version bump for testing testpypi upload --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd330c0..66f7aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openwfs" -version = "0.1.0rc3" +version = "0.1.0rc4" description = 'A libary for performing wavefront shaping experiments and simulations' authors = ["Ivo Vellekoop ", "Daniël Cox", "Jeroen Doornbos"] license = "BSD-3-Clause" From bc3c3e73d5eff227707807ff35fbdc0f00e0f57b Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 19:53:41 +0200 Subject: [PATCH 35/37] updating subpackage install and documentation --- docs/source/conf.py | 4 ++-- docs/source/development.rst | 4 ++-- docs/source/readme.rst | 12 ++++++++++-- openwfs/devices/__init__.py | 12 ++++-------- openwfs/devices/camera.py | 2 +- openwfs/devices/galvo_scanner.py | 2 +- openwfs/devices/nidaq_gain.py | 2 +- openwfs/devices/slm/context.py | 2 +- openwfs/devices/slm/patch.py | 3 +-- openwfs/devices/slm/slm.py | 4 ++-- openwfs/devices/slm/texture.py | 4 +--- pyproject.toml | 2 +- 12 files changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 16a13ec..090fd96 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,8 +72,8 @@ OpenWFS addresses these challenges by providing a modular Python library that separates hardware control from the wavefront shaping algorithm itself. - Using these elements, an wavefront shaping algorithm can be written - in just a few lines of code, with OpenWFS taking care of low-level hardware control, synchronization, + Using these elements, a wavefront shaping algorithm can be written + in a minimal amount of code, with OpenWFS taking care of low-level hardware control, synchronization, and troubleshooting. Algorithms can be used on different hardware or in a completely simulated environment without changing the code. Moreover, we provide full integration with the \textmu Manager microscope control software, enabling wavefront shaping experiments to be diff --git a/docs/source/development.rst b/docs/source/development.rst index 19207ca..8ef1c0a 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -14,9 +14,9 @@ To download the source code, including tests and examples, clone the repository poetry install --with dev --with docs poetry run pytest -The examples are located in the ``examples`` directory. Note that a lot of functionality is also demonstrated in the automatic tests located in the ``tests`` directory. As an alternative to downloading the source code, the samples can also be copied directly from the example gallery on the documentation website :cite:`readthedocsOpenWFS`. +The examples are located in the ``examples`` directory. Note that a lot of functionality is also demonstrated in the automatic tests located in the ``tests`` directory. As an alternative to downloading the source code, the samples can also be copied directly from the example gallery on the documentation website :cite:`readthedocsOpenWFS`. -Important to note for adding hardware devices, is that many of the components rely on third-party, in some case proprietary drivers. For using NI DAQ components, the nidaqmx package needs to be installed, and for openCV and Genicam their respective drivers need to be installed. The specific requirements are always listed in the documentation of the functions and classes that require packages like these. +By default, this only installs the dependencies for the basic OpenWFS package. To install the dependencies for the other components (the OpenGL, genicam or nidaq), use ``poetry -E opengl -E genicam -E nidaq install`` or ``poetry -E all`. Building the documentation -------------------------------------------------- diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 76e0964..4d9c97a 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -59,9 +59,17 @@ To use OpenWFS, Python 3.9 or later is required. Since it is available on the Py pip install openwfs[all] -This will also install the optional dependencies for OpenWFS, such as ``PyOpenGL``, ``nidaqmx`` and ``harvesters``, which are used for OpenGL-accelerated SLM control, scanning microscopy, and camera control, respectively. If these dependencies cannot be installed on your system, the installation will fail. This can happen with ``PyOpenGL`` on systems with no OpenGL driver installed, or for ``harvesters``, which currently only works for Python versions up to 3.11. In this case, you can instead install OpenWFS without dependencies by omitting ``[all]`` in the installation command, and manually install only the required dependencies as indicated in the documentation for each hardware component. +This will also install the optional dependencies for OpenWFS: -At the time of writing, OpenWFS is tested up to Python version 3.11 on Windows 11 and Manjaro Linux, and the latest version is 0.1.0. Note that the latest versions of the package will be available on the PyPI repository, and the latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`. The source code can be found on :cite:`openwfsgithub`. +*opengl* For the OpenGL-accelerated SLM control, the ``PyOpenGL`` package is installed. In order for this package to work, an OpenGL-compatible graphics card and driver is required. + +*genicam* For the GenICam camera support, the ``harvesters`` package is installed, which, in turn, needs the ``genicam`` package. At the time of writing, this package is only available for Python versions up to 3.11. To use the GenICam camera support, you also need a compatible camera with driver installed. + +* nidaq* For the scanning microscope, the ``nidaqmx`` package is installed, which requires a National Instruments data acquisition card with corresponding drivers to be installed on your system. + +If these dependencies cannot be installed on your system, the installation will fail. In this case, you can instead install OpenWFS without dependencies by omitting ``[all]`` in the installation command, and manually install only the required dependencies, e.g. ``pip install openwfs[opengl]``. + +At the time of writing, OpenWFS is at version 0.1.0, and it is tested up to Python version 3.11 on Windows 11 and Manjaro and Ubuntu Linux distributions. Note that the latest versions of the package will be available on the PyPI repository, and the latest documentation and the example code can be found on the `Read the Docs `_ website :cite:`openwfsdocumentation`. The source code can be found on :cite:`openwfsgithub`. :numref:`hello-wfs` shows an example of how to use OpenWFS to run a simple wavefront shaping experiment. This example illustrates several of the main concepts of OpenWFS. First, the code initializes objects to control a spatial light modulator (SLM) connected to a video port, and a camera that provides feedback to the wavefront shaping algorithm. It then runs a WFS algorithm to focus the light. diff --git a/openwfs/devices/__init__.py b/openwfs/devices/__init__.py index a53b058..cc92a5e 100644 --- a/openwfs/devices/__init__.py +++ b/openwfs/devices/__init__.py @@ -2,20 +2,16 @@ import warnings -def safe_import(module_name): +def safe_import(module_name: str, extra_name: str): try: return importlib.import_module(module_name) except ModuleNotFoundError: warnings.warn( f"""Could not import {module_name}, because the package is not installed. - To install, use: - pip install {module_name} - - Alternatively, specify to install the required extras when installing openwfs, using one of: + To install, using: + pip install openwfs[{extra_name}] + or pip install openwfs[all] - pip install openwfs[opengl] - pip install openwfs[genicam] - pip install openwfs[nidaq] """ ) return None diff --git a/openwfs/devices/camera.py b/openwfs/devices/camera.py index f3a4e6d..2324f5d 100644 --- a/openwfs/devices/camera.py +++ b/openwfs/devices/camera.py @@ -6,7 +6,7 @@ from . import safe_import -hc = safe_import("harvesters.core") +hc = safe_import("harvesters.core", "genicam") if hc is not None: from harvesters.core import Harvester diff --git a/openwfs/devices/galvo_scanner.py b/openwfs/devices/galvo_scanner.py index d503c71..815c387 100644 --- a/openwfs/devices/galvo_scanner.py +++ b/openwfs/devices/galvo_scanner.py @@ -9,7 +9,7 @@ from . import safe_import -ni = safe_import("nidaqmx") +ni = safe_import("nidaqmx", "nidaq") if ni is not None: import nidaqmx.system from nidaqmx.constants import TerminalConfiguration, DigitalWidthUnits diff --git a/openwfs/devices/nidaq_gain.py b/openwfs/devices/nidaq_gain.py index b359029..eec4c75 100644 --- a/openwfs/devices/nidaq_gain.py +++ b/openwfs/devices/nidaq_gain.py @@ -5,7 +5,7 @@ from . import safe_import -ni = safe_import("nidaqmx") +ni = safe_import("nidaqmx", "nidaq") class Gain: diff --git a/openwfs/devices/slm/context.py b/openwfs/devices/slm/context.py index 2f16f2b..10a9dae 100644 --- a/openwfs/devices/slm/context.py +++ b/openwfs/devices/slm/context.py @@ -3,7 +3,7 @@ from .. import safe_import -glfw = safe_import("glfw") +glfw = safe_import("glfw", "opengl") SLM = "slm.SLM" diff --git a/openwfs/devices/slm/patch.py b/openwfs/devices/slm/patch.py index fd15ef8..dfad8bf 100644 --- a/openwfs/devices/slm/patch.py +++ b/openwfs/devices/slm/patch.py @@ -1,4 +1,3 @@ -import warnings from typing import Sequence, Optional import numpy as np @@ -7,7 +6,7 @@ from .context import Context from .. import safe_import -GL = safe_import("OpenGL.GL") +GL = safe_import("OpenGL.GL", "opengl") if GL is not None: from OpenGL.GL import shaders diff --git a/openwfs/devices/slm/slm.py b/openwfs/devices/slm/slm.py index 4e83f47..a2a6ae1 100644 --- a/openwfs/devices/slm/slm.py +++ b/openwfs/devices/slm/slm.py @@ -11,8 +11,8 @@ from .. import safe_import from ...simulation import PhaseToField -GL = safe_import("OpenGL.GL") -glfw = safe_import("glfw") +GL = safe_import("OpenGL.GL", "opengl") +glfw = safe_import("glfw", "opengl") from .patch import FrameBufferPatch, Patch, VertexArray from ...core import PhaseSLM, Actuator, Device, Detector from ...utilities import Transform diff --git a/openwfs/devices/slm/texture.py b/openwfs/devices/slm/texture.py index f07e699..6d8228d 100644 --- a/openwfs/devices/slm/texture.py +++ b/openwfs/devices/slm/texture.py @@ -1,11 +1,9 @@ -import warnings - import numpy as np from .context import Context from .. import safe_import -GL = safe_import("OpenGL.GL") +GL = safe_import("OpenGL.GL", "opengl") class Texture: diff --git a/pyproject.toml b/pyproject.toml index 66f7aef..e6c2338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ glfw = { version = "^2.5.9", optional = true } nidaq = ["nidaqmx"] genicam = ["harvesters"] opengl = ["PyOpenGL", "glfw"] -all = ["dev", "doc", "harvesters", "opengl", "nidaq"] +all = ["nidaqmx", "harvesters", "PyOpenGL", "glfw"] [tool.black] From 3bfae15e19201ffe62ab028e5183844997a34893 Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 19:57:44 +0200 Subject: [PATCH 36/37] Update pytest.yml --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 32eef13..134d520 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | - poetry install --with dev + poetry install --with dev --extras all - name: Run tests run: | From e3f7c5cab92b6fdda573c827b84ee666d41fae9d Mon Sep 17 00:00:00 2001 From: Ivo Vellekoop Date: Thu, 10 Oct 2024 21:45:28 +0200 Subject: [PATCH 37/37] version bump --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6c2338..c13bfcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] name = "openwfs" -version = "0.1.0rc4" +version = "0.1.0rc5" description = 'A libary for performing wavefront shaping experiments and simulations' -authors = ["Ivo Vellekoop ", "Daniël Cox", "Jeroen Doornbos"] +authors = ["Ivo Vellekoop ", "Jeroen Doornbos", "Daniël Cox"] license = "BSD-3-Clause" readme = "README.md" repository = "https://github.com/ivovellekoop/openwfs"