Skip to content

genetic_presets Module

Built-in genetic presets and preset application helpers.

Overview

The genetic_presets module provides reusable preset abstractions and concrete implementations (for example, gene drives) that can inject gamete modifiers, zygote modifiers, and fitness patches into populations.

Complete Module Reference

natal.genetic_presets

Genetic presets for mutations, gene drives, and allele conversions.

This module provides a framework for defining reusable genetic modifications including: - Gene drives (e.g., CRISPR/Cas9 homing drives) - General mutations (point mutations, insertions, deletions) - Allele conversion rules and fitness effects - Complex genetic constructions

Each preset can modify population genetics through three mechanisms: - gamete_modifier: modifies gamete frequencies during gametogenesis - zygote_modifier: modifies embryo genotypes after fertilization - fitness_patch: declarative specification of viability/fecundity/sexual selection effects

GeneticPreset

GeneticPreset(name: str = '', species: Optional[Species] = None)

Bases: ABC

Abstract base for genetic modification presets including gene drives, mutations, and allele conversions.

A preset bundles gamete modifiers, zygote modifiers, and fitness effects that form a cohesive genetic system. This can include: - Gene drives (e.g., CRISPR/Cas9 homing drives) - General mutations (point mutations, insertions, deletions) - Complex allele conversion systems

Presets should implement
  • gamete_modifier(): returns GameteModifier callable or None
  • zygote_modifier(): returns ZygoteModifier callable or None
  • fitness_patch(): returns declarative fitness configuration dict or None

All methods are optional (can return None). At least one method should be implemented for the preset to have any effect.

Examples:

>>> population.apply_preset(preset)

Attributes:

Name Type Description
name str

Human-readable preset name.

hook_id Optional[int]

Optional identifier used when registering modifiers.

Initialize the preset.

Parameters:

Name Type Description Default
name str

Optional human-readable name for the preset.

''
species Optional[Species]

Optional species bound at construction time. If provided, applying this preset to a population with a different species will raise an error.

None
Source code in src/natal/genetic_presets.py
def __init__(
    self,
    name: str = "",
    species: Optional[Species] = None,
):
    """Initialize the preset.

    Args:
        name: Optional human-readable name for the preset.
        species: Optional species bound at construction time. If provided,
            applying this preset to a population with a different species
            will raise an error.
    """
    self.name = name or self.__class__.__name__
    self.hook_id: Optional[int] = None
    self._bound_species: Optional[Species] = species
bind_species
bind_species(species: Species) -> None

Bind this preset instance to a concrete species.

This enables delayed species injection: users can construct presets without passing species, and binding happens automatically when the preset is applied to a population.

Source code in src/natal/genetic_presets.py
def bind_species(self, species: Species) -> None:
    """Bind this preset instance to a concrete species.

    This enables delayed species injection: users can construct presets
    without passing species, and binding happens automatically when the
    preset is applied to a population.
    """
    if self._bound_species is None:
        self._bound_species = species
        return

    if self._bound_species is species:
        return

    raise ValueError(
        f"Preset '{self.name}' is already bound to species "
        f"'{self._bound_species.name}' and cannot be applied to population species '{species.name}'."
    )
gamete_modifier abstractmethod
gamete_modifier(population: BasePopulation[Any]) -> Optional[GameteModifier]

Return a gamete modifier or None.

The modifier should return:

Dict[(sex_idx, genotype_idx) -> Dict[compressed_hg_glab_idx -> freq]]

where compressed_hg_glab_idx is an integer index into the compressed haploid genotype space.

