Skip to content

User guide

Overview

evortran implements genetic algorithms (GAs) for function minimization. GAs are population-based metaheuristic optimizers that mimic biological evolution: a set of candidate solutions (individuals) is evolved over many generations through selection, crossover (mating), and mutation.

The library exposes two main entry points:

  • evolve_population() — runs a single-population GA
  • evolve_migration() — runs several populations in parallel with periodic migration of individuals between them

Both functions return the best individual found.

Modules to import in your program:

use evortran__util_kinds,      only : wp          ! working precision (real64)
use evortran__individuals_float, only : individual  ! individual type
use evortran__evolutions_float,  only : evolve_population
use evortran__migrations_float,  only : evolve_migration

The fitness function

The fitness function evaluates the quality of a candidate solution. evortran minimizes fitness, so define it so that better solutions give smaller values (e.g., a squared residual).

The function must follow this signature:

subroutine my_fitness(ind, f)
  use evortran__individuals_float, only : individual
  use evortran__util_kinds,        only : wp
  class(individual), intent(in) :: ind
  real(wp), intent(out)         :: f
  ! compute f from ind%genes(:)
end subroutine

Each individual carries a real array ind%genes(1:gene_length). Gene values lie in the interval [lower_lim, upper_lim] (default [0, 1]). When you do not set lower_lim/upper_lim, map genes to your physical parameters inside the fitness function:

x = x_min + ind%genes(1) * (x_max - x_min)

When you do set lower_lim/upper_lim, use ind%genes directly as your parameters.

The fitness function can be declared pure (thread-safe). If it is pure, OpenMP parallelization is applied automatically across the population.


evolve_population — single-population GA (float genes)

use evortran__evolutions_float, only : evolve_population

best_ind = evolve_population(pop_size, gene_length, fit_func, ...)

Returns the individual with the lowest fitness found.

Required arguments

Argument Type Description
pop_size integer Number of individuals in the population.
gene_length integer Number of genes (parameters) per individual. Must be ≥ 2.
fit_func subroutine The fitness function (see above).

Optional arguments

General

Argument Type Default Description
lower_lim real(wp) 0.0 Lower bound for all genes. Must be set together with upper_lim.
upper_lim real(wp) 1.0 Upper bound for all genes.
max_generations integer pop_size Maximum number of generations.
fitness_target real(wp) Stop early when the best fitness falls below this value.
verbose logical .false. Print progress to the terminal.
gene_seed real(wp) Initialize all genes of the first individual to this value. Useful to seed the search near a known point. Cannot be combined with init_pop.
init_pop type(population) Provide a custom initial population. Cannot be combined with gene_seed.
add_ind type(individual) Replace the first individual in the initial population with this individual.

Random number generator

Argument Type Default Description
prng character(*) 'twister' Pseudo-random number generator. Options: 'twister' (Mersenne Twister), 'intrinsic' (Fortran intrinsic).
prng_seed integer 0 Seed for the PRNG. Use 0 for a random seed based on the system clock.
nthreads integer all available Number of OpenMP threads.

Selection

Argument Type Default Description
selection character(*) 'tournament' Selection method. Options: 'tournament', 'rank', 'roulette'.
selection_size integer pop_size Number of individuals selected as parents.
tourn_size integer 2 Number of individuals per tournament (only for selection='tournament').
wheele_size integer 3 Wheel size for roulette selection (only for selection='roulette').

Elitism

Argument Type Default Description
elitism character(*) 'best_fitness' Elitism mode. Options: 'best_fitness' (carry over the fittest individuals unchanged), 'none' (no elitism).
elite_size integer 1 Number of elite individuals to carry over.

Crossover (mating)

Argument Type Default Description
mating character(*) 'one-point' Crossover operator. Options: 'one-point', 'two-point', 'uniform', 'blend', 'sbx'.
mating_prob real(wp) 0.95 Probability that two selected parents actually mate (otherwise a parent is copied).
uniform_mating_ratio real(wp) 0.5 For mating='uniform': probability that each gene is taken from parent 1 (vs. parent 2).
blend_alpha real(wp) 0.5 For mating='blend': extent of the blend beyond the parents' gene range.
sbx_eta_c real(wp) library default For mating='sbx': distribution index η_c. Larger values produce offspring closer to the parents.
sbx_p_c real(wp) library default For mating='sbx': probability of crossover per gene.

