Skip to content

age_structured_population Module

Age-structured population models with overlapping generations.

Overview

The age_structured_population module implements age-structured population models with support for age-dependent survival, fecundity, and optional sperm-storage mechanics.

Complete Module Reference

natal.age_structured_population

Age-structured population models.

This module implements age-structured (overlapping generation) population models and utilities for survival, reproduction, juvenile recruitment, and fitness management.

AgeStructuredPopulation

AgeStructuredPopulation(species: Species, population_config: PopulationConfig, name: Optional[str] = None, initial_individual_count: Optional[Mapping[str, Mapping[Union[Genotype, str], Union[List[int], Dict[int, int]]]]] = None, initial_sperm_storage: Optional[Mapping[Union[Genotype, str], Mapping[Union[Genotype, str], Union[Dict[int, float], List[float], float]]]] = None, hooks: Optional[HookRegistrationMap] = None)

Bases: BasePopulation[PopulationState]

Age-structured population model (overlapping generations).

An age-structured population built on BasePopulation and PopulationState. Supports age-dependent survival and fecundity, juvenile recruitment modes, optional sperm-storage mechanics, and a hook/modifier system for user extensions.

Attributes:

Name Type Description
snapshots Dict[str, object]

Storage for custom state snapshots.

Initialize an age-structured population instance using a PopulationConfig.

Parameters:

Name Type Description Default
species Species

Species object describing genetic architecture.

required
population_config PopulationConfig

Fully initialized PopulationConfig instance.

required
name Optional[str]

Human-readable population name. If None, uses "AgeStructuredPop".

None
initial_individual_count Optional[Mapping[str, Mapping[Union[Genotype, str], Union[List[int], Dict[int, int]]]]]

Initial population distribution. Format: {sex: {genotype: counts_by_age}}

None
initial_sperm_storage Optional[Mapping[Union[Genotype, str], Mapping[Union[Genotype, str], Union[Dict[int, float], List[float], float]]]]

Initial sperm storage state (if supported).

None
hooks Optional[HookRegistrationMap]

Event hook registrations to apply.

None

Examples:

>>> pop_config = PopulationConfigBuilder.build(species, ...)
>>> pop = AgeStructuredPopulation(
...     species,
...     pop_config,
...     name="MyPop",
...     initial_individual_count={...}
... )
Source code in src/natal/age_structured_population.py
def __init__(
    self,
    species: Species,
    population_config: PopulationConfig,
    name: Optional[str] = None,
    initial_individual_count: Optional[Mapping[str, Mapping[Union[Genotype, str], Union[List[int], Dict[int, int]]]]] = None,
    initial_sperm_storage: Optional[Mapping[Union[Genotype, str], Mapping[Union[Genotype, str], Union[Dict[int, float], List[float], float]]]] = None,
    hooks: Optional[HookRegistrationMap] = None,
):
    """Initialize an age-structured population instance using a PopulationConfig.

    Args:
        species: Species object describing genetic architecture.
        population_config: Fully initialized PopulationConfig instance.
        name: Human-readable population name. If None, uses "AgeStructuredPop".
        initial_individual_count: Initial population distribution.
            Format: {sex: {genotype: counts_by_age}}
        initial_sperm_storage: Initial sperm storage state (if supported).
        hooks: Event hook registrations to apply.

    Examples:
        >>> pop_config = PopulationConfigBuilder.build(species, ...)
        >>> pop = AgeStructuredPopulation(
        ...     species,
        ...     pop_config,
        ...     name="MyPop",
        ...     initial_individual_count={...}
        ... )
    """
    if name is None:
        name = "AgeStructuredPop"

    hooks_map: HookRegistrationMap = hooks or {}
    super().__init__(species, name, hooks=hooks_map)

    config_hook_slot = int(getattr(population_config, "hook_slot", 0))
    if config_hook_slot <= 0:
        config_hook_slot = self.hook_slot
    self._config = population_config._replace(hook_slot=np.int32(config_hook_slot))

    self._genotypes_list = species.get_all_genotypes()
    self._haploid_genotypes_list = species.get_all_haploid_genotypes()

    self._initialize_registry()

    self._state = PopulationState.create(
        n_genotypes=population_config.n_genotypes,
        n_sexes=population_config.n_sexes,
        n_ages=population_config.n_ages,
    )

    # Initialize from builder-injected config arrays if available.
    cfg_init_ind = population_config.get_scaled_initial_individual_count()
    if cfg_init_ind.shape == self._state_nn.individual_count.shape:
        self._state_nn.individual_count[:] = cfg_init_ind
    cfg_init_sperm = population_config.get_scaled_initial_sperm_storage()
    if cfg_init_sperm.shape == self._state_nn.sperm_storage.shape:
        self._state_nn.sperm_storage[:] = cfg_init_sperm

    self.snapshots = {}

    if initial_individual_count is not None:
        self._state_nn.individual_count.fill(0.0)
        self._distribute_initial_population(initial_individual_count)

    if initial_sperm_storage is not None:
        # TODO: add population_config.use_sperm_storage
        self._distribute_initial_sperm_storage(species, initial_sperm_storage)

    self._initial_population_snapshot = (
        self._state_nn.individual_count.copy(),
        self._state_nn.sperm_storage.copy(),
        None,
    )

    self._initialize_registry()
    self._finalize_hooks()