Source code in src/natal/genetic_presets.py
@abstractmethod
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Return a gamete modifier or None.

    The modifier should return:

        Dict[(sex_idx, genotype_idx) -> Dict[compressed_hg_glab_idx -> freq]]

    where compressed_hg_glab_idx is an integer index into the compressed
    haploid genotype space.
    """
    return None
zygote_modifier abstractmethod
zygote_modifier(population: BasePopulation[Any]) -> Optional[ZygoteModifier]

Return a zygote modifier or None.

The modifier should return:

Dict[(c1, c2) -> (idx_modified | Genotype | Dict[idx -> prob])]

where c1, c2 are compressed coordinate pairs representing the parental diploid genotypes.

Source code in src/natal/genetic_presets.py
@abstractmethod
def zygote_modifier(self, population: 'BasePopulation[Any]') -> Optional[ZygoteModifier]:
    """Return a zygote modifier or None.

    The modifier should return:

        Dict[(c1, c2) -> (idx_modified | Genotype | Dict[idx -> prob])]

    where c1, c2 are compressed coordinate pairs representing the parental
    diploid genotypes.
    """
    return None
fitness_patch
fitness_patch() -> Optional[PresetFitnessPatch]

Return declarative fitness patch.

Returns:

Type Description
Optional[PresetFitnessPatch]

Fitness patch from custom function if set, otherwise None.

Optional[PresetFitnessPatch]

Subclasses should override this method for built-in behavior.

Source code in src/natal/genetic_presets.py
def fitness_patch(self) -> Optional[PresetFitnessPatch]:
    """Return declarative fitness patch.

    Returns:
        Fitness patch from custom function if set, otherwise None.
        Subclasses should override this method for built-in behavior.
    """
    if self._custom_fitness_patch is not None:
        return self._custom_fitness_patch()
    return None
with_fitness_patch
with_fitness_patch(patch_func: Callable[[], Optional[PresetFitnessPatch]]) -> Self

Set a custom fitness patch function and return self for chaining.

This allows dynamic modification of fitness effects at runtime without subclassing, using a fluent interface.

Parameters:

Name Type Description Default
patch_func Callable[[], Optional[PresetFitnessPatch]]

Callable that returns a PresetFitnessPatch or None.

required

Returns:

Type Description
Self

Self for method chaining.

Example

preset = (HomingDrive(...) ... .with_fitness_patch(lambda: { ... 'viability_allele': {'Drive': (0.8, 'dominant')} ... })) population.apply_preset(preset)

Also works with complex custom logic

def conditional_patch(): ... if some_condition: ... return {'fecundity_allele': {'Mut': (0.5, 'recessive')}} ... return None

preset = HomingDrive(...).with_fitness_patch(conditional_patch)

Note

This overrides any fitness patch defined in subclasses. To preserve subclass behavior while adding modifications, subclass and call super().fitness_patch() instead.

Source code in src/natal/genetic_presets.py
def with_fitness_patch(
    self,
    patch_func: Callable[[], Optional[PresetFitnessPatch]]
) -> Self:
    """Set a custom fitness patch function and return self for chaining.

    This allows dynamic modification of fitness effects at runtime
    without subclassing, using a fluent interface.

    Args:
        patch_func: Callable that returns a PresetFitnessPatch or None.

    Returns:
        Self for method chaining.

    Example:
        >>> preset = (HomingDrive(...)
        ...     .with_fitness_patch(lambda: {
        ...         'viability_allele': {'Drive': (0.8, 'dominant')}
        ...     }))
        >>> population.apply_preset(preset)

        >>> # Also works with complex custom logic
        >>> def conditional_patch():
        ...     if some_condition:
        ...         return {'fecundity_allele': {'Mut': (0.5, 'recessive')}}
        ...     return None
        >>>
        >>> preset = HomingDrive(...).with_fitness_patch(conditional_patch)

    Note:
        This overrides any fitness patch defined in subclasses.
        To preserve subclass behavior while adding modifications,
        subclass and call super().fitness_patch() instead.
    """
    if not callable(patch_func):
        raise TypeError(f"patch_func must be callable, got {type(patch_func)}")
    self._custom_fitness_patch = patch_func
    return self
clear_fitness_patch
clear_fitness_patch() -> GeneticPreset

Remove any custom fitness patch, restoring default behavior.

Returns:

Type Description
GeneticPreset

Self for method chaining.

Source code in src/natal/genetic_presets.py
def clear_fitness_patch(self) -> 'GeneticPreset':
    """Remove any custom fitness patch, restoring default behavior.

    Returns:
        Self for method chaining.
    """
    self._custom_fitness_patch = None
    return self
apply
apply(population: BasePopulation[Any]) -> None

Register this preset onto a population (DEPRECATED).

.. deprecated:: Use population.apply_preset(preset) instead. This method is kept for backwards compatibility and may be removed in future versions.

Parameters:

Name Type Description Default
population BasePopulation[Any]

The BasePopulation instance to modify.

required
See Also

:meth:natal.base_population.BasePopulation.apply_preset - Preferred modern API

Source code in src/natal/genetic_presets.py
def apply(self, population: 'BasePopulation[Any]') -> None:
    """Register this preset onto a population (DEPRECATED).

    .. deprecated::
        Use population.apply_preset(preset) instead.
        This method is kept for backwards compatibility and may be removed in future versions.

    Args:
        population: The BasePopulation instance to modify.

    See Also:
        :meth:`natal.base_population.BasePopulation.apply_preset` - Preferred modern API
    """
    apply_preset_to_population(population, self)

HomingDrive

HomingDrive(name: str, drive_allele: _AlleleSpecifier, target_allele: _AlleleSpecifier, resistance_allele: Optional[_AlleleSpecifier] = None, functional_resistance_allele: Optional[_AlleleSpecifier] = None, cas9_allele: Optional[_AlleleSpecifier] = None, drive_conversion_rate: _SexSpecificRates = 0.5, late_germline_resistance_formation_rate: _SexSpecificRates = 0.0, embryo_resistance_formation_rate: _SexSpecificRates = 0.0, functional_resistance_ratio: float = 0.0, fecundity_scaling: _FecundityScalingConfig = 1.0, viability_scaling: _ViabilityScalingConfig = 1.0, sexual_selection_scaling: _SexualSelectionScalingConfig = 1.0, zygote_viability_scaling: _ZygoteViabilityScalingConfig = 1.0, viability_mode: _AlleleScalingMode = 'multiplicative', fecundity_mode: _AlleleScalingMode = 'multiplicative', sexual_selection_mode: _AlleleScalingMode = 'multiplicative', zygote_viability_mode: _AlleleScalingMode = 'multiplicative', cas9_deposition_glab: Optional[str] = None, species: Optional[Species] = None, use_paternal_deposition: bool = False)

Bases: GeneticPreset

Homing-based gene drive (e.g., CRISPR/Cas9 homing drives).

This preset implements a homing gene drive that spreads through homology-directed repair (HDR) converting wild-type alleles into drive alleles in heterozygotes. It can also generate resistance alleles through non-homologous end joining (NHEJ).

Key features include drive conversion in heterozygotes, germline/embryo resistance formation, optional parental Cas9 deposition, and sex-specific rate control.

The drive operates through a sequential cascade: 1. Homing conversion (WT -> Drive) 2. Resistance formation in remaining WT alleles 3. Optional functional resistance split

Attributes:

Name Type Description
drive_conversion_rate Tuple[float, float]

Female/male homing rates.

late_germline_resistance_formation_rate Tuple[float, float]

Female/male late germline resistance rates.

embryo_resistance_formation_rate Tuple[float, float]

Female/male embryo resistance rates.

Examples:

drive = HomingDrive( name="MyDrive", drive_allele="Drive", target_allele="WT", resistance_allele="Resistance", drive_conversion_rate=0.95, late_germline_resistance_formation_rate=0.03 ) population.apply_preset(drive)

Initialize a homing-based gene drive (e.g., CRISPR/Cas9 homing drives).

This drive spreads via homology-directed repair (HDR) converting wild-type alleles into drive alleles in heterozygotes. It can also generate resistance alleles through non-homologous end joining (NHEJ).

Parameters:

Name Type Description Default
name str

Name of the gene drive.

required
drive_allele str or Gene

The allele carrying the drive cassette.

required
target_allele str or Gene

The wild-type allele targeted by the drive.

required
resistance_allele str or Gene

The non-functional resistance allele formed by NHEJ.

None
functional_resistance_allele str or Gene

The functional resistance allele formed by in-frame NHEJ. If not provided, assume no functional resistance.

None
cas9_allele str or Gene

The allele carrying Cas9 for cleavage, used when modeling a split drive where Cas9 is separate from the drive locus.

None
drive_conversion_rate float or dict

Probability of drive conversion caused by Cas9 cleavage and homology-directed repair in heterozygotes. Can be a single float (applies to both sexes), a dict with sex keys, or a tuple (female_rate, male_rate) for sex-specific rates.

0.5
late_germline_resistance_formation_rate float or dict

Probability of resistance formation after drive conversion in the germline. Can be a single float (applies to both sexes), a dict with sex keys, or a tuple (female_rate, male_rate) for sex-specific rates.

0.0
embryo_resistance_formation_rate float or dict

Probability of resistance formation in embryos due to maternal/paternal Cas9 deposition. Can be a single float, dict, or tuple.

0.0
functional_resistance_ratio float

Proportion of resistance alleles that are functional (in-frame mutations). Range: 0.0 (all non-functional) to 1.0 (all functional).

0.0
fecundity_scaling float or dict

Fitness multiplier for drive carriers affecting fecundity. Applied multiplicatively based on allele copy number.

1.0
viability_scaling float or dict

Fitness multiplier for drive carriers affecting viability. Applied multiplicatively based on allele copy number.

1.0
sexual_selection_scaling float or tuple

Fitness multiplier affecting sexual selection. Can be a single float or tuple (default_selection, carrier_selection).

1.0
zygote_viability_scaling float or dict

Fitness multiplier affecting survival of zygotes before competition takes place. Applied multiplicatively based on allele copy number.

1.0
viability_mode str

Scaling mode: "multiplicative", "dominant", "recessive", or "custom". If "custom", scaling values must be tuples (het_val, hom_val).

'multiplicative'
fecundity_mode str

Scaling mode: "multiplicative", "dominant", "recessive", or "custom". If "custom", scaling values must be tuples (het_val, hom_val).

'multiplicative'
sexual_selection_mode str

Scaling mode for scalar sexual_selection_scaling. Note: if sexual_selection_scaling is a tuple, mode is ignored.

'multiplicative'
zygote_viability_mode str

Scaling mode: "multiplicative", "dominant", "recessive", or "custom". If "custom", scaling values must be tuples (het_val, hom_val).

'multiplicative'
cas9_deposition_glab str

Gamete label for Cas9 deposition tracking. Used for maternal/paternal effect modeling.

None
species Species

Species to bind at construction time. If None, will be bound when applied to population.

None
use_paternal_deposition bool

Whether to enable paternal Cas9 deposition. If True, fathers can deposit Cas9 in embryos.

False

Examples:

>>> drive = HomingDrive(
...     name="MyDrive",
...     drive_allele="Drive",
...     target_allele="WT",
...     resistance_allele="R2",
...     drive_conversion_rate=0.95,
...     late_germline_resistance_formation_rate=0.03
... )
>>> population.apply_preset(drive)
Source code in src/natal/genetic_presets.py
def __init__(
    self,
    name: str,
    drive_allele: _AlleleSpecifier,
    target_allele: _AlleleSpecifier,
    resistance_allele: Optional[_AlleleSpecifier] = None,
    functional_resistance_allele: Optional[_AlleleSpecifier] = None,
    cas9_allele: Optional[_AlleleSpecifier] = None,
    drive_conversion_rate: _SexSpecificRates = 0.5,
    late_germline_resistance_formation_rate: _SexSpecificRates = 0.0,
    embryo_resistance_formation_rate: _SexSpecificRates = 0.0,
    functional_resistance_ratio: float = 0.0,
    fecundity_scaling: _FecundityScalingConfig = 1.0,
    viability_scaling: _ViabilityScalingConfig = 1.0,
    sexual_selection_scaling: _SexualSelectionScalingConfig = 1.0,
    zygote_viability_scaling: _ZygoteViabilityScalingConfig = 1.0,
    viability_mode: _AlleleScalingMode = "multiplicative",
    fecundity_mode: _AlleleScalingMode = "multiplicative",
    sexual_selection_mode: _AlleleScalingMode = "multiplicative",
    zygote_viability_mode: _AlleleScalingMode = "multiplicative",
    cas9_deposition_glab: Optional[str] = None,
    species: Optional[Species] = None,
    use_paternal_deposition: bool = False,
):
    """Initialize a homing-based gene drive (e.g., CRISPR/Cas9 homing drives).

    This drive spreads via homology-directed repair (HDR) converting wild-type alleles into drive alleles in heterozygotes.
    It can also generate resistance alleles through non-homologous end joining (NHEJ).

    Args:
        name (str): Name of the gene drive.
        drive_allele (str or Gene): The allele carrying the drive cassette.
        target_allele (str or Gene): The wild-type allele targeted by the drive.
        resistance_allele (str or Gene, optional): The non-functional resistance allele formed by NHEJ.
        functional_resistance_allele (str or Gene, optional): The functional resistance allele
            formed by in-frame NHEJ. If not provided, assume no functional resistance.
        cas9_allele (str or Gene, optional): The allele carrying Cas9 for cleavage, used
            when modeling a split drive where Cas9 is separate from the drive locus.
        drive_conversion_rate (float or dict): Probability of drive conversion caused by Cas9 cleavage
            and homology-directed repair in heterozygotes. Can be a single float (applies to both sexes),
            a dict with sex keys, or a tuple (female_rate, male_rate) for sex-specific rates.
        late_germline_resistance_formation_rate (float or dict): Probability of resistance formation
            *after* drive conversion in the germline. Can be a single float (applies to both sexes),
            a dict with sex keys, or a tuple (female_rate, male_rate) for sex-specific rates.
        embryo_resistance_formation_rate (float or dict): Probability of resistance formation
            in embryos due to maternal/paternal Cas9 deposition. Can be a single float, dict, or tuple.
        functional_resistance_ratio (float): Proportion of resistance alleles that are functional
            (in-frame mutations). Range: 0.0 (all non-functional) to 1.0 (all functional).
        fecundity_scaling (float or dict): Fitness multiplier for drive carriers affecting fecundity.
            Applied multiplicatively based on allele copy number.
        viability_scaling (float or dict): Fitness multiplier for drive carriers affecting viability.
            Applied multiplicatively based on allele copy number.
        sexual_selection_scaling (float or tuple): Fitness multiplier affecting sexual selection.
            Can be a single float or tuple (default_selection, carrier_selection).
        zygote_viability_scaling (float or dict): Fitness multiplier affecting survival of zygotes before
            competition takes place. Applied multiplicatively based on allele copy number.
        viability_mode (str): Scaling mode: "multiplicative", "dominant", "recessive", or "custom".
            If "custom", scaling values must be tuples (het_val, hom_val).
        fecundity_mode (str): Scaling mode: "multiplicative", "dominant", "recessive", or "custom".
            If "custom", scaling values must be tuples (het_val, hom_val).
        sexual_selection_mode (str): Scaling mode for scalar sexual_selection_scaling.
            Note: if sexual_selection_scaling is a tuple, mode is ignored.
        zygote_viability_mode (str): Scaling mode: "multiplicative", "dominant", "recessive", or "custom".
            If "custom", scaling values must be tuples (het_val, hom_val).
        cas9_deposition_glab (str, optional): Gamete label for Cas9 deposition tracking.
            Used for maternal/paternal effect modeling.
        species (Species, optional): Species to bind at construction time. If None,
            will be bound when applied to population.
        use_paternal_deposition (bool): Whether to enable paternal Cas9 deposition.
            If True, fathers can deposit Cas9 in embryos.

    Examples:
        >>> drive = HomingDrive(
        ...     name="MyDrive",
        ...     drive_allele="Drive",
        ...     target_allele="WT",
        ...     resistance_allele="R2",
        ...     drive_conversion_rate=0.95,
        ...     late_germline_resistance_formation_rate=0.03
        ... )
        >>> population.apply_preset(drive)
    """
    self._str_drive_allele = self._resolve_allele_name(drive_allele)
    self._str_target_allele = self._resolve_allele_name(target_allele)
    self._str_resistance_allele = (self._resolve_allele_name(resistance_allele)
        if resistance_allele else None)
    self._str_functional_resistance_allele = (self._resolve_allele_name(functional_resistance_allele)
        if functional_resistance_allele else None)
    self._str_cas9_allele = self._resolve_allele_name(cas9_allele) if cas9_allele else None

    self.drive_conversion_rate = self._resolve_rates(drive_conversion_rate)
    self.late_germline_resistance_formation_rate = self._resolve_rates(late_germline_resistance_formation_rate)
    self.embryo_resistance_formation_rate = self._resolve_rates(embryo_resistance_formation_rate)
    self.functional_resistance_ratio = float(functional_resistance_ratio)

    # Store declarative fitness scaling configs.
    self.fecundity_scaling = fecundity_scaling
    self.viability_scaling = viability_scaling
    self.sexual_selection_scaling = sexual_selection_scaling
    self.zygote_viability_scaling = zygote_viability_scaling

    self.viability_mode: _AlleleScalingMode = viability_mode
    self.fecundity_mode: _AlleleScalingMode = fecundity_mode
    self.sexual_selection_mode: _AlleleScalingMode = sexual_selection_mode
    self.zygote_viability_mode: _AlleleScalingMode = zygote_viability_mode

    self.cas9_deposition_glab = str(cas9_deposition_glab) if cas9_deposition_glab else None
    self.use_paternal_deposition = bool(use_paternal_deposition)

    super().__init__(name=name, species=species)
fitness_patch
fitness_patch() -> PresetFitnessPatch

Return declarative fitness patch for homing drive scaling configs.

Source code in src/natal/genetic_presets.py
def fitness_patch(self) -> PresetFitnessPatch:
    """Return declarative fitness patch for homing drive scaling configs."""
    # Combine drive and non-functional resistance alleles into a single group.
    # This ensures that a "Drive|Resistance" genotype is treated as having
    # 2 copies of the "disrupted" allele class, which is crucial for correct
    # dominant/recessive scaling logic.
    alleles = [self._str_drive_allele]
    if self._str_resistance_allele:
        alleles.append(self._str_resistance_allele)

    patch = _make_fitness_patch_given_allele_scaling(
        alleles,
        self.viability_scaling,
        self.fecundity_scaling,
        self.sexual_selection_scaling,
        self.zygote_viability_scaling,
        self.viability_mode,
        self.fecundity_mode,
        self.sexual_selection_mode,
        self.zygote_viability_mode,
    )

    return patch
gamete_modifier
gamete_modifier(population: BasePopulation[Any]) -> Optional[GameteModifier]

Implement homing in heterozygous parents, germline resistance, and Cas9 deposition.

In heterozygotes (drive/wild-type), gametes are biased towards drive.

Source code in src/natal/genetic_presets.py
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Implement homing in heterozygous parents, germline resistance, and Cas9 deposition.

    In heterozygotes (drive/wild-type), gametes are biased towards drive.
    """
    def drive_carrier_filter(gt: Genotype) -> bool:
        from natal.genetic_presets import count_allele_copies

        has_drive = count_allele_copies(gt, self.drive_allele) > 0
        if self.cas9_allele:
            has_cas9 = count_allele_copies(gt, self.cas9_allele) > 0
            return has_drive and has_cas9
        return has_drive

    # RuleSet compiles these rules into a Sequential Cascade.
    # This means the target pool shrinks after every rule.
    # So Rule 2 (Resistance) only acts on the targets that FAILED Rule 1 (Homing).
    rule_set = GameteConversionRuleSet(f"{self.name}_Homing")
    for sex in (Sex.FEMALE, Sex.MALE):
        homing_rate = self.drive_conversion_rate[sex]
        res_rate = self.late_germline_resistance_formation_rate[sex]

        # 1. Homing (Target -> Drive)
        # Examples: If homing_rate is 0.7, 70% of targets become Drive. 30% pass to the next rule.
        if homing_rate > 0:
            rule_set.add_allele_convert(
                from_allele=self.target_allele,
                to_allele=self.drive_allele,
                rate=homing_rate,
                sex_filter=sex,
                genotype_filter=drive_carrier_filter,
            )

        # 2. Germline Resistance (Target -> Resistance)
        # This operates ON THE REMAINDER of the target alleles (e.g. the 30% that survived Homing).
        if res_rate > 0:
            if self.functional_resistance_allele and self.functional_resistance_ratio > 0:
                # 2a. Functional resistance
                # Applying absolute `res_rate * func_res_ratio` directly works because GameteAlleleConversionRule
                # calculates rates against the *current* target pool. So if 30% targets are left, and this
                # rate is 0.1, it converts 10% of that 30% (overall 3% of origin).
                rule_set.add_allele_convert(
                    from_allele=self.target_allele,
                    to_allele=self.functional_resistance_allele,
                    rate=res_rate * self.functional_resistance_ratio,
                    sex_filter=sex,
                    genotype_filter=drive_carrier_filter,
                )

                # 2b. Non-functional resistance
                # The functional rule above removed `res_rate * func_res_ratio` from the available targets.
                # To hit the correct math for the *remaining* non-functional portion, we divide the
                # non-functional rate by whatever remains of the target pool after the functional edits.
                target_remaining = 1.0 - (res_rate * self.functional_resistance_ratio)
                adjusted_nf_rate = ((res_rate * (1.0 - self.functional_resistance_ratio))
                                    / target_remaining) if target_remaining > 0 else 0.0
                if adjusted_nf_rate > 0:
                    rule_set.add_allele_convert(
                        from_allele=self.target_allele,
                        to_allele=self.resistance_genotype,
                        rate=adjusted_nf_rate,
                        sex_filter=sex,
                        genotype_filter=drive_carrier_filter,
                    )
            else:
                # Generic resistance (no functional/non-functional split)
                rule_set.add_allele_convert(
                    from_allele=self.target_allele,
                    to_allele=self.resistance_genotype,
                    rate=res_rate,
                    sex_filter=sex,
                    genotype_filter=drive_carrier_filter,
                )

        # 3. Gamete labeling for maternal Cas9 deposition
        # Instead of editing alleles, this tags the entire output gamete from drive-carrying females
        # with `cas9_deposition_glab`. The zygote modifier will read this tag to apply embryo resistance.
        if sex == Sex.FEMALE or self.use_paternal_deposition:
            rule_set.add_hg_convert(
                hg_match=lambda hg: True,
                to_haploid_genotype=lambda hg: hg,
                rate=1.0,
                sex_filter=sex,
                genotype_filter=drive_carrier_filter,
                target_glab=self.cas9_deposition_glab
            )

    return rule_set.to_gamete_modifier(population) if rule_set.rules else None