Offspring

Argument Type Default Description
offspring_size integer pop_size Number of offspring produced each generation.
offspring_include_elite logical .true. Whether to include elite individuals among the offspring pool. Requires elitism /= 'none'.

Mutation

Argument Type Default Description
mutate character(*) 'uniform' Mutation operator. Options: 'uniform' (random value in bounds), 'gaussian' (Gaussian perturbation), 'shuffle' (swap two genes), 'none'.
mutate_prob real(wp) 0.1 Probability that a given individual is mutated.
mutate_gene_prob real(wp) 0.1 Probability that each gene of a selected individual is mutated.
mutate_gaussian_sigma real(wp) 0.5 * (upper_lim - lower_lim) Standard deviation of the Gaussian mutation.

Output collections

Argument Type Description
fittest_inds_from_gen type(individual), allocatable(:) Output. Array of the fittest individual at each generation.
pops_from_gen type(population), allocatable(:) Output. Array of the full population at each generation.
final_pop type(population) Output. The population at the end of the run.

evolve_population — single-population GA (integer genes)

use evortran__evolutions_integer, only : evolve_population

best_ind = evolve_population(pop_size, gene_length, fit_func, base_pairs, ...)

Integer-gene variant where each gene takes discrete values in {0, 1, ..., base_pairs - 1}.

Import the individual type from the integer module:

use evortran__individuals_integer, only : individual

The fitness function signature is identical to the float version but uses evortran__individuals_integer :: individual.

Additional required argument

Argument Type Description
base_pairs integer Number of distinct integer values each gene can take (≥ 2).

Optional arguments

The integer GA supports the same selection, elitism, mating (only 'one-point', 'two-point', 'uniform'), offspring, and mutate ('uniform', 'shuffle', 'none') arguments as the float GA. It does not support lower_lim/upper_lim, PRNG control, verbose, blend, sbx, or gaussian mutation.

Argument Type Default Description
gene_seed integer Initialize all genes to this value (0 ≤ gene_seed < base_pairs).
fittest_inds_from_gen type(individual), allocatable(:) Output. Array of fittest individual per generation.

evolve_migration — multi-population GA with migration

use evortran__migrations_float, only : evolve_migration

best_ind = evolve_migration( &
  pop_number, epoches, pop_size, gene_length, fit_func, ...)

Runs pop_number independent populations in parallel for max_generations generations (one epoch), then exchanges individuals between populations (migration), and repeats for epoches epochs total. Returns the best individual across all populations.

This approach is more effective than a single large population for multi-modal problems (functions with many local minima) because different populations can explore different regions of parameter space.

Required arguments (in addition to single-population arguments)

Argument Type Description
pop_number integer Number of independent populations (≥ 2).
epoches integer Number of migration epochs.

Migration-specific optional arguments

Argument Type Default Description
migration character(*) 'rank' Migration strategy. Currently only 'rank' is available (migrate the fittest individuals).
migration_size integer 1 Number of individuals migrating between populations per epoch.
migration_order character(*) 'random' Order in which populations exchange individuals. Options: 'random' (random pairs), 'LR' (left-to-right ring), 'RL' (right-to-left ring).

Output argument

Argument Type Description
fittest_inds_final_pops type(individual), allocatable(:) Output. Array of length pop_number with the fittest individual from each final population. Useful for sampling all basins of attraction.

All other optional arguments (lower_lim, upper_lim, max_generations, fitness_target, nthreads, prng, prng_seed, verbose, selection, elitism, mating, offspring, mutate, etc.) are the same as for evolve_population.


The individual type

The return value of both evolve_population and evolve_migration is of type individual:

type(individual) :: best_ind

Public components:

Component Type Description
best_ind%genes real(wp), allocatable(:) Gene array of length gene_length.
best_ind%lower_lim real(wp) Lower gene bound used during evolution.
best_ind%upper_lim real(wp) Upper gene bound used during evolution.
best_ind%length integer Number of genes.

Public methods:

