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 GAevolve_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:
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:
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:
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:
-
Via the
nthreadsargument (float GA only): -
Via the environment variable (before running):
-
Via the
omp_libsubroutine (inside your program):
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-inrandom_numberroutine. 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:
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. |