zygote_modifier
zygote_modifier(population: BasePopulation[Any]) -> Optional[ZygoteModifier]

Implement embryo resistance.

Cleavage in the embryo (due to deposited Cas9 or zygotic expression) converts wild-type alleles into resistance alleles.

Source code in src/natal/genetic_presets.py
def zygote_modifier(self, population: 'BasePopulation[Any]') -> Optional[ZygoteModifier]:
    """Implement embryo resistance.

    Cleavage in the embryo (due to deposited Cas9 or zygotic expression)
    converts wild-type alleles into resistance alleles.
    """
    rule_set = ZygoteConversionRuleSet(f"{self.name}_EmbryoResistance")

    def zygote_has_cas9(gt: Genotype) -> bool:
        """Check if the zygote itself carries the Cas9 source (somatic cleavage)."""
        from natal.genetic_presets import count_allele_copies

        target = self.cas9_allele if self.cas9_allele else self.drive_allele
        return count_allele_copies(gt, target) > 0

    for sex in (Sex.FEMALE, Sex.MALE):
        rate = self.embryo_resistance_formation_rate[sex]
        if rate > 0:
            m_glab = None
            p_glab = None
            g_filter = None

            if self.cas9_deposition_glab:
                # Label-based deposition (Maternal/Paternal effect)
                if sex == Sex.FEMALE:
                    m_glab = self.cas9_deposition_glab
                elif self.use_paternal_deposition:
                    p_glab = self.cas9_deposition_glab
                else:
                    # Male rate > 0 but no paternal deposition -> somatic/zygotic expression
                    g_filter = zygote_has_cas9
            else:
                # No labels provided -> cleavage depends on zygote's own Cas9 alleles
                g_filter = zygote_has_cas9

            # Skip if no filter is active to avoid global mutation bug
            if m_glab is None and p_glab is None and g_filter is None:
                continue

            func_res_ratio = self.functional_resistance_ratio
            if self.functional_resistance_allele and func_res_ratio > 0:
                # 1. Functional resistance
                rule_set.add_allele_convert(
                    from_allele=self.target_allele,
                    to_allele=self.functional_resistance_allele,
                    rate=rate * func_res_ratio,
                    maternal_glab=m_glab,
                    paternal_glab=p_glab,
                    genotype_filter=g_filter,
                )
                # 2. Non-functional resistance on remaining targets
                target_remaining = 1.0 - (rate * func_res_ratio)
                nf_rate = (rate * (1.0 - func_res_ratio)) / target_remaining if target_remaining > 0 else 0.0
                if nf_rate > 0:
                    rule_set.add_allele_convert(
                        from_allele=self.target_allele,
                        to_allele=self.resistance_genotype,
                        rate=nf_rate,
                        maternal_glab=m_glab,
                        paternal_glab=p_glab,
                        genotype_filter=g_filter,
                    )
            else:
                # Generic resistance (no functional split)
                rule_set.add_allele_convert(
                    from_allele=self.target_allele,
                    to_allele=self.resistance_genotype,
                    rate=rate,
                    maternal_glab=m_glab,
                    paternal_glab=p_glab,
                    genotype_filter=g_filter,
                )

    return rule_set.to_zygote_modifier(population) if rule_set.rules else None