state property
state: PopulationState

PopulationState: The current state container for the population.

n_ages property
n_ages: int

int: Number of age classes in this population.

new_adult_age property
new_adult_age: int

int: Minimum age at which individuals are considered adults.

genotypes_present property
genotypes_present: Set[Genotype]

Set[Genotype]: Returns the set of genotypes with count > 0.

setup classmethod
setup(species: Species, name: str = 'AgeStructuredPop', stochastic: bool = True, use_continuous_sampling: bool = False, gamete_labels: Optional[List[str]] = None, use_fixed_egg_count: bool = False) -> AgeStructuredPopulationBuilder

Create and preconfigure an age-structured population builder.

Parameters:

Name Type Description Default
species Species

Species definition used to initialize the builder.

required
name str

Population name.

'AgeStructuredPop'
stochastic bool

Whether to use stochastic sampling.

True
use_continuous_sampling bool

Whether to use Dirichlet sampling.

False
gamete_labels Optional[List[str]]

Optional labels for gamete tracking.

None
use_fixed_egg_count bool

Whether egg count is deterministic.

False

Returns:

Type Description
AgeStructuredPopulationBuilder

A configured AgeStructuredPopulationBuilder for fluent chaining.

Examples:

AgeStructuredPopulation.setup(species).age_structure(...).initial_state(...).build()

Source code in src/natal/age_structured_population.py
@classmethod
def setup(
    cls,
    species: Species,
    name: str = "AgeStructuredPop",
    stochastic: bool = True,
    use_continuous_sampling: bool = False,
    gamete_labels: Optional[List[str]] = None,
    use_fixed_egg_count: bool = False,
) -> 'AgeStructuredPopulationBuilder':
    """Create and preconfigure an age-structured population builder.

    Args:
        species: Species definition used to initialize the builder.
        name: Population name.
        stochastic: Whether to use stochastic sampling.
        use_continuous_sampling: Whether to use Dirichlet sampling.
        gamete_labels: Optional labels for gamete tracking.
        use_fixed_egg_count: Whether egg count is deterministic.

    Returns:
        A configured ``AgeStructuredPopulationBuilder`` for fluent chaining.

    Examples:
        ``AgeStructuredPopulation.setup(species).age_structure(...).initial_state(...).build()``
    """
    from natal.population_builder import AgeStructuredPopulationBuilder
    builder = AgeStructuredPopulationBuilder(species)
    builder.setup(
        name=name,
        stochastic=stochastic,
        use_continuous_sampling=use_continuous_sampling,
        use_fixed_egg_count=use_fixed_egg_count
    )
    return builder
reset
reset() -> None

Reset the population to its initial state.

Restores individual counts and sperm storage to original values.

Source code in src/natal/age_structured_population.py
def reset(self) -> None:
    """Reset the population to its initial state.

    Restores individual counts and sperm storage to original values.
    """
    self._tick = 0
    self._history = []
    self._finished = False
    if hasattr(self, '_initial_population_snapshot'):
        ind_copy, sperm_copy, _ = self._initial_population_snapshot

        self._state = PopulationState.create(
            n_genotypes=self._config_nn.n_genotypes,
            n_sexes=self._config_nn.n_sexes,
            n_ages=self._config_nn.n_ages,
            n_tick=0,
            individual_count=ind_copy.copy(),
            sperm_storage=sperm_copy.copy(),
        )
get_total_count
get_total_count() -> int

