base_population Module
Base population model implementation.
Overview
The base_population module provides the foundational population model that other population types inherit from.
Complete Module Reference
natal.base_population
Base population model helpers and abstractions.
This module provides the abstract base class and utilities for population models (discrete-generation and age-structured). The base class defines common interfaces, evolution methods, history management, and helpers that are implemented by concrete population classes.
This module provides a common abstraction layer for population models while keeping internal state representations compatible with NumPy/Numba kernels.
BasePopulation
BasePopulation(species: Species, name: str = 'Population', hooks: Optional[HookRegistrationMap] = None)
Bases: ABC, Generic[T_State]
Abstract base class for population models.
The base class unifies common behavior for different population model implementations (for example, Wright-Fisher and age-structured non-Wright-Fisher models). It manages the species/genetic architecture, indexing, hook registration, and modifier pipelines.
Attributes:
| Name | Type | Description |
|---|---|---|
ALLOWED_EVENTS |
List[str]
|
Event names supported by the hook system. |
species |
Species
|
Genetic architecture descriptor for this population. |
name |
str
|
Human-readable population name. |
tick |
int
|
Current simulation tick. |
registry |
IndexRegistry
|
Index registry for genotype/haplotype mappings. |
config |
PopulationConfig
|
Active static tensor/config container. |
state |
T_State
|
Active population state container. |
history |
List[Tuple[int, ndarray]]
|
Recorded state snapshots by tick. |
Initialize the base population.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
species
|
Species
|
Genetic architecture specifying chromosomes, loci, and alleles. |
required |
name
|
str
|
Optional population name (default: "Population"). |
'Population'
|
hooks
|
Optional[HookRegistrationMap]
|
Optional mapping of event names to hook registrations. Each
entry should be a sequence of tuples in the form |
None
|
Note
Registry and genotypes are initialized lazily via Template Method. Subclasses must implement _create_registry() and _get_genotypes().
Source code in src/natal/base_population.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
record_observation
property
writable
The compiled Observation used for observation-mode history.
registry
property
IndexRegistry instance managing genotype, haplotype, and label indices.
index_registry
property
Public accessor for the internal IndexRegistry.
state
property
Return the current population state container.
Returns:
| Name | Type | Description |
|---|---|---|
PopulationState |
T_State
|
The current state object used by the population. |
history
property
A list of recorded historical states as (tick, flattened_array) tuples.
total_population_size
property
Total population size (alias of get_total_count).
sex_ratio
property
Return the female-to-male ratio, or np.inf when male count is zero.
set_observations
Register observation groups and immediately compile the binary mask.
Once set, the mask is passed to simulation kernels to record observation-aggregated snapshots (compressed format) instead of raw flattened state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
groups
|
GroupsInput
|
Observation groups passed to |
required |
collapse_age
|
bool
|
Whether to collapse the age axis during projection. The stored kernel mask is always 4-D; collapse_age is recorded as metadata and respected by export functions. |
False
|
Source code in src/natal/base_population.py
refresh_modifier_maps
add_gamete_modifier
add_gamete_modifier(modifier: GameteModifier, name: Optional[str] = None, modifier_id: Optional[int] = None, refresh: bool = True) -> None
Register a gamete-level modifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modifier
|
GameteModifier
|
A |
required |
name
|
Optional[str]
|
Optional human-readable name for debugging. |
None
|
modifier_id
|
Optional[int]
|
Optional numeric priority used for ordering. |
None
|
Source code in src/natal/base_population.py
add_zygote_modifier
add_zygote_modifier(modifier: ZygoteModifier, name: Optional[str] = None, modifier_id: Optional[int] = None, refresh: bool = True) -> None
Register a zygote-level modifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modifier
|
ZygoteModifier
|
A |
required |
name
|
Optional[str]
|
Optional human-readable name for debugging. |
None
|
modifier_id
|
Optional[int]
|
Optional numeric priority used for ordering. |
None
|
Source code in src/natal/base_population.py
set_zygote_modifier
set_zygote_modifier(modifier: ZygoteModifier, modifier_id: Optional[int] = None, modifier_name: Optional[str] = None) -> None
Register a zygote modifier with an optional priority.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modifier
|
ZygoteModifier
|
A |
required |
modifier_id
|
Optional[int]
|
Numeric priority (lower values execute earlier). If omitted an id will be auto-assigned. |
None
|
modifier_name
|
Optional[str]
|
Optional name for debugging. |
None
|
Source code in src/natal/base_population.py
set_gamete_modifier
set_gamete_modifier(modifier: GameteModifier, modifier_id: Optional[int] = None, modifier_name: Optional[str] = None) -> None
Register a gamete modifier with optional priority and name.
Source code in src/natal/base_population.py
apply_preset
Apply a genetic preset to this population.
This is the preferred API for registering presets. The preset's gamete modifiers, zygote modifiers, and fitness effects are registered in the correct order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
preset
|
GeneticPreset
|
A GeneticPreset instance (e.g., HomingDrive or custom preset). |
required |
Examples:
>>> from natal.genetic_presets import HomingDrive
>>> drive = HomingDrive(
... name="MyDrive",
... drive_allele="Drive",
... target_allele="WT",
... drive_conversion_rate=0.95
... )
>>> population.apply_preset(drive)
See Also
:class:natal.genetic_presets.GeneticPreset - Base class for creating custom presets
:class:natal.genetic_presets.HomingDrive - Built-in gene drive preset
Source code in src/natal/base_population.py
builder
classmethod
Create a builder for this population type.
This is the recommended way to construct populations with presets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
species
|
Species
|
Genetic architecture for the population. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
A builder instance for this population type. |
Examples:
>>> pop = (AgeStructuredPopulation.builder(species)
... .set_age_structure(n_ages=10)
... .add_preset(HomingModificationDrive(...))
... .build())
Source code in src/natal/base_population.py
initialize_config
Initialize static lookup tensors used by the population model.
This prepares precomputed maps such as gametes_to_zygote_map and
genotype_to_gametes_map and wraps high-level modifiers so they can
be applied at tensor-level during simulation steps.
Note
Ensures registry is initialized before proceeding.
Source code in src/natal/base_population.py
register_gamete_labels
Register gamete labels in the IndexRegistry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels
|
Optional[Sequence[str]]
|
Sequence of string labels to register. Labels must be unique in the provided sequence. Existing labels are ignored. |
required |
Source code in src/natal/base_population.py
run_tick
abstractmethod
Execute one simulation tick.
Typical sequence:
1. Check termination and re-entrancy guards.
2. Trigger first hooks.
3. Run reproduction step.
4. Trigger early hooks.
5. Run survival step.
6. Trigger late hooks.
7. Run aging step.
8. Increment tick and clear running flag.
If any hook returns RESULT_STOP, remaining steps are skipped and
the population is marked as finished.
Returns:
| Type | Description |
|---|---|
BasePopulation[T_State]
|
BasePopulation[T_State]: |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the population is finished or already running. |
Source code in src/natal/base_population.py
step
get_total_count
abstractmethod
get_female_count
abstractmethod
get_male_count
abstractmethod
create_observation
create_observation(*, groups: Optional[GroupsInput] = None, collapse_age: bool = False) -> Observation
Create a compiled observation from the current population schema.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
groups
|
Optional[GroupsInput]
|
Observation groups passed to |
None
|
collapse_age
|
bool
|
Whether observation collapses the age axis. |
False
|
Returns:
| Type | Description |
|---|---|
Observation
|
Compiled |
Source code in src/natal/base_population.py
output_current_state
output_current_state(*, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, output_path: Optional[Union[str, Path]] = None, indent: int = 2) -> Dict[str, Any]
Export the current population state with observation rules applied.
This method integrates observation with state translation and can optionally write the JSON payload to a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observation
|
Optional[Observation]
|
Optional prebuilt observation object. When provided,
|
None
|
groups
|
Optional[GroupsInput]
|
Observation groups passed to |
None
|
collapse_age
|
bool
|
Whether observation rule generation collapses age axis. |
False
|
include_zero_counts
|
bool
|
Whether to keep zero-valued entries. |
False
|
output_path
|
Optional[Union[str, Path]]
|
Optional JSON file path. When provided, the payload is written to this file as UTF-8 JSON. |
None
|
indent
|
int
|
Indentation used when writing JSON. |
2
|
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
A dictionary with observation metadata and observed counts. |
Source code in src/natal/base_population.py
output_history
output_history(*, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, history: Optional[ndarray] = None, output_path: Optional[Union[str, Path]] = None, indent: int = 2) -> Dict[str, Any]
Export the observation history for this population.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observation
|
Optional[Observation]
|
Optional prebuilt observation object. When provided,
|
None
|
groups
|
Optional[GroupsInput]
|
Observation groups passed to |
None
|
collapse_age
|
bool
|
Whether observation rule generation collapses age axis. |
False
|
include_zero_counts
|
bool
|
Whether to keep zero-valued entries. |
False
|
history
|
Optional[ndarray]
|
Optional flattened history array. When omitted, the
population history is fetched from |
None
|
output_path
|
Optional[Union[str, Path]]
|
Optional JSON file path. When provided, the payload is written to this file as UTF-8 JSON. |
None
|
indent
|
int
|
Indentation used when writing JSON. |
2
|
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
A dictionary containing observation metadata and per-snapshot outputs. |
Source code in src/natal/base_population.py
finish_simulation
End simulation, trigger the finish event, and lock the population.
This method may be called by hooks for early termination.
After calling it, step(), run_tick(), and run() cannot run again.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the population is already finished. |
Examples:
>>> def check_extinction(pop):
... if pop.get_total_count() == 0:
... print("Population extinct, finishing simulation.")
... pop.finish_simulation()
>>> pop.set_hook('late', check_extinction)
Source code in src/natal/base_population.py
run
abstractmethod
reset
abstractmethod
compute_allele_frequencies
Compute frequencies of all alleles in the population, normalized per locus.
Returns:
| Type | Description |
|---|---|
Dict[str, float]
|
Dict[str, float]: Mapping |
Dict[str, float]
|
Frequencies are per-locus proportions in the range |
Source code in src/natal/base_population.py
set_hook
set_hook(event_name: str, func: HookCallback, hook_id: Optional[int] = None, hook_name: Optional[str] = None, compile: bool = True, deme_selector: Optional[DemeSelector] = None) -> None
Register an event hook with optional automatic compilation.
When compile=True and the function carries @hook metadata,
it enters the DSL compilation pipeline:
- declarative hook -> CSR plan in HookProgram (kernel executable)
- selector hook -> py_wrapper or njit_fn (mode dependent)
- numba hook -> njit_fn
Plain Python functions are still registered in traditional _hooks
for backward-compatible execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_name
|
str
|
Event name (must exist in |
required |
func
|
HookCallback
|
Callback function, supported forms include:
- plain function: |
required |
hook_id
|
Optional[int]
|
Numeric execution priority (optional, auto-assigned if omitted). Lower IDs execute first. |
None
|
hook_name
|
Optional[str]
|
Optional human-readable name for debugging. |
None
|
compile
|
bool
|
Whether to try compiling |
True
|
deme_selector
|
Optional[DemeSelector]
|
Optional deme selector.
- |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If event does not exist or hook_id is already in use. |
Examples:
>>> # Plain function (backward compatible)
>>> pop.set_hook('first', lambda p: print(f'Step {p.tick}'))
>>>
>>> # Declarative @hook function (auto-compiled)
>>> @hook()
>>> def reduce_juveniles():
... return [Op.scale(genotypes='AA', ages=[0, 1], factor=0.9)]
>>> pop.set_hook('early', reduce_juveniles)
>>>
>>> # Selector @hook function (auto-compiled)
>>> @hook(selectors={'target': 'AA'})
>>> def release(pop, target):
... pop.state.individual_count[1, 2, target] += 100
>>> pop.set_hook('first', release)
Source code in src/natal/base_population.py
1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 | |
trigger_event
Trigger an event and execute all registered hooks.
Execution order:
1. CSR operations (Numba fast path)
2. ``njit_fn`` hooks (user-defined Numba functions)
3. ``py_wrapper`` hooks (Python wrapper functions)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_name
|
str
|
Event name to trigger. |
required |
deme_id
|
int
|
Deme index. Default -1 for non-spatial populations. |
-1
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
|
Note
- Prefer HookExecutor (unified three-layer coordination).
- If executor is not built, fall back to traditional
_hooks(Python callbacks only). - In accelerated
run(), core events are mostly executed by kernels;trigger_eventis used mainly for explicit events (for examplefinish) and compatibility paths.
Examples:
>>> if result == RESULT_STOP:
... print("Simulation stopped by hook")
Source code in src/natal/base_population.py
get_hooks
Get all registered hooks for a specific event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_name
|
str
|
Event name. |
required |
Returns:
| Type | Description |
|---|---|
List[HookEntry]
|
List of tuples |
Source code in src/natal/base_population.py
remove_hook
Remove a specific hook from an event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_name
|
str
|
Event name. |
required |
hook_id
|
int
|
Hook ID. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if removed successfully, otherwise False. |
Source code in src/natal/base_population.py
has_python_hooks
Return whether any Python-layer hooks are currently registered.
Source code in src/natal/base_population.py
has_mixed_hook_types
Return whether any event mixes declarative/njit/python hook types.
Source code in src/natal/base_population.py
should_use_python_dispatch
Return whether this population should run with Python event dispatch.
Policy
- When Numba is disabled, any registered hook type uses Python dispatch so py/declarative/njit hooks share one sequential path.
- When Numba is enabled, only mixed hook-type timelines fall back to Python dispatch; homogeneous compiled timelines stay on kernel wrappers.
Source code in src/natal/base_population.py
ensure_hook_executor
Build HookExecutor lazily for Python event-dispatch paths.
register_compiled_hook
get_compiled_hooks
Get compiled hook descriptors, optionally filtered by event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
Optional[str]
|
Optional event name to filter by. |
None
|
Returns:
| Type | Description |
|---|---|
List[Any]
|
List of CompiledHookDescriptor sorted by priority. |
Source code in src/natal/base_population.py
register_declarative_hook
register_declarative_hook(event: str, ops: List[Any], priority: int = 0, name: str = 'declarative_hook') -> Any
Register a declarative hook from a list of operations.
This is an alternative to using the @hook decorator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
str
|
Event name ('first', 'early', 'late', 'finish') |
required |
ops
|
List[Any]
|
List of HookOp operations (from Op.scale, Op.add, etc.) |
required |
priority
|
int
|
Execution priority (lower = earlier) |
0
|
name
|
str
|
Hook name for debugging |
'declarative_hook'
|
Returns:
| Name | Type | Description |
|---|---|---|
CompiledHookDescriptor |
Any
|
The compiled descriptor |
Examples:
>>> from natal.hook_dsl import Op
>>> pop.register_declarative_hook(
... event='early',
... ops=[
... Op.scale(genotypes='AA', ages=[0, 1], factor=0.9),
... Op.add(genotypes='*', ages=0, delta=50, when='tick % 10 == 0'),
... ],
... name='juvenile_control'
... )
Source code in src/natal/base_population.py
get_compiled_event_hooks
Get compiled hooks for use with generated kernel wrappers.
This method collects all registered hooks and compiles them into Numba-friendly combined functions, one per event.
Returns:
| Name | Type | Description |
|---|---|---|
CompiledEventHooks |
CompiledEventHooks
|
Container with combined @njit hooks per event. Access via .first, .early, .late, .finish |
Examples: