Skip to content

Continuous genetic algorithm #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions doc/guide/guide-control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,107 @@ Eventually, the optimization for a desired `fid_err_targ` can be started by call
},
)

The Genetic Algorithm (GA)
==========================

The Genetic Algorithm (GA) is a global optimization technique inspired by natural selection.
Unlike gradient-based methods like GRAPE or CRAB, GA explores the solution space stochastically,
making it robust against local minima and suitable for problems with non-differentiable or noisy objectives.

In QuTiP, the GA-based optimizer evolves a population of candidate control pulses across multiple
generations to minimize the infidelity between the system's final and target states.

The GA optimization consists of the following components:

**Population**:
A collection of candidate solutions (chromosomes), where each chromosome encodes a full set
of control amplitudes over time for the given control Hamiltonians.

**Fitness Function**:
The fitness of each candidate is evaluated using a fidelity-based measure, such as:

- **PSU (Projective State Overlap)**:
:math:`1 - |\langle \psi_{\text{target}} | \psi_{\text{final}} \rangle|`
- **TRACEDIFF**:
Trace distance between final and target density matrices.

**Genetic Operations**:

- **Selection**:
A subset of the best-performing candidates (based on fitness) are chosen to survive.

- **Crossover (Mating)**:
New candidates are generated by combining genes from selected parents using arithmetic crossover.

- **Mutation**:
Random perturbations are added to the new candidates' genes to maintain diversity and escape local minima.

This process continues until either a target fidelity error is reached or a fixed number of generations
have passed without improvement (stagnation criterion).

Each generation represents a full evaluation of the population, making the method inherently parallelizable
and effective in high-dimensional control landscapes.

In QuTiP, the GA optimization is implemented via the ``_Genetic`` class, and can be invoked using the
standard ``optimize_pulses`` interface by setting the algorithm to ``"Genetic"``.

Optimal Quantum Control in QuTiP (Genetic Algorithm)
====================================================

Defining a control problem in QuTiP using the Genetic Algorithm follows the same pattern as with other algorithms.

.. code-block:: python

import qutip as qt
import qutip_qoc as qoc
import numpy as np

# state to state transfer
initial = qt.basis(2, 0)
target = qt.basis(2, 1)

# define the drift and control Hamiltonians
drift = qt.sigmaz()
controls = [qt.sigmax(), qt.sigmay()]

H = [drift] + controls

# define the objective
objective = qoc.Objective(initial, H, target)

# discretized time grid (e.g., 100 steps over 10 units of time)
tlist = np.linspace(0, 10, 100)

# define control parameter bounds (same for all controls in GA)
control_parameters = {
"p": {"bounds": [(-1.0, 1.0)]}
}

# set genetic algorithm hyperparameters
algorithm_kwargs = {
"alg": "Genetic",
"population_size": 50,
"generations": 100,
"mutation_rate": 0.2,
"fid_err_targ": 1e-3,
}

# run the optimization
result = qoc.optimize_pulses(
objectives=[objective],
control_parameters=control_parameters,
tlist=tlist,
algorithm_kwargs=algorithm_kwargs,
)

print("Final infidelity:", result.infidelity)
print("Best control parameters:", result.optimized_params)

References
==========

.. [Brown2023] J. Brown, M. Paternostro, and A. Ferraro, "Optimal quantum control via genetic algorithms for quantum state engineering in driven‑resonator mediated networks," *Quantum Sci. Technol.*, vol. 8, no. 2, p. 025004, 2023. https://doi.org/10.1088/2058-9565/acb2f2

.. TODO: add examples

Examples for Liouvillian dynamics and multi-objective optimization will follow soon.
226 changes: 226 additions & 0 deletions src/qutip_qoc/_genetic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import numpy as np
import qutip as qt
import time
from qutip_qoc.result import Result


class _Genetic:
"""
Genetic Algorithm-based optimizer for quantum control problems.

This class implements a global optimization routine using a Genetic Algorithm
to optimize control pulses that drive a quantum system from an initial state
to a target state (or unitary).

Based on the code from Jonathan Brown
"""

def __init__(
self,
objectives,
control_parameters,
time_interval,
time_options,
alg_kwargs,
optimizer_kwargs,
minimizer_kwargs,
integrator_kwargs,
qtrl_optimizers,
):
self._objective = objectives[0]
self._Hd = self._objective.H[0]
self._Hc_lst = [
H[0] if isinstance(H, list) else H for H in self._objective.H[1:]
]
self._initial = self._objective.initial
self._target = self._objective.target
self._norm_fac = 1 / self._target.norm()

self._evo_time = time_interval.evo_time
self.N_steps = time_interval.n_tslots
self.N_controls = len(self._Hc_lst)
self.N_var = self.N_controls * self.N_steps

self._alg_kwargs = alg_kwargs
self.N_pop = alg_kwargs.get("population_size", 100)
self.generations = alg_kwargs.get("generations", 100)
self.mutation_rate = alg_kwargs.get("mutation_rate", 0.3)
self.fid_err_targ = alg_kwargs.get("fid_err_targ", 1e-4)
self._stagnation_patience = 50 # Internally fixed

self._integrator_kwargs = integrator_kwargs
self._fid_type = alg_kwargs.get("fid_type", "PSU")

self._generator = self._prepare_generator()
self._solver = (
qt.MESolver(H=self._generator, options=self._integrator_kwargs)
if self._Hd.issuper
else qt.SESolver(H=self._generator, options=self._integrator_kwargs)
)

self._result = Result(
objectives=[self._objective],
time_interval=time_interval,
start_local_time=time.time(),
guess_controls=[self._generator],
guess_params=control_parameters.get("guess"),
var_time=False,
qtrl_optimizers=qtrl_optimizers,
)
self._result.iter_seconds = []
self._result.infidelity = np.inf
self._result._final_states = []