ToxinAntidoteDrive

ToxinAntidoteDrive(name: str, drive_allele: _AlleleSpecifier, target_allele: _AlleleSpecifier, disrupted_allele: _AlleleSpecifier, conversion_rate: _SexSpecificRates = 0.8, embryo_disruption_rate: _SexSpecificRates = 0.0, viability_scaling: _ViabilityScalingConfig = 1.0, fecundity_scaling: _FecundityScalingConfig = 1.0, sexual_selection_scaling: Optional[_SexualSelectionScalingConfig] = None, zygote_viability_scaling: _ZygoteViabilityScalingConfig = 0.0, viability_mode: _AlleleScalingMode = 'recessive', fecundity_mode: _AlleleScalingMode = 'recessive', sexual_selection_mode: _AlleleScalingMode = 'recessive', zygote_viability_mode: _AlleleScalingMode = 'recessive', cas9_deposition_glab: Optional[str] = None, species: Optional[Species] = None, use_paternal_deposition: bool = False)

Bases: GeneticPreset

Toxin-Antidote gene drive (e.g., TARE, TADE).

This preset implements a toxin-antidote gene drive system where a "drive" allele disrupts a "target" allele into a "disrupted" version. The "disrupted" allele typically carries a high fitness cost (the toxin effect), while the "drive" allele itself often provides a functional rescue (the antidote).