Method Returns Description
best_ind%get_fitness() real(wp) Returns the fitness value (calculates it on first call).
best_ind%calc_fitness() Forces recalculation of fitness.
best_ind%reset_fitness() Marks the fitness as uncalculated.

Parallelization with OpenMP

evortran uses OpenMP to evaluate fitness functions in parallel across the population. The number of threads can be controlled in three ways:

  1. Via the nthreads argument (float GA only):

    best_ind = evolve_population(100, 2, fit_func, nthreads=4)
    

  2. Via the environment variable (before running):

    export OMP_NUM_THREADS=4
    

  3. Via the omp_lib subroutine (inside your program):

    use omp_lib
    call omp_set_num_threads(4)
    

By default, evortran uses all available CPU threads.

For parallel fitness evaluation to work, the fitness function must be thread-safe (i.e., it must not write to shared global state without thread-local protection).


Controlling the PRNG

evortran uses a pseudo-random number generator (PRNG) to initialize the population and to drive selection, crossover, and mutation. Two generators are available:

  • 'twister' — Mersenne Twister (MT19937-64). This is the default and recommended choice. It supports deterministic seeding, has a very long period, and good statistical properties. Each thread maintains its own independent PRNG state, so parallel execution remains fully deterministic.
  • 'intrinsic' — the Fortran compiler's built-in random_number routine. This mode does not support seeding and does not produce deterministic results.

Reproducible results

In 'twister' mode, prng_seed sets the seed value directly. The default prng_seed=0 seeds the generator with the value 0, which is deterministic. Any non-negative integer seed produces a fully reproducible run:

best_ind = evolve_population( &
  100, 2, fit_func,           &
  prng='twister',             &
  prng_seed=42)

Explicit initialization

The PRNG can also be initialized separately before calling evolve_population or evolve_migration, using the initialize_rands subroutine:

use evortran__prng_rand, only : initialize_rands

call initialize_rands(mode='twister', seed=42, nthreads=4)

If the PRNG has already been initialized and you do not pass prng, prng_seed, or nthreads to a subsequent call, evortran will reuse the existing state rather than reinitializing it.


Seeding the initial population

By default, evortran generates the initial population by drawing gene values uniformly at random from [lower_lim, upper_lim]. There are three ways to inject prior knowledge into the starting point.

gene_seed — uniform initialization

Setting gene_seed initializes all genes of every individual in the initial population to the same value. This concentrates the starting population around a known point and can speed up convergence when a good approximation of the solution is already available:

! Initialize all genes to 0.5 (midpoint of the default [0, 1] range)
best_ind = evolve_population(100, 3, fit_func, gene_seed=0.5_wp)

The value must satisfy lower_lim <= gene_seed <= upper_lim. Because all individuals start identically, diversity is introduced entirely by mutation in the first generation.

add_ind — inject a single individual

To insert one specific individual into the initial population without constraining the rest, use add_ind. The provided individual is placed in the first slot; the remaining pop_size - 1 individuals are generated randomly as usual:

use evortran__individuals_float, only : individual

type(individual) :: known_ind

known_ind = individual(3, fit_func, lower_lim=0.0_wp, upper_lim=1.0_wp)
known_ind%genes = [0.1_wp, 0.5_wp, 0.9_wp]

best_ind = evolve_population(100, 3, fit_func, add_ind=known_ind)

This is the best option when you have a single good starting point but still want the population to explore the full parameter space.

init_pop — provide a custom population

For full control over the starting population, pass a population object directly. This allows you to resume a previous run, combine results from multiple runs, or hand-craft the initial set of individuals:

use evortran__populations_float, only : population

type(population) :: my_pop
type(individual) :: best_ind

my_pop = population(100, 3, fit_func, lower_lim=0.0_wp, upper_lim=1.0_wp)
! optionally modify my_pop%inds(:) before passing it in

best_ind = evolve_population(100, 3, fit_func, init_pop=my_pop)

The init_pop and gene_seed arguments are mutually exclusive.


Build profiles

evortran's fpm.toml defines three build profiles:

Profile Purpose
debug Enables all runtime argument checks (-DDEBUG). Use while setting up your problem.
release Full compiler optimization (-O3, -march=native). Use for production runs.
coverage For code coverage analysis.
fpm build --profile="debug"    # development
fpm build --profile="release"  # production