Return the total number of individuals in the population.

Returns:

Name Type Description
float int

Grand total across all sexes, ages, and genotypes.

Source code in src/natal/age_structured_population.py
def get_total_count(self) -> int:
    """Return the total number of individuals in the population.

    Returns:
        float: Grand total across all sexes, ages, and genotypes.
    """
    return self._state_nn.individual_count.sum()
get_female_count
get_female_count() -> int

Return the total number of female individuals.

Returns:

Name Type Description
float int

Sum of all female individual counts.

Source code in src/natal/age_structured_population.py
def get_female_count(self) -> int:
    """Return the total number of female individuals.

    Returns:
        float: Sum of all female individual counts.
    """
    return self._state_nn.individual_count[Sex.FEMALE.value, :, :].sum()
get_male_count
get_male_count() -> int

Return the total number of male individuals.

Returns:

Name Type Description
float int

Sum of all male individual counts.

Source code in src/natal/age_structured_population.py
def get_male_count(self) -> int:
    """Return the total number of male individuals.

    Returns:
        float: Sum of all male individual counts.
    """
    return self._state_nn.individual_count[Sex.MALE.value, :, :].sum()
get_adult_count
get_adult_count(sex: str = 'both') -> int

Return the number of adult individuals for the given sex.

Parameters:

Name Type Description Default
sex str

One of 'female', 'male', or 'both' (aliases accepted).

'both'

Returns:

Name Type Description
float int

Total number of adults for the requested sex(es).

Raises:

Type Description
ValueError

If the sex identifier is not recognized.

Source code in src/natal/age_structured_population.py
def get_adult_count(self, sex: str = 'both') -> int:
    """Return the number of adult individuals for the given sex.

    Args:
        sex: One of ``'female'``, ``'male'``, or ``'both'`` (aliases accepted).

    Returns:
        float: Total number of adults for the requested sex(es).

    Raises:
        ValueError: If the sex identifier is not recognized.
    """
    if sex not in ('female', 'male', 'both', 'F', 'M'):
        raise ValueError(f"sex must be 'female', 'male', or 'both', got '{sex}'")

    total = 0

    if sex in ('female', 'F', 'both'):
        total += self._state_nn.individual_count[Sex.FEMALE.value, self.new_adult_age:self.n_ages, :].sum()

    if sex in ('male', 'M', 'both'):
        total += self._state_nn.individual_count[Sex.MALE.value, self.new_adult_age:self.n_ages, :].sum()

    return int(total)
export_config
export_config() -> PopulationConfig

Export population configuration to Config jitclass.

Returns:

Name Type Description
PopulationConfig PopulationConfig

A copy of the current population configuration.

Source code in src/natal/age_structured_population.py
def export_config(self) -> 'PopulationConfig':
    """Export population configuration to Config jitclass.

    Returns:
        PopulationConfig: A copy of the current population configuration.
    """
    return self._config_nn
import_config
import_config(config: PopulationConfig) -> None

Import configuration into the population.

Parameters:

Name Type Description Default
config PopulationConfig

Config jitclass instance.

required
Source code in src/natal/age_structured_population.py
def import_config(self, config: 'PopulationConfig') -> None:
    """Import configuration into the population.

    Args:
        config: Config jitclass instance.
    """
    # Configuration is usually read-only (used by run_tick),
    # kept here for completeness.
    self._config = config
create_history_snapshot
create_history_snapshot() -> None

Record current state to history records.

Saves the current tick and a flattened copy of state to _history.

Source code in src/natal/age_structured_population.py
def create_history_snapshot(self) -> None:
    """Record current state to history records.

    Saves the current tick and a flattened copy of state to _history.
    """
    flattened = self._state_nn.flatten_all()
    self._history.append((self._tick, flattened.copy()))
get_history
get_history() -> np.ndarray

Return history records as a 2D NumPy array.

Returns:

Type Description
ndarray

np.ndarray: Float64 array with shape (n_snapshots, 1 + n_sexes*n_ages*n_genotypes + n_ages*n_genotypes^2), where each row is a flattened snapshot state.

Raises:

Type Description
ValueError

If no history is recorded.