Key features include germline disruption, embryo disruption, and configurable fitness costs for the disrupted allele.

In a typical TARE (Toxin-Antidote Recessive Embryo lethality) configuration, the disrupted allele is set to be recessive lethal (viability_scaling=0.0, viability_mode="recessive").

Attributes:

Name Type Description
conversion_rate Tuple[float, float]

Female/male germline disruption rates.

embryo_disruption_rate Tuple[float, float]

Female/male embryo disruption rates.

viability_mode _AlleleScalingMode

Scaling mode for viability effects.

fecundity_mode _AlleleScalingMode

Scaling mode for fecundity effects.

Initialize a toxin-antidote gene drive.

Parameters:

Name Type Description Default
name str

Name of the gene drive.

required
drive_allele _AlleleSpecifier

The allele carrying the antidote and disruption machinery.

required
target_allele _AlleleSpecifier

The wild-type allele targeted for disruption.

required
disrupted_allele _AlleleSpecifier

The resulting non-functional/disrupted allele.

required
conversion_rate _SexSpecificRates

Probability of target disruption in the germline.

0.8
embryo_disruption_rate _SexSpecificRates

Probability of target disruption in embryos.

0.0
viability_scaling _ViabilityScalingConfig

Fitness multiplier for the disrupted allele.

1.0
fecundity_scaling _FecundityScalingConfig

