Skip to content

Commit

Permalink
implement the tournament selection method (#27)
Browse files Browse the repository at this point in the history
* enable the initialization of the EVQE population with individuals using more than one circuit layer

* make internal variables of the evqe selection operator private, add changes made to the changelog
  • Loading branch information
dleidreiter authored May 27, 2024
1 parent 62fa4b0 commit a9e8b00
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 25 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Current version: 0.2.0

## Unreleased

### Added

- Added tournament selection as an alternative selection method for EVQE ([Issue #25])

### Fixed

- Fix Pauli strings being in inverse bit order ([Issue #23])
Expand All @@ -20,4 +24,5 @@ Current version: 0.2.0

- Initial codeless pypi commit

[Issue #25]: https://github.com/DLR-RB/QUEASARS/issues/25
[Issue #23]: https://github.com/DLR-RB/QUEASARS/issues/23
109 changes: 84 additions & 25 deletions queasars/minimum_eigensolvers/evqe/evolutionary_algorithm/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from concurrent.futures import Future as ConcurrentFuture, wait as concurrent_wait
from random import Random
from typing import Optional, Union, cast
from warnings import warn

from dask.distributed import Future as DaskFuture, wait as dask_wait, Client
from numpy import argmin
Expand All @@ -28,14 +29,37 @@ class EVQESelection(BaseEvolutionaryOperator[EVQEPopulation]):
:type: float
:param beta_penalty: scaling factor for penalizing the amount of controlled gates of an individual
:type: float
:param use_tournament_selection: indicates whether to use tournament selection. By default, this is
set to False. In that case, roulette wheel selection is used. Should be true, if the measured expectation
values can be negative.
:type use_tournament_selection: bool
:param tournament_size: indicates the size of the tournaments used. This can be in the range [1, population_size].
It cannot be None, if use_tournament_selection is set to True. A tournament_size of 1 yields random selection,
with increasing tournament selection sizes increasing the selection pressure.
:type tournament_size: int
:param random_seed: integer value to control randomness
:type: int
"""

def __init__(self, alpha_penalty: float, beta_penalty: float, random_seed: Optional[int]):
self.alpha_penalty: float = alpha_penalty
self.beta_penalty: float = beta_penalty
self.random_generator: Random = Random(random_seed)
def __init__(
self,
alpha_penalty: float,
beta_penalty: float,
use_tournament_selection: bool = False,
tournament_size: Optional[int] = None,
random_seed: Optional[int] = None,
):
self._alpha_penalty: float = alpha_penalty
self._beta_penalty: float = beta_penalty
self._use_tournament_selection: bool = use_tournament_selection
if self._use_tournament_selection:
if tournament_size is not None:
self._tournament_size: int = tournament_size
if self._tournament_size < 1:
raise ValueError("the tournament_size must be at least 1!")
else:
raise ValueError("tournament_size cannot be None, if tournament selection should be used!")
self._random_generator: Random = Random(random_seed)

def apply_operator(self, population: EVQEPopulation, operator_context: OperatorContext) -> EVQEPopulation:
# measure the expectation values for all individuals
Expand Down Expand Up @@ -84,29 +108,64 @@ def apply_operator(self, population: EVQEPopulation, operator_context: OperatorC
)
operator_context.result_callback(result)

# disallow negative or 0 values in fitnesses by shifting all evaluation results by a fixed offset
offset: float
if evaluation_results[best_individual_index] <= 0:
offset = -evaluation_results[best_individual_index] + 1
else:
offset = 0

fitness_values: list[float] = [
(
evaluation_results[i]
+ offset
+ self.alpha_penalty * len(individual.layers)
+ self.beta_penalty * individual.get_n_controlled_gates()
selected_individuals: list[EVQEIndividual] = []
fitness_values: list[float] = []

if not self._use_tournament_selection:
# disallow negative or 0 values in fitnesses by shifting all evaluation results by a fixed offset
offset: float
if evaluation_results[best_individual_index] <= 0:
offset = -evaluation_results[best_individual_index] + 1
warn(
"Tournament selection should be preferred over roulette wheel selection, "
+ "if negative expectation values are involved in the fitness!"
)
else:
offset = 0

fitness_values = [
(
evaluation_results[i]
+ offset
+ self._alpha_penalty * len(individual.layers)
+ self._beta_penalty * individual.get_n_controlled_gates()
)
* float(len(population.species_members[population.species_membership[i]]))
for i, individual in enumerate(population.individuals)
]

fitness_weights: list[float] = [1 / (fitness + offset) for fitness in fitness_values]
selected_individuals = self._random_generator.choices(
population.individuals, weights=fitness_weights, k=len(population.individuals)
)
* float(len(population.species_members[population.species_membership[i]]))
for i, individual in enumerate(population.individuals)
]

fitness_weights: list[float] = [1 / fitness for fitness in fitness_values]
else:

selected_individuals: list[EVQEIndividual] = self.random_generator.choices(
population.individuals, weights=fitness_weights, k=len(population.individuals)
)
fitness_values = [
(
evaluation_results[i]
+ self._alpha_penalty * len(individual.layers)
+ self._beta_penalty * individual.get_n_controlled_gates()
)
* float(len(population.species_members[population.species_membership[i]]))
for i, individual in enumerate(population.individuals)
]

while len(selected_individuals) < len(population.individuals):
tournament_indices = self._random_generator.choices(
range(0, len(population.individuals)), k=self._tournament_size
)
best_index: Optional[int] = None
best_fitness_value: Optional[float] = None

for tournament_index in tournament_indices:
if best_fitness_value is None or fitness_values[tournament_index] < best_fitness_value:
best_index = tournament_index
best_fitness_value = fitness_values[tournament_index]

if best_index is not None:
selected_individuals.append(population.individuals[best_index])
else:
raise EVQESelectionException("No individual was selected for a round of tournament selection!")

return EVQEPopulation(
individuals=tuple(selected_individuals),
Expand Down
21 changes: 21 additions & 0 deletions queasars/minimum_eigensolvers/evqe/evqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class EVQEMinimumEigensolverConfiguration:
a python ThreadPool executor. If a dask Client is used, both the Sampler and Estimator need to be serializable
by dask, otherwise the computation will fail. If no parallel_executor is provided a ThreadPoolExecutor
with as many threads as population_size will be launched
:param use_tournament_selection: indicates whether to use tournament selection. By default, this is
set to False. In that case, roulette wheel selection is used. Should be true, if the measured expectation
values can be negative.
:type use_tournament_selection: bool
:param tournament_size: indicates the size of the tournaments used. This can be in the range [1, population_size].
It cannot be None, if use_tournament_selection is set to True. A tournament_size of 1 yields random selection,
with increasing tournament selection sizes increasing the selection pressure.
:type tournament_size: int
:param randomize_initial_population_parameters: Determines whether the parameter values of the individuals in
the first population shall be initialized randomly or at 0. By default, the parameter values in the
initial population are initialized randomly
Expand Down Expand Up @@ -116,6 +124,8 @@ class EVQEMinimumEigensolverConfiguration:
parameter_search_probability: float
topological_search_probability: float
layer_removal_probability: float
use_tournament_selection: bool = False
tournament_size: Optional[int] = None
randomize_initial_population_parameters: bool = True
parallel_executor: Union[Client, ThreadPoolExecutor, None] = None
mutually_exclusive_primitives: bool = True
Expand All @@ -132,6 +142,15 @@ def __post_init__(self):
raise ValueError("The topological_search_probability must not exceed the range (0, 1)!")
if not 0 <= self.layer_removal_probability <= 1:
raise ValueError("The layer_removal_probability must not exceed the range (0, 1)!")
if self.use_tournament_selection and self.tournament_size is None:
raise ValueError("To use tournament_selection, a tournament_size must be specified! It cannot be None!")
if self.use_tournament_selection and not 1 <= self.tournament_size:
raise ValueError(f"The tournament_size cannot be smaller than 1!, but it was {self.tournament_size}!")
if self.use_tournament_selection and self.population_size < self.tournament_size:
raise ValueError(
f"The tournament_size cannot be larger than the size of the population ({self.population_size})! \n"
+ f"Yet the tournament_size is {self.tournament_size}!"
)


class EVQEMinimumEigensolver(EvolvingAnsatzMinimumEigensolver):
Expand Down Expand Up @@ -166,6 +185,8 @@ def __init__(self, configuration: EVQEMinimumEigensolverConfiguration):
EVQESelection(
alpha_penalty=configuration.selection_alpha_penalty,
beta_penalty=configuration.selection_beta_penalty,
use_tournament_selection=configuration.use_tournament_selection,
tournament_size=configuration.tournament_size,
random_seed=new_random_seed(random_generator=self.random_generator),
),
EVQEParameterSearch(
Expand Down

0 comments on commit a9e8b00

Please sign in to comment.