Source code in src/natal/age_structured_population.py
def get_history(self) -> np.ndarray:
    """Return history records as a 2D NumPy array.

    Returns:
        np.ndarray: Float64 array with shape
            ``(n_snapshots, 1 + n_sexes*n_ages*n_genotypes + n_ages*n_genotypes^2)``,
            where each row is a flattened snapshot state.

    Raises:
        ValueError: If no history is recorded.
    """
    if len(self._history) == 0:
        raise ValueError("No history recorded")

    # Stack flattened data of all snapshots
    flat_array = np.array([rec[1] for rec in self._history], dtype=np.float64)
    return flat_array
clear_history
clear_history() -> None

Clear history records.

Source code in src/natal/age_structured_population.py
def clear_history(self) -> None:
    """Clear history records."""
    self._history.clear()
export_state
export_state() -> Tuple[NDArray[np.float64], Optional[NDArray[np.float64]]]

Export population state as a flattened array.

Returns:

Type Description
Tuple[NDArray[float64], Optional[NDArray[float64]]]

Tuple[NDArray, Optional[NDArray]]: (state_flat, history). state_flat: [n_tick, ind_count.ravel(), sperm_storage.ravel()] history: Optional array of shape (n_snapshots, flatten_size).

Source code in src/natal/age_structured_population.py
def export_state(self) -> Tuple[NDArray[np.float64], Optional[NDArray[np.float64]]]:
    """Export population state as a flattened array.

    Returns:
        Tuple[NDArray, Optional[NDArray]]: (state_flat, history).
            state_flat: [n_tick, ind_count.ravel(), sperm_storage.ravel()]
            history: Optional array of shape (n_snapshots, flatten_size).
    """
    state_flat = self._state_nn.flatten_all()
    history = self.get_history() if self._history else None
    return state_flat, history
import_state
import_state(state: Union[PopulationState, NDArray[float64], Dict[str, ndarray], Tuple[ndarray, ndarray]], history: Optional[ndarray] = None) -> None

Import state and optional history records.

Parameters:

Name Type Description Default
state Union[PopulationState, NDArray[float64], Dict[str, ndarray], Tuple[ndarray, ndarray]]

Flattened array, PopulationState object, or data dictionary.

required
history Optional[ndarray]

Optional history 2D array.

None
Source code in src/natal/age_structured_population.py
def import_state(self, state: Union['PopulationState', NDArray[np.float64], Dict[str, np.ndarray], Tuple[np.ndarray, np.ndarray]],
                 history: Optional[np.ndarray] = None) -> None:
    """Import state and optional history records.

    Args:
        state: Flattened array, PopulationState object, or data dictionary.
        history: Optional history 2D array.
    """
    from natal.population_state import PopulationState, parse_flattened_state

    if isinstance(state, np.ndarray):
        # Reconstruct state from flattened array
        n_sexes, n_ages, n_genotypes = self._state_nn.individual_count.shape
        state_obj = parse_flattened_state(state, n_sexes, n_ages, n_genotypes)
        self._state_nn.individual_count[:] = state_obj.individual_count
        self._state_nn.sperm_storage[:] = state_obj.sperm_storage
        self._state = PopulationState(
            n_tick=state_obj.n_tick,
            individual_count=self._state_nn.individual_count,
            sperm_storage=self._state_nn.sperm_storage,
        )
    elif isinstance(state, dict):
        self._state_nn.individual_count[:] = state['individual_count']
        self._state_nn.sperm_storage[:] = state['sperm_storage']
    elif isinstance(state, PopulationState):
        self._state_nn.individual_count[:] = state.individual_count
        self._state_nn.sperm_storage[:] = state.sperm_storage
        self._state = PopulationState(
            n_tick=state.n_tick,
            individual_count=self._state_nn.individual_count,
            sperm_storage=self._state_nn.sperm_storage,
        )
    else:
        self._state_nn.individual_count[:] = state[0]
        self._state_nn.sperm_storage[:] = state[1]

    if history is not None and history.shape[0] > 0:
        self.clear_history()
        for row_idx in range(history.shape[0]):
            flat = history[row_idx, :]
            tick = int(flat[0])
            self._history.append((tick, flat.copy()))
get_history_as_objects
get_history_as_objects(indices: Optional[List[int]] = None) -> List[Tuple[int, PopulationState]]

Convert selected flattened snapshots back to PopulationState objects.

Parameters:

Name Type Description Default
indices Optional[List[int]]

List of snapshot indices to convert. If None, converts all.

None

Returns:

Type Description
List[Tuple[int, PopulationState]]