Fecundity multiplier for the disrupted allele.

1.0
sexual_selection_scaling Optional[_SexualSelectionScalingConfig]

Optional sexual-selection multiplier for the disrupted allele. Supports a scalar copy-number effect or a tuple (default_male, carrier_male).

None
zygote_viability_scaling _ZygoteViabilityScalingConfig

Zygote viability scaling configuration for the disrupted allele. Applied during reproduction stage before survival; represents probability that a zygote survives to become an individual.

0.0
viability_mode _AlleleScalingMode

Scaling mode for viability (default "recessive").

'recessive'
fecundity_mode _AlleleScalingMode

Scaling mode for fecundity (default "recessive").

'recessive'
sexual_selection_mode _AlleleScalingMode

Scaling mode for scalar sexual-selection values. Ignored when sexual_selection_scaling is a tuple.

'recessive'
zygote_viability_mode _AlleleScalingMode

Scaling mode for zygote viability (default "multiplicative").

'recessive'
cas9_deposition_glab Optional[str]

Gamete label for Cas9 deposition tracking.

None
species Optional[Species]

Optional species to bind at construction.

None
use_paternal_deposition bool

Whether to enable paternal Cas9 deposition.

False
Source code in src/natal/genetic_presets.py
def __init__(
    self,
    name: str,
    drive_allele: _AlleleSpecifier,
    target_allele: _AlleleSpecifier,
    disrupted_allele: _AlleleSpecifier,
    conversion_rate: _SexSpecificRates = 0.8,
    embryo_disruption_rate: _SexSpecificRates = 0.0,
    viability_scaling: _ViabilityScalingConfig = 1.0,
    fecundity_scaling: _FecundityScalingConfig = 1.0,
    sexual_selection_scaling: Optional[_SexualSelectionScalingConfig] = None,
    zygote_viability_scaling: _ZygoteViabilityScalingConfig = 0.0,
    viability_mode: _AlleleScalingMode = "recessive",
    fecundity_mode: _AlleleScalingMode = "recessive",
    sexual_selection_mode: _AlleleScalingMode = "recessive",
    zygote_viability_mode: _AlleleScalingMode = "recessive",
    cas9_deposition_glab: Optional[str] = None,
    species: Optional[Species] = None,
    use_paternal_deposition: bool = False,
):
    """Initialize a toxin-antidote gene drive.

    Args:
        name: Name of the gene drive.
        drive_allele: The allele carrying the antidote and disruption machinery.
        target_allele: The wild-type allele targeted for disruption.
        disrupted_allele: The resulting non-functional/disrupted allele.
        conversion_rate: Probability of target disruption in the germline.
        embryo_disruption_rate: Probability of target disruption in embryos.
        viability_scaling: Fitness multiplier for the disrupted allele.
        fecundity_scaling: Fecundity multiplier for the disrupted allele.
        sexual_selection_scaling: Optional sexual-selection multiplier for the disrupted allele.
            Supports a scalar copy-number effect or a tuple
            ``(default_male, carrier_male)``.
        zygote_viability_scaling: Zygote viability scaling configuration for the disrupted allele.
            Applied during reproduction stage before survival; represents probability that a zygote
            survives to become an individual.
        viability_mode: Scaling mode for viability (default "recessive").
        fecundity_mode: Scaling mode for fecundity (default "recessive").
        sexual_selection_mode: Scaling mode for scalar sexual-selection values.
            Ignored when ``sexual_selection_scaling`` is a tuple.
        zygote_viability_mode: Scaling mode for zygote viability (default "multiplicative").
        cas9_deposition_glab: Gamete label for Cas9 deposition tracking.
        species: Optional species to bind at construction.
        use_paternal_deposition: Whether to enable paternal Cas9 deposition.
    """
    self._str_drive_allele = self._resolve_allele_name(drive_allele)
    self._str_target_allele = self._resolve_allele_name(target_allele)
    self._str_disrupted_allele = self._resolve_allele_name(disrupted_allele)

    self.conversion_rate = self._resolve_rates(conversion_rate)
    self.embryo_disruption_rate = self._resolve_rates(embryo_disruption_rate)

    self.viability_scaling = viability_scaling
    self.fecundity_scaling = fecundity_scaling
    self.sexual_selection_scaling = sexual_selection_scaling
    self.zygote_viability_scaling = zygote_viability_scaling
    self.viability_mode: _AlleleScalingMode = viability_mode
    self.fecundity_mode: _AlleleScalingMode = fecundity_mode
    self.sexual_selection_mode: _AlleleScalingMode = sexual_selection_mode
    self.zygote_viability_mode: _AlleleScalingMode = zygote_viability_mode

    self.cas9_deposition_glab = str(cas9_deposition_glab) if cas9_deposition_glab else None
    self.use_paternal_deposition = bool(use_paternal_deposition)

    super().__init__(name=name, species=species)
