Population#
Population is a domain data structure for individuals and evaluation results.
On the legacy vector path, it is a structured array with x, f, g, and cv columns.
On the current graph-native path, GenomeBatch and SearchSpace define individual representations, and Population.x is a compatibility view available only when possible.
Initializer generates Genomes from the Problem’s SearchSpace and registers them with their candidate IDs in state and the Archive.
Therefore, not every Problem can be converted into a floating-point x array.
Population’s role#
Population holds Genomes, objective values, constraint values, constraint violations, and Algorithm-specific auxiliary attributes.
A Genome can be represented by a GenomeBatch implementation such as DenseVectorBatch, ObjectBatch, or PermutationBatch.
Use x or a dense numeric view only when the values can be safely obtained as numeric vectors.
Population.genomes is a read-only view borrowed from the live Population’s genome storage, not an immutable snapshot.
Subsequent Population operations such as update_array may update that storage in place, so the contents observed through an existing genomes view can change.
Use Population as an instance in ordinary cases, and subclass it only when custom individual management is needed.
Define the attribute schema as a list of PopulationAttribute(name, dtype, shape, default) values.
On the legacy vector path, add x, f, g, cv, and the auxiliary attributes returned by Algorithm.get_required_attrs(problem) to this schema.
On the Genome-native path, the GenomeBatch and SearchSpace contracts own the representation structure; individuals are not represented by a column schema alone.
What Population holds#
Method |
Role |
|---|---|
|
Gets an attribute’s raw array as a write-protected view |
|
Updates an attribute array in bulk |
|
A safe getter that returns |
|
Adds a single individual |
|
Adds a batch of individuals from another |
|
Extracts a subset as a new |
|
Resizing and deletion |
|
Reordering |
|
Creates an empty |
|
Holds a computed result as a cache valid until the Population changes |
|
Returns an |
|
The number of individuals |
The cache from set_cache/get_cache is automatically invalidated whenever an operation that changes the population — append/delete/update_array, etc. — is called.
NSGA2Comparator uses this mechanism when reusing its front-splitting and crowding-distance computation results within a generation.
import numpy as np
from saealib import Population, PopulationAttribute
attrs = [
PopulationAttribute("x", np.float64, (2,)),
PopulationAttribute("f", np.float64, (1,)),
PopulationAttribute("cv", np.float64, ()),
]
pop = Population(attrs, init_capacity=4)
pop.append(x=np.array([0.1, 0.2]), f=np.array([1.0]), cv=0.0)
pop.append(x=np.array([0.3, 0.4]), f=np.array([2.0]), cv=0.0)
pop.x # design-variable array with shape (2, 2)
pop[0] # an Individual view of the first individual
pop[0:1] # a Population containing only the first individual
Individual#
Individual is a lightweight view of a single individual, obtained via pop[i].
It doesn’t duplicate the actual data — it holds only a reference to the source Population and its own index.
You can read and write values via get_readonly_value(key)/update_value(key, value), or equivalently via attribute access such as ind.x/ind.f = ....
Using a stale Individual after the source Population’s structure (number of individuals or ordering) has changed raises an exception, as an invalid reference.
Invariants#
Individual-attribute arrays share the same individual count and schema, and mutating operations invalidate the cache. Updating them with mismatched row counts or schemas breaks the correspondence between individuals and evaluation results.
Archive#
Archive is a concrete class mixing ArchiveMixin into Population, used to accumulate evaluated solutions without duplicates.
add(element, **kwargs) takes almost the same arguments as append, but differs in that it ignores duplicate solutions.
The attribute used for duplicate detection is specified via the key_attr argument (default "x"), with atol/rtol adjusting the tolerance.
get_knn(x, k) provides nearest-neighbor search via a kd-tree (lazily built on first call), used by LocalSurrogateManager’s default training_set when gathering per-candidate local training data.
from saealib import Archive
arc = Archive(attrs, init_capacity=4, key_attr="x")
arc.add(x=np.array([0.1, 0.2]), f=np.array([1.0]), cv=0.0)
arc.add(
x=np.array([0.1, 0.2]), f=np.array([1.0]), cv=0.0
) # the duplicate solution is ignored
idx, dist = arc.get_knn(np.array([0.1, 0.2]), k=1)
ParetoArchive#
ParetoArchive is a concrete class mixing ParetoMixin into Population, continuously maintaining a non-dominated solution set.
Every time a new solution is added, existing solutions it dominates are removed, and if the new solution is itself dominated by an existing one, the new solution is discarded.
Dominance is judged using a feasibility-first scheme.
A feasible solution (cv <= eps_cv) always dominates an infeasible one; Dominator’s dominates is used only when both are feasible.
The dominator argument lets you swap out the definition of the dominance relation.
eps_cv’s default is 0.0 (only strictly feasible solutions are considered acceptable), but during an Optimizer run, this value is overwritten every generation with problem.handler.feasibility_threshold.
The 0.0 default only matters when using ParetoArchive standalone, detached from Optimizer.
Extending Population#
ArchiveMixin/ParetoMixin are designed on the assumption that they’re mixed into a subclass of Population via multiple inheritance.
If you need custom population-management logic, you can define a new class combining these mixins (class MyArchive(ArchiveMixin, Population): ...).
You can also override Algorithm’s population_class/archive_class to swap the Population/Archive that Initializer generates for your own custom subclass.