List[Tuple[int, PopulationState]]: List of (tick, state) tuples.

Raises:

Type Description
IndexError

If an index is out of range.

Source code in src/natal/age_structured_population.py
def get_history_as_objects(self, indices: Optional[List[int]] = None) -> List[Tuple[int, PopulationState]]:
    """Convert selected flattened snapshots back to PopulationState objects.

    Args:
        indices: List of snapshot indices to convert. If None, converts all.

    Returns:
        List[Tuple[int, PopulationState]]: List of (tick, state) tuples.

    Raises:
        IndexError: If an index is out of range.
    """
    if indices is None:
        indices = list(range(len(self._history)))

    from natal.population_state import parse_flattened_state
    result: List[Tuple[int, PopulationState]] = []
    for idx in indices:
        if idx < 0 or idx >= len(self._history):
            raise IndexError(f"History index {idx} out of range [0, {len(self._history)})")

        tick, flattened = self._history[idx]
        state = parse_flattened_state(
            flattened,
            n_sexes=2,
            n_ages=self._config_nn.n_ages,
            n_genotypes=len(self._registry_nn.index_to_genotype)
        )
        result.append((tick, state))
    return result
restore_checkpoint
restore_checkpoint(tick: int) -> None

Restore the population to a specific history tick.

Parameters:

Name Type Description Default
tick int

The target tick number.

required

Raises:

Type Description
ValueError

If no record is found for the specified tick.

Source code in src/natal/age_structured_population.py
def restore_checkpoint(self, tick: int) -> None:
    """Restore the population to a specific history tick.

    Args:
        tick: The target tick number.

    Raises:
        ValueError: If no record is found for the specified tick.
    """
    from natal.population_state import parse_flattened_state

    for t, flattened in self._history:
        if t == tick:
            state = parse_flattened_state(
                flattened,
                n_sexes=2,
                n_ages=self._config_nn.n_ages,
                n_genotypes=len(self._registry_nn.index_to_genotype)
            )
            # Copy state data directly.
            self._state_nn.individual_count[:] = state.individual_count
            self._state_nn.sperm_storage[:] = state.sperm_storage
            self._tick = tick
            return

    raise ValueError(f"No history record found for tick {tick}")
run
run(n_steps: int, record_every: Optional[int] = None, finish: bool = False, clear_history_on_start: bool = False) -> AgeStructuredPopulation

Run multi-step evolution using optimized simulation kernels.

Parameters:

Name Type Description Default
n_steps int

Number of steps to evolve.

required
record_every Optional[int]

Interval for recording snapshots. If None, uses self.record_every. If 0, no snapshots are recorded.

None
finish bool

Whether to mark the population as finished after the run.

False
clear_history_on_start bool

Whether to clear existing history before starting.

False

Returns:

Name Type Description
AgeStructuredPopulation AgeStructuredPopulation

Self for chaining.

Raises:

Type Description
RuntimeError

If the population is already finished and cannot continue.

Source code in src/natal/age_structured_population.py
def run(
    self,
    n_steps: int,
    record_every: Optional[int] = None,
    finish: bool = False,
    clear_history_on_start: bool = False
) -> 'AgeStructuredPopulation':
    """Run multi-step evolution using optimized simulation kernels.

    Args:
        n_steps: Number of steps to evolve.
        record_every: Interval for recording snapshots.
            If None, uses self.record_every. If 0, no snapshots are recorded.
        finish: Whether to mark the population as finished after the run.
        clear_history_on_start: Whether to clear existing history before starting.

    Returns:
        AgeStructuredPopulation: Self for chaining.

    Raises:
        RuntimeError: If the population is already finished and cannot continue.
    """

    if self._finished:
        raise RuntimeError(
            f"Population '{self.name}' has finished. "
            "Cannot run() again after finish=True."
        )

    if record_every is None:
        record_every = self.record_every

    if self.should_use_python_dispatch():
        return self._run_python_dispatch(
            n_steps=n_steps,
            record_every=record_every,
            finish=finish,
            clear_history_on_start=clear_history_on_start,
        )

    config = sk.export_config(self)

    hooks = self.get_compiled_event_hooks()

    assert hooks.registry is not None, "hooks.registry should always be initialized"

    obs_mask = self._observation_mask
    n_obs = len(self._observation.labels) if self._observation is not None else 0

    if hooks.run_fn is not None:
        final_state_tuple, history_new, was_stopped = hooks.run_fn(
            state=self._state_nn,
            config=config,
            registry=hooks.registry,
            n_ticks=n_steps,
            record_interval=record_every,
            observation_mask=obs_mask,
            n_obs_groups=n_obs,
        )
    else:
        from natal.kernels.simulation_kernels import run_with_hooks

        final_state_tuple, history_new, was_stopped = run_with_hooks(
            state=self._state_nn,
            config=config,
            registry=hooks.registry,
            first_hook=hooks.first,
            early_hook=hooks.early,
            late_hook=hooks.late,
            n_ticks=n_steps,
            record_interval=record_every,
            observation_mask=obs_mask,
            n_obs_groups=n_obs,
        )

    # Process final state (tuple format: ind_count, sperm, tick)
    self._state = PopulationState(
        n_tick=int(final_state_tuple[2]),
        individual_count=final_state_tuple[0],
        sperm_storage=final_state_tuple[1],
    )
    self._tick = int(final_state_tuple[2])

    # history_new is a 2D NDArray (n_snapshots, history_size)
    self._process_kernel_history(history_new, clear_history_on_start)

    # If terminated early by hooks, set _finished flag
    if was_stopped:
        self._finished = True
        self.trigger_event("finish")
    elif finish:
        # Otherwise, if finish parameter is True, actively trigger finish
        self.finish_simulation()

    return self