fitness_patch
fitness_patch() -> PresetFitnessPatch

Return declarative fitness patch for the disrupted allele.

Source code in src/natal/genetic_presets.py
def fitness_patch(self) -> PresetFitnessPatch:
    """Return declarative fitness patch for the disrupted allele."""
    return _make_fitness_patch_given_allele_scaling(
        self._str_disrupted_allele,
        self.viability_scaling,
        self.fecundity_scaling,
        self.sexual_selection_scaling,
        self.zygote_viability_scaling,  # zygote_viability_scaling
        self.viability_mode,
        self.fecundity_mode,
        self.sexual_selection_mode,
        self.zygote_viability_mode,  # zygote_viability_mode
    )
gamete_modifier
gamete_modifier(population: BasePopulation[Any]) -> Optional[GameteModifier]

Implement target disruption in the germline of drive carriers.

Source code in src/natal/genetic_presets.py
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Implement target disruption in the germline of drive carriers."""
    def drive_carrier_filter(gt: Genotype) -> bool:
        return _count_allele_copies(gt, self.drive_allele) > 0

    rule_set = GameteConversionRuleSet(f"{self.name}_GermlineDisruption")
    for sex in (Sex.FEMALE, Sex.MALE):
        rate = self.conversion_rate[sex]
        if rate > 0:
            rule_set.add_allele_convert(
                from_allele=self.target_allele,
                to_allele=self.disrupted_allele,
                rate=rate,
                sex_filter=sex,
                genotype_filter=drive_carrier_filter,
            )

        if self.cas9_deposition_glab and (sex == Sex.FEMALE or self.use_paternal_deposition):
            rule_set.add_hg_convert(
                hg_match=lambda hg: True,
                to_haploid_genotype=lambda hg: hg,
                rate=1.0,
                sex_filter=sex,
                genotype_filter=drive_carrier_filter,
                target_glab=self.cas9_deposition_glab
            )

    return rule_set.to_gamete_modifier(population) if rule_set.rules else None
zygote_modifier
zygote_modifier(population: BasePopulation[Any]) -> Optional[ZygoteModifier]

Implement target disruption in embryos.

Source code in src/natal/genetic_presets.py
def zygote_modifier(self, population: 'BasePopulation[Any]') -> Optional[ZygoteModifier]:
    """Implement target disruption in embryos."""
    rule_set = ZygoteConversionRuleSet(f"{self.name}_EmbryoDisruption")

    def zygote_has_drive(gt: Genotype) -> bool:
        return _count_allele_copies(gt, self.drive_allele) > 0

    for sex in (Sex.FEMALE, Sex.MALE):
        rate = self.embryo_disruption_rate[sex]
        if rate > 0:
            m_glab = self.cas9_deposition_glab if sex == Sex.FEMALE else None
            p_glab = self.cas9_deposition_glab if (sex == Sex.MALE and self.use_paternal_deposition) else None
            g_filter = None if (m_glab or p_glab) else zygote_has_drive

            if m_glab or p_glab or g_filter:
                rule_set.add_allele_convert(
                    from_allele=self.target_allele,
                    to_allele=self.disrupted_allele,
                    rate=rate,
                    maternal_glab=m_glab,
                    paternal_glab=p_glab,
                    genotype_filter=g_filter,
                )

    return rule_set.to_zygote_modifier(population) if rule_set.rules else None

apply_preset_to_population

apply_preset_to_population(population: BasePopulation[Any], preset: GeneticPreset) -> None

Apply a genetic preset to a population by registering its modifiers and fitness effects.

This function handles the mechanical application of a preset to a population, including: 1. Species binding and validation 2. Registration of gamete modifiers 3. Registration of zygote modifiers 4. Application of fitness patches

Parameters:

Name Type Description Default
population BasePopulation[Any]

The BasePopulation instance to modify.

required
preset GeneticPreset

The GeneticPreset instance to apply.

required
Note

This is typically called through the modern API: population.apply_preset(preset)

The legacy API preset.apply(population) is deprecated but still supported.

Raises:

Type Description
ValueError

If preset is bound to a different species than the population

RuntimeError

If preset has no bound species

Source code in src/natal/genetic_presets.py
def apply_preset_to_population(population: 'BasePopulation[Any]', preset: 'GeneticPreset') -> None:
    """Apply a genetic preset to a population by registering its modifiers and fitness effects.

    This function handles the mechanical application of a preset to a population,
    including:
    1. Species binding and validation
    2. Registration of gamete modifiers
    3. Registration of zygote modifiers
    4. Application of fitness patches

    Args:
        population: The BasePopulation instance to modify.
        preset: The GeneticPreset instance to apply.

    Note:
        This is typically called through the modern API:
        ``population.apply_preset(preset)``

        The legacy API ``preset.apply(population)`` is deprecated but still supported.

    Raises:
        ValueError: If preset is bound to a different species than the population
        RuntimeError: If preset has no bound species
    """
    preset.bind_species(population.species)

    gamete_mod = preset.gamete_modifier(population)
    zygote_mod = preset.zygote_modifier(population)

    if gamete_mod is not None:
        population.add_gamete_modifier(
            gamete_mod,
            name=f"{preset.name}/gamete",
            refresh=False,
        )

    if zygote_mod is not None:
        population.add_zygote_modifier(
            zygote_mod,
            name=f"{preset.name}/zygote",
            refresh=False,
        )

    if gamete_mod is not None or zygote_mod is not None:
        population.refresh_modifier_maps()

    # Preferred path: declarative fitness patch
    patch = preset.fitness_patch()
    if patch:
        _apply_preset_fitness_patch(population, patch)
        return