Source code for openwfs.algorithms.genetic

import itertools

import numpy as np

from .utilities import WFSResult, DummyProgressBar
from ..core import Detector, PhaseSLM


[docs] class SimpleGenetic: """Simple genetic algorithm for wavefront shaping. This algorithm is included for illustrative purposes. It is based on the algorithm described in [1] and [2]. The algorithm performs the following steps: 1. Initialize all wavefronts in the population with random phases 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__( self, feedback: Detector, slm: PhaseSLM, shape: tuple[int, int] = (500, 500), population_size: int = 30, elite_size: int = 5, generations: int = 100, mutation_probability: float = 0.005, generator=None, ): """ Args: feedback: Source of feedback slm: The spatial light modulator shape: Width × height (in segments) of the wavefront population_size (int): The number of individuals in the population elite_size (int): The number of individuals in the elite pool 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") self.feedback = feedback self.slm = slm self.shape = shape self.population_size = population_size self.elite_size = elite_size self.generations = generations self.generator = generator or np.random.default_rng() self.mutation_count = round((population_size - elite_size) * np.prod(shape) * mutation_probability) def _generate_random_phases(self, shape): return self.generator.random(size=shape, dtype=np.float32) * (2 * np.pi)
[docs] def execute(self, *, progress_bar=DummyProgressBar()) -> WFSResult: """Executes the algorithm. Args: progress_bar: Optional tqdm-like progress bar for displaying progress """ # Initialize the population population = self._generate_random_phases((self.population_size, *self.shape)) # initialize the progress bar if available progress_bar.total = self.generations * self.population_size for i in itertools.count(): # Try all phase patterns measurements = np.zeros(self.population_size, dtype=np.float32) for p in range(self.population_size): self.slm.set_phases(population[p]) self.feedback.trigger(out=measurements[p, ...]) progress_bar.update() self.feedback.wait() # Sort the measurements in ascending order sorted_indices = np.argsort(measurements) elite = sorted_indices[-self.elite_size :] plebs = sorted_indices[: -self.elite_size] # Terminate after the specified number of generations, return the best wavefront if i >= self.generations: return WFSResult(t=np.exp(-1.0j * population[sorted_indices[-1]]), axis=2) # We keep the elite individuals, and regenerate the rest by mixing the elite # For this mixing, the probability of selecting an individual is proportional to its measured intensity. probabilities = measurements[elite] probabilities /= np.sum(probabilities) couples = self.generator.choice(elite, size=(2, len(plebs)), p=probabilities) if np.mean(population[couples[0]] == population[couples[1]]) > 0.97: # if the parents are too similar, randomly generate the plebs population[plebs] = self._generate_random_phases((len(plebs), *self.shape)) else: # otherwise, mix and mutate the parents mix_masks = self.generator.integers(1, size=(len(plebs), *self.shape), dtype=bool) offspring = np.where(mix_masks, population[couples[0]], population[couples[1]]) mutations = self.generator.integers(offspring.size, size=self.mutation_count) offspring.ravel()[mutations] = self._generate_random_phases(self.mutation_count) population[plebs] = offspring