run_tick
run_tick() -> AgeStructuredPopulation

Execute a single tick of evolution.

Returns:

Name Type Description
AgeStructuredPopulation AgeStructuredPopulation

Self for chaining.

Raises:

Type Description
RuntimeError

If the population is already finished and cannot continue.

Source code in src/natal/age_structured_population.py
def run_tick(self) -> 'AgeStructuredPopulation':
    """
    Execute a single tick of evolution.

    Returns:
        AgeStructuredPopulation: Self for chaining.

    Raises:
        RuntimeError: If the population is already finished and cannot continue.
    """
    return self.run(n_steps=1, record_every=self.record_every, clear_history_on_start=False)
get_age_distribution
get_age_distribution(sex: str = 'both') -> np.ndarray

Return the age distribution for the requested sex.

Parameters:

Name Type Description Default
sex str

One of 'female', 'male', or 'both'.

'both'

Returns:

Type Description
ndarray

NDArray[np.float64]: Age distribution array with shape (n_ages,).

Raises:

Type Description
ValueError

If sex identifier is invalid.

Source code in src/natal/age_structured_population.py
def get_age_distribution(self, sex: str = 'both') -> np.ndarray:
    """Return the age distribution for the requested sex.

    Args:
        sex: One of ``'female'``, ``'male'``, or ``'both'``.

    Returns:
        NDArray[np.float64]: Age distribution array with shape (n_ages,).

    Raises:
        ValueError: If sex identifier is invalid.
    """
    if sex not in ('female', 'male', 'both', 'F', 'M'):
        raise ValueError(f"sex must be 'female', 'male', or 'both', got '{sex}'")

    # Access directly from PopulationState
    if sex in ('female', 'F'):
        return self._state_nn.individual_count[Sex.FEMALE.value, :, :].sum(axis=1)
    elif sex in ('male', 'M'):
        return self._state_nn.individual_count[Sex.MALE.value, :, :].sum(axis=1)
    else:
        return self._state_nn.individual_count.sum(axis=(0, 2))
get_genotype_count
get_genotype_count(genotype: Genotype) -> Tuple[int, int]

Return total counts for a genotype as (female_count, male_count).

Parameters:

Name Type Description Default
genotype Genotype

Target genotype instance.

required

Returns:

Type Description
Tuple[int, int]

Tuple[int,int]: (female_count, male_count) across all ages.

Source code in src/natal/age_structured_population.py
def get_genotype_count(self, genotype: Genotype) -> Tuple[int, int]:
    """Return total counts for a genotype as (female_count, male_count).

    Args:
        genotype: Target genotype instance.

    Returns:
        Tuple[int,int]: ``(female_count, male_count)`` across all ages.
    """
    genotype_idx = self._registry_nn.genotype_to_index[genotype]
    female_count = self._state_nn.individual_count[Sex.FEMALE.value, :, genotype_idx].sum()
    male_count = self._state_nn.individual_count[Sex.MALE.value, :, genotype_idx].sum()
    return (female_count, male_count)