def _prepare_generator(self):
args = {
f"p{i+1}_{j}": 0.0
for i in range(self.N_controls)
for j in range(self.N_steps)
}

def make_coeff(i, j):
return lambda t, args: (
args[f"p{i+1}_{j}"]
if int(t / (self._evo_time / self.N_steps)) == j
else 0
)

H_qev = [self._Hd]
for i, Hc in enumerate(self._Hc_lst):
for j in range(self.N_steps):
H_qev.append([Hc, make_coeff(i, j)])

return qt.QobjEvo(H_qev, args=args)

def _infid(self, params):
args = {
f"p{i+1}_{j}": params[i * self.N_steps + j]
for i in range(self.N_controls)
for j in range(self.N_steps)
}
result = self._solver.run(self._initial, [0.0, self._evo_time], args=args)
final_state = result.final_state
self._result._final_states.append(final_state)

if self._fid_type == "TRACEDIFF":
diff = final_state - self._target
fid = 0.5 * np.real((diff.dag() * diff).tr())
else:
overlap = self._norm_fac * self._target.overlap(final_state)
fid = (
1 - np.abs(overlap) if self._fid_type == "PSU" else 1 - np.real(overlap)
)

return fid

def step(self, params):
t0 = time.time()
val = -self._infid(params)
self._result.iter_seconds.append(time.time() - t0)
return val

def initial_population(self):
return np.random.uniform(-1, 1, (self.N_pop, self.N_var))

def darwin(self, population, fitness):
indices = np.argsort(-fitness)[: self.N_pop // 2]
return population[indices], fitness[indices]

def pairing(self, survivors, survivor_fitness):
prob_dist = survivor_fitness - np.min(survivor_fitness)
prob_dist /= np.sum(prob_dist)

mothers = np.random.choice(len(survivors), size=self.N_pop // 4, p=prob_dist)
fathers = []
for m in mothers:
p = prob_dist.copy()
p[m] = 0
p /= np.sum(p)
fathers.append(np.random.choice(len(survivors), p=p))
return survivors[mothers], survivors[fathers]

def mating_procedure(self, ma, da):
beta = np.random.rand(*ma.shape)
swap = np.random.randint(0, 2, ma.shape)
beta_inv = 1 - beta

new1 = swap * beta * ma + swap * beta_inv * da + (1 - swap) * ma
new2 = swap * beta * da + swap * beta_inv * ma + (1 - swap) * da
return np.vstack((new1, new2))

def build_next_gen(self, survivors, offspring):
return np.vstack((survivors, offspring))

def mutate(self, population):
n_mut = int(
(population.shape[0] - 1) * population.shape[1] * self.mutation_rate
)
row = np.random.randint(1, population.shape[0], size=n_mut)
col = np.random.randint(0, population.shape[1], size=n_mut)
population[row, col] += np.random.normal(0, 0.3, size=n_mut)
population[row, col] = np.clip(population[row, col], -2, 2)
return population

def optimize(self):
population = self.initial_population()
best_fit = -np.inf
best_chrom = None
history = []
no_improvement_counter = 0

for gen in range(self.generations):
fitness = np.array([self.step(chrom) for chrom in population])
max_fit = np.max(fitness)
history.append(max_fit)

if max_fit > best_fit:
best_fit = max_fit
best_chrom = population[np.argmax(fitness)]
no_improvement_counter = 0
else:
no_improvement_counter += 1

self._result.infidelity = min(self._result.infidelity, -max_fit)

if -max_fit <= self.fid_err_targ:
break

if no_improvement_counter >= self._stagnation_patience:
break

survivors, survivor_fit = self.darwin(population, fitness)
mothers, fathers = self.pairing(survivors, survivor_fit)
offspring = self.mating_procedure(mothers, fathers)
population = self.build_next_gen(survivors, offspring)
population = self.mutate(population)

self._result.optimized_params = best_chrom.tolist()
self._result.infidelity = -best_fit
self._result.end_local_time = time.time()
self._result.n_iters = len(history)
self._result.new_params = self._result.optimized_params
self._result._optimized_controls = best_chrom.tolist()
self._result._optimized_H = [self._generator]
self._result._final_states = self._result._final_states # expose final_states
self._result.guess_params = self._result.guess_params or []
self._result.var_time = False

self._result.message = (
f"Stopped early: reached infidelity target {self.fid_err_targ}"
if -best_fit <= self.fid_err_targ
else (
f"Stopped due to stagnation after {self._stagnation_patience} generations"
if no_improvement_counter >= self._stagnation_patience
else "Optimization completed successfully"
)
)
return self._result

def result(self):
self._result.start_local_time = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(self._result.start_local_time)
)
self._result.end_local_time = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(self._result.end_local_time)
)
return self._result
18 changes: 17 additions & 1 deletion src/qutip_qoc/pulse_optim.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from qutip_qoc._time import _TimeInterval

import qutip as qt
from qutip_qoc._genetic import _Genetic

try:
from qutip_qoc._rl import _RL
Expand Down Expand Up @@ -418,6 +419,21 @@ def optimize_pulses(
)
rl_env.train()
return rl_env.result()

elif alg == "Genetic":
gen_env = _Genetic(
objectives,
control_parameters,
time_interval,
time_options,
algorithm_kwargs,
optimizer_kwargs,
minimizer_kwargs,
integrator_kwargs,
qtrl_optimizers,
)
gen_env.optimize()
return gen_env.result()

return _global_local_optimization(
objectives,
Expand All @@ -429,4 +445,4 @@ def optimize_pulses(
minimizer_kwargs,
integrator_kwargs,
qtrl_optimizers,
)
)
Loading
Loading