Skip to content

presets Module

Built-in genetic presets and preset application helpers.

Overview

The presets subpackage 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.presets

Genetic presets subpackage.

Provides genetic modification presets including gene drives (HomingDrive, ToxinAntidoteDrive), cytoplasmic inheritance (Wolbachia), allele conversion systems, and the GeneticPreset base class.

GameteAlleleConversionRule

GameteAlleleConversionRule(from_allele: Union[str, Gene], to_allele: Union[str, Gene], rate: float, name: Optional[str] = None, sex_filter: Optional[Union[str, int, Sex]] = 'both', genotype_filter: _GenotypeFilter = None, source_glab: Optional[Union[str, int]] = None, target_glab: Optional[Union[str, int]] = None)

Defines a single allele conversion rule: from_allele -> to_allele with probability.

This is a pure data container specifying
  • source allele (from_allele)
  • target allele (to_allele)
  • conversion probability (rate)
  • optional context constraints (sex, genotype filters)

Examples:

rule = GameteAlleleConversionRule(from_allele="A", to_allele="B", rate=0.5)

In heterozygotes carrying A, 50% of gametes convert A -> B

Initialize an allele conversion rule.

Parameters:

Name Type Description Default
from_allele Union[str, Gene]

Source allele (string identifier or Gene object).

required
to_allele Union[str, Gene]

Target allele (string identifier or Gene object).

required
rate float

Conversion probability, must be in [0, 1].

required
name Optional[str]

Optional human-readable name.

None
sex_filter Optional[Union[str, int, Sex]]

Apply only to specific sex ("female", "male", or "both").

'both'
genotype_filter _GenotypeFilter

Optional filter for applicable genotypes. Accepts callable or genotype pattern string.

None
source_glab Optional[Union[str, int]]

Optional gamete label filter. If specified, this rule only applies to gametes carrying this label (str name or int index). If None, applies to all glab variants.

None
target_glab Optional[Union[str, int]]

Optional gamete label for converted gametes. If specified, the converted gamete will be tagged with this label. If None, the converted gamete retains the source's glab.

None

Raises:

Type Description
ValueError

If rate is not in [0, 1].

TypeError

If from_allele and to_allele types don't match.

Source code in src/natal/modifiers/gamete_conversion.py
def __init__(
    self,
    from_allele: Union[str, Gene],
    to_allele: Union[str, Gene],
    rate: float,
    name: Optional[str] = None,
    sex_filter: Optional[Union[str, int, Sex]] = "both",
    genotype_filter: _GenotypeFilter = None,
    source_glab: Optional[Union[str, int]] = None,
    target_glab: Optional[Union[str, int]] = None,
):
    """Initialize an allele conversion rule.

    Args:
        from_allele: Source allele (string identifier or Gene object).
        to_allele: Target allele (string identifier or Gene object).
        rate: Conversion probability, must be in [0, 1].
        name: Optional human-readable name.
        sex_filter: Apply only to specific sex ("female", "male", or "both").
        genotype_filter: Optional filter for applicable genotypes.
                       Accepts callable or genotype pattern string.
        source_glab: Optional gamete label filter. If specified, this rule only
                    applies to gametes carrying this label (str name or int index).
                    If None, applies to all glab variants.
        target_glab: Optional gamete label for converted gametes. If specified,
                    the converted gamete will be tagged with this label.
                    If None, the converted gamete retains the source's glab.

    Raises:
        ValueError: If rate is not in [0, 1].
        TypeError: If from_allele and to_allele types don't match.
    """
    if not 0 <= rate <= 1:
        raise ValueError(f"rate must be in [0, 1], got {rate}")

    # Normalize allele representations to strings for comparison
    self.from_allele_str = from_allele if isinstance(from_allele, str) else from_allele.name
    self.to_allele_str = to_allele if isinstance(to_allele, str) else to_allele.name

    # Store original objects for reference
    self.from_allele = from_allele
    self.to_allele = to_allele
    self.rate = rate
    self.name = name or f"{self.from_allele_str}→{self.to_allele_str}({sex_filter or 'both'})"
    if sex_filter is None:
        self.sex_filter = "both"
    else:
        self.sex_filter = sex_filter
    self.genotype_filter = genotype_filter
    self._compiled_genotype_filter: Optional[Callable[[Genotype], bool]] = None
    self.source_glab = source_glab
    self.target_glab = target_glab
applies_to_sex
applies_to_sex(sex_idx: _SexSpecifier, sex_name: Optional[str] = None) -> bool

Check if rule applies to a given sex.

Parameters:

Name Type Description Default
sex_idx _SexSpecifier

Integer sex index (0 for first sex, 1 for second, etc.).

required
sex_name Optional[str]

Optional sex name for clarity ("female", "male").

None

Returns:

Type Description
bool

True if rule applies to this sex.

Source code in src/natal/modifiers/gamete_conversion.py
def applies_to_sex(self, sex_idx: _SexSpecifier, sex_name: Optional[str] = None) -> bool:
    """Check if rule applies to a given sex.

    Args:
        sex_idx: Integer sex index (0 for first sex, 1 for second, etc.).
        sex_name: Optional sex name for clarity ("female", "male").

    Returns:
        True if rule applies to this sex.
    """
    if self.sex_filter == "both":
        return True
    try:
        target_sex_idx = resolve_sex_label(self.sex_filter)
        return sex_idx == target_sex_idx
    except (ValueError, TypeError) as e:
        raise ValueError(f"Invalid sex_filter: {self.sex_filter}") from e
applies_to_genotype
applies_to_genotype(genotype: Genotype) -> bool

Check if rule applies to a given genotype.

If no filter is set, rule applies to all genotypes.

Parameters:

Name Type Description Default
genotype Genotype

The Genotype to check.

required

Returns:

Type Description
bool

True if rule should apply to this genotype.

Source code in src/natal/modifiers/gamete_conversion.py
def applies_to_genotype(self, genotype: Genotype) -> bool:
    """Check if rule applies to a given genotype.

    If no filter is set, rule applies to all genotypes.

    Args:
        genotype: The Genotype to check.

    Returns:
        True if rule should apply to this genotype.
    """
    applies, compiled = _evaluate_genotype_filter(
        self.genotype_filter,
        genotype,
        self._compiled_genotype_filter,
    )
    self._compiled_genotype_filter = compiled
    return applies

GameteConversionRuleSet

GameteConversionRuleSet(name: str = 'GameteConversionRuleSet')

Manages a collection of gamete conversion rules.

Accepts both :class:GameteAlleleConversionRule (allele-level) and :class:GameteHaploidGenomeConversionRule (whole-HaploidGenotype-level). Rules are evaluated in insertion order; the first matching rule wins for each (hg, glab) entry.

Example usage::

ruleset = GameteConversionRuleSet()
# allele-level
ruleset.add_allele_convert("A", "B", rate=0.5)
# haploid-genome-level
ruleset.add_hg_convert(hg_AB, hg_CD, rate=0.8)

gamete_mod = ruleset.to_gamete_modifier(population)
population.add_gamete_modifier(gamete_mod, name="conversions")

Initialize an empty ruleset.

Parameters:

Name Type Description Default
name str

Human-readable name for this ruleset.

'GameteConversionRuleSet'
Source code in src/natal/modifiers/gamete_conversion.py
def __init__(self, name: str = "GameteConversionRuleSet"):
    """Initialize an empty ruleset.

    Args:
        name: Human-readable name for this ruleset.
    """
    self.name = name
    self.rules: List[_GameteRuleType] = []
add_rule
add_rule(rule: _GameteRuleType) -> GameteConversionRuleSet

Append a rule (allele-level or HaploidGenotype-level). Returns self.

Source code in src/natal/modifiers/gamete_conversion.py
def add_rule(self, rule: _GameteRuleType) -> 'GameteConversionRuleSet':
    """Append a rule (allele-level or HaploidGenotype-level).  Returns *self*."""
    assert isinstance(rule, (GameteAlleleConversionRule, GameteHaploidGenomeConversionRule)), \
            "rule must be an instance of GameteAlleleConversionRule or GameteHaploidGenomeConversionRule"
    self.rules.append(rule)
    return self
add_allele_convert
add_allele_convert(from_allele: Union[str, Gene], to_allele: Union[str, Gene], rate: float, sex_filter: Optional[Union[str, int]] = None, genotype_filter: _GenotypeFilter = None, source_glab: Optional[Union[str, int]] = None, target_glab: Optional[Union[str, int]] = None) -> GameteConversionRuleSet

Add an allele-level conversion rule.

Parameters:

Name Type Description Default
from_allele Union[str, Gene]

Source allele identifier or Gene.

required
to_allele Union[str, Gene]

Target allele identifier or Gene.

required
rate float

Conversion probability.

required
sex_filter Optional[Union[str, int]]

Rule applies only to this sex ("male"/"female" or index).

None
genotype_filter _GenotypeFilter

Rule applies only if diploid parent passes this filter.

None
source_glab Optional[Union[str, int]]

Rule applies only to gametes currently holding this label.

None
target_glab Optional[Union[str, int]]

Gametes that successfully convert get reassigned to this label.

None

Returns:

Type Description
GameteConversionRuleSet

self for chaining.

Source code in src/natal/modifiers/gamete_conversion.py
def add_allele_convert(
    self,
    from_allele: Union[str, Gene],
    to_allele: Union[str, Gene],
    rate: float,
    sex_filter: Optional[Union[str, int]] = None,
    genotype_filter: _GenotypeFilter = None,
    source_glab: Optional[Union[str, int]] = None,
    target_glab: Optional[Union[str, int]] = None,
) -> 'GameteConversionRuleSet':
    """Add an allele-level conversion rule.

    Args:
        from_allele: Source allele identifier or Gene.
        to_allele: Target allele identifier or Gene.
        rate: Conversion probability.
        sex_filter: Rule applies only to this sex ("male"/"female" or index).
        genotype_filter: Rule applies only if diploid parent passes this filter.
        source_glab: Rule applies only to gametes currently holding this label.
        target_glab: Gametes that successfully convert get reassigned to this label.

    Returns:
        *self* for chaining.
    """
    rule = GameteAlleleConversionRule(
        from_allele=from_allele,
        to_allele=to_allele,
        rate=rate,
        sex_filter=sex_filter,
        genotype_filter=genotype_filter,
        source_glab=source_glab,
        target_glab=target_glab,
    )
    return self.add_rule(rule)
add_hg_convert
add_hg_convert(hg_match: Union[Callable[[HaploidGenotype], bool], HaploidGenotype], to_haploid_genotype: Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]], rate: float, sex_filter: Optional[Union[str, int]] = None, genotype_filter: _GenotypeFilter = None, source_glab: Optional[Union[str, int]] = None, target_glab: Optional[Union[str, int]] = None) -> GameteConversionRuleSet

Add a HaploidGenotype-level conversion rule.

Parameters:

Name Type Description Default
hg_match Union[Callable[[HaploidGenotype], bool], HaploidGenotype]

Match predicate / HaploidGenotype.

required
to_haploid_genotype Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]]

Replacement HaploidGenotype or callable.

required
rate float

Conversion probability.

required
sex_filter Optional[Union[str, int]]

Rule applies only to this sex ("male"/"female" or index).

None
genotype_filter _GenotypeFilter

Rule applies only if diploid parent passes this filter.

None
source_glab Optional[Union[str, int]]

Rule applies only to gametes currently holding this label.

None
target_glab Optional[Union[str, int]]

Gametes that successfully convert get reassigned to this label.

None

Returns:

Type Description
GameteConversionRuleSet

self for chaining.

Source code in src/natal/modifiers/gamete_conversion.py
def add_hg_convert(
    self,
    hg_match: Union[Callable[[HaploidGenotype], bool], HaploidGenotype],
    to_haploid_genotype: Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]],
    rate: float,
    sex_filter: Optional[Union[str, int]] = None,
    genotype_filter: _GenotypeFilter = None,
    source_glab: Optional[Union[str, int]] = None,
    target_glab: Optional[Union[str, int]] = None,
) -> 'GameteConversionRuleSet':
    """Add a HaploidGenotype-level conversion rule.

    Args:
        hg_match: Match predicate / HaploidGenotype.
        to_haploid_genotype: Replacement HaploidGenotype or callable.
        rate: Conversion probability.
        sex_filter: Rule applies only to this sex ("male"/"female" or index).
        genotype_filter: Rule applies only if diploid parent passes this filter.
        source_glab: Rule applies only to gametes currently holding this label.
        target_glab: Gametes that successfully convert get reassigned to this label.

    Returns:
        *self* for chaining.
    """
    rule = GameteHaploidGenomeConversionRule(
        hg_match=hg_match,
        to_haploid_genotype=to_haploid_genotype,
        rate=rate,
        sex_filter=sex_filter,
        genotype_filter=genotype_filter,
        source_glab=source_glab,
        target_glab=target_glab,
    )
    return self.add_rule(rule)
to_gamete_modifier
to_gamete_modifier(population: BasePopulation[Any]) -> GameteModifier

Convert the ruleset to a GameteModifier for population integration.

The returned modifier will
  1. Iterate over all genotypes in the population
  2. For each genotype, extract glab-aware gamete frequencies
  3. Apply conversion rules respecting source_glab/target_glab constraints
  4. Return compressed-index frequency mappings

Parameters:

Name Type Description Default
population BasePopulation[Any]

The BasePopulation that will use this modifier.

required

Returns:

Type Description
GameteModifier

A callable that implements GameteModifier protocol.

Source code in src/natal/modifiers/gamete_conversion.py
def to_gamete_modifier(
    self,
    population: 'BasePopulation[Any]'
) -> GameteModifier:
    """Convert the ruleset to a GameteModifier for population integration.

    The returned modifier will:
      1. Iterate over all genotypes in the population
      2. For each genotype, extract glab-aware gamete frequencies
      3. Apply conversion rules respecting source_glab/target_glab constraints
      4. Return compressed-index frequency mappings

    Args:
        population: The BasePopulation that will use this modifier.

    Returns:
        A callable that implements GameteModifier protocol.
    """
    rules = self.rules

    def gamete_modifier_func(*_args: object, **_kwargs: object) -> Dict[Tuple[int, int], Dict[int, float]]:
        """Apply all conversion rules to gamete frequencies.

        Returns dict mapping (sex_idx, genotype_idx) -> {compressed_hg_glab_idx -> freq}.
        """
        result: Dict[Tuple[int, int], Dict[int, float]] = {}

        n_glabs = int(population.config.n_glabs)
        zygotes_to_gametes_map = population.config.zygotes_to_gametes_map
        haploid_genotypes = population.registry.index_to_haplo

        # Resolve glab names to indices for all rules (once)
        resolved_rules = _resolve_rule_glabs(rules, population)

        for sex_idx in range(population.config.n_sexes):
            for genotype_idx, genotype in enumerate(population.registry.index_to_genotype):
                if not any(rule.applies_to_sex(sex_idx) and
                          rule.applies_to_genotype(genotype)
                          for rule in rules):
                    continue

                # Extract glab-aware gamete frequencies
                initial_freqs = extract_gamete_frequencies_by_glab(
                    zygotes_to_gametes_map,
                    sex_idx,
                    genotype_idx,
                    haploid_genotypes,
                    n_glabs
                )

                if not initial_freqs:
                    continue

                # Compute converted frequencies at (HaploidGenotype, glab_idx) level
                converted_freqs = _compute_converted_gamete_freqs(
                    genotype,
                    resolved_rules,
                    sex_idx,
                    population,
                    initial_freqs=initial_freqs
                )

                if converted_freqs:
                    # Convert (HaploidGenotype, glab_idx) -> compressed index
                    compressed_freqs: Dict[int, float] = {}
                    for (hg, glab_idx), freq in converted_freqs.items():
                        if freq > 0:
                            glab_label_str = population.registry.glab_labels[glab_idx]
                            cidx = population.registry.gtype_index(hg, glab_label_str)
                            compressed_freqs[cidx] = compressed_freqs.get(cidx, 0.0) + freq

                    if compressed_freqs:
                        result[(sex_idx, genotype_idx)] = compressed_freqs

        return result

    return gamete_modifier_func  # TODO: protocol typing still needs cleanup.

GameteHaploidGenomeConversionRule

GameteHaploidGenomeConversionRule(hg_match: Union[Callable[[HaploidGenotype], bool], HaploidGenotype], to_haploid_genotype: Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]], rate: float, name: Optional[str] = None, sex_filter: Optional[Union[str, int, Sex]] = 'both', genotype_filter: _GenotypeFilter = None, source_glab: Optional[Union[str, int]] = None, target_glab: Optional[Union[str, int]] = None)

Defines a whole-HaploidGenotype replacement rule at the gamete level.

Unlike :class:GameteAlleleConversionRule which swaps a single allele, this rule matches an entire HaploidGenotype and replaces it with another HaploidGenotype (or a dynamically computed one).

Examples:

# Replace haploid genome hg_AB with hg_CD at 80 % probability
rule = GameteHaploidGenomeConversionRule(
    hg_match=hg_AB,
    to_haploid_genotype=hg_CD,
    rate=0.8,
)

Initialise a haploid-genome-level gamete conversion rule.

Parameters:

Name Type Description Default
hg_match Union[Callable[[HaploidGenotype], bool], HaploidGenotype]

Either a specific HaploidGenotype instance (matched by identity) or a callable (HaploidGenotype) -> bool predicate.

required
to_haploid_genotype Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]]

The replacement HaploidGenotype, or a callable (original) -> HaploidGenotype for dynamic replacement.

required
rate float

Probability of conversion, in [0, 1].

required
name Optional[str]

Human-readable label.

None
sex_filter Optional[Union[str, int, Sex]]

Apply only to specific sex.

'both'
genotype_filter _GenotypeFilter

Optional filter on the diploid Genotype of the gamete producer. Accepts callable or genotype pattern string.

None
source_glab Optional[Union[str, int]]

Optional glab filter on the input gamete.

None
target_glab Optional[Union[str, int]]

Optional glab to assign to the converted gamete.

None

Raises:

Type Description
ValueError

If rate is not in [0, 1].

Source code in src/natal/modifiers/gamete_conversion.py
def __init__(
    self,
    hg_match: Union[Callable[[HaploidGenotype], bool], HaploidGenotype],
    to_haploid_genotype: Union[HaploidGenotype, Callable[[HaploidGenotype], HaploidGenotype]],
    rate: float,
    name: Optional[str] = None,
    sex_filter: Optional[Union[str, int, Sex]] = "both",
    genotype_filter: _GenotypeFilter = None,
    source_glab: Optional[Union[str, int]] = None,
    target_glab: Optional[Union[str, int]] = None,
):
    """Initialise a haploid-genome-level gamete conversion rule.

    Args:
        hg_match: Either a specific ``HaploidGenotype`` instance
            (matched by identity) or a callable
            ``(HaploidGenotype) -> bool`` predicate.
        to_haploid_genotype: The replacement ``HaploidGenotype``, or a
            callable ``(original) -> HaploidGenotype`` for dynamic
            replacement.
        rate: Probability of conversion, in [0, 1].
        name: Human-readable label.
        sex_filter: Apply only to specific sex.
        genotype_filter: Optional filter on the *diploid* Genotype
            of the gamete producer. Accepts callable or genotype
            pattern string.
        source_glab: Optional glab filter on the input gamete.
        target_glab: Optional glab to assign to the converted gamete.

    Raises:
        ValueError: If *rate* is not in [0, 1].
    """
    if not 0 <= rate <= 1:
        raise ValueError(f"rate must be in [0, 1], got {rate}")

    if isinstance(hg_match, HaploidGenotype):
        _hg = hg_match
        self._match_fn: Callable[[HaploidGenotype], bool] = lambda h, _hg=_hg: h is _hg
    elif callable(hg_match):
        self._match_fn = hg_match
    else:
        raise TypeError(
            "hg_match must be a HaploidGenotype instance or a callable"
        )

    if isinstance(to_haploid_genotype, HaploidGenotype):
        _thg = to_haploid_genotype
        self._replacement_fn: Callable[[HaploidGenotype], HaploidGenotype] = lambda h, _thg=_thg: _thg
    elif callable(to_haploid_genotype):
        self._replacement_fn = to_haploid_genotype
    else:
        raise TypeError(
            "to_haploid_genotype must be a HaploidGenotype instance or a callable"
        )

    self.hg_match = hg_match
    self.to_haploid_genotype = to_haploid_genotype
    self.rate = rate
    self.name = name or f"GameteHGConversion(rate={rate}, sex={sex_filter or 'both'})"
    if sex_filter is None:
        self.sex_filter = "both"
    else:
        self.sex_filter = sex_filter
    self.genotype_filter = genotype_filter
    self._compiled_genotype_filter: Optional[Callable[[Genotype], bool]] = None
    self.source_glab = source_glab
    self.target_glab = target_glab
matches
matches(hg: HaploidGenotype) -> bool

Return True if hg satisfies this rule's match predicate.

Source code in src/natal/modifiers/gamete_conversion.py
def matches(self, hg: HaploidGenotype) -> bool:
    """Return True if *hg* satisfies this rule's match predicate."""
    return self._match_fn(hg)
replacement
replacement(hg: HaploidGenotype) -> HaploidGenotype

Return the replacement HaploidGenotype for a matched original.

Source code in src/natal/modifiers/gamete_conversion.py
def replacement(self, hg: HaploidGenotype) -> HaploidGenotype:
    """Return the replacement HaploidGenotype for a matched original."""
    return self._replacement_fn(hg)
applies_to_sex
applies_to_sex(sex_idx: _SexSpecifier, sex_name: Optional[str] = None) -> bool

Check if rule applies to a given sex.

Source code in src/natal/modifiers/gamete_conversion.py
def applies_to_sex(self, sex_idx: _SexSpecifier, sex_name: Optional[str] = None) -> bool:
    """Check if rule applies to a given sex."""
    if self.sex_filter == "both":
        return True
    try:
        target_sex_idx = resolve_sex_label(self.sex_filter)
        return sex_idx == target_sex_idx
    except (ValueError, TypeError) as e:
        raise ValueError(f"Invalid sex_filter: {self.sex_filter}") from e
applies_to_genotype
applies_to_genotype(genotype: Genotype) -> bool

Check if rule applies to a given diploid genotype.

Source code in src/natal/modifiers/gamete_conversion.py
def applies_to_genotype(self, genotype: Genotype) -> bool:
    """Check if rule applies to a given diploid genotype."""
    applies, compiled = _evaluate_genotype_filter(
        self.genotype_filter,
        genotype,
        self._compiled_genotype_filter,
    )
    self._compiled_genotype_filter = compiled
    return applies

ZygoteAlleleConversionRule

ZygoteAlleleConversionRule(from_allele: Union[str, Gene], to_allele: Union[str, Gene], rate: float, name: Optional[str] = None, side: Literal['maternal', 'paternal', 'both'] = 'both', genotype_filter: _GenotypeFilter = None, maternal_glab: Optional[Union[str, int]] = None, paternal_glab: Optional[Union[str, int]] = None)

Defines an allele-level zygote conversion rule: from_allele -> to_allele.

Unlike :class:ZygoteGenotypeConversionRule which matches and replaces whole diploid genotypes, this rule operates at the single-allele level — if the zygote's genotype contains from_allele, it is replaced by to_allele on the specified side(s) of the diploid.

The side parameter controls which parental copy is subject to conversion:

  • "maternal" — only the maternal haploid genome.
  • "paternal" — only the paternal haploid genome.
  • "both" — both sides independently (can produce up to 4 outcome genotypes for a double-heterozygote).

Examples:

# In the zygote, convert allele W→D on both sides with 95% each
rule = ZygoteAlleleConversionRule("W", "D", rate=0.95, side="both")

Initialise an allele-level zygote conversion rule.

Parameters:

Name Type Description Default
from_allele Union[str, Gene]

Source allele (string name or Gene object).

required
to_allele Union[str, Gene]

Target allele (string name or Gene object).

required
rate float

Per-copy conversion probability, in [0, 1].

required
name Optional[str]

Human-readable label.

None
side Literal['maternal', 'paternal', 'both']

Which parental copy to attempt conversion on.

'both'
genotype_filter _GenotypeFilter

Optional genotype filter. Accepts predicate (Genotype) -> bool or genotype pattern string. If set, the rule is skipped for genotypes that do not pass.

None
maternal_glab Optional[Union[str, int]]

Optional glab filter on maternal gamete (c1).

None
paternal_glab Optional[Union[str, int]]

Optional glab filter on paternal gamete (c2).

None

Raises:

Type Description
ValueError

If rate is not in [0, 1].

Source code in src/natal/modifiers/zygote_conversion.py
def __init__(
    self,
    from_allele: Union[str, Gene],
    to_allele: Union[str, Gene],
    rate: float,
    name: Optional[str] = None,
    side: Literal["maternal", "paternal", "both"] = "both",
    genotype_filter: _GenotypeFilter = None,
    maternal_glab: Optional[Union[str, int]] = None,
    paternal_glab: Optional[Union[str, int]] = None,
):
    """Initialise an allele-level zygote conversion rule.

    Args:
        from_allele: Source allele (string name or ``Gene`` object).
        to_allele: Target allele (string name or ``Gene`` object).
        rate: Per-copy conversion probability, in [0, 1].
        name: Human-readable label.
        side: Which parental copy to attempt conversion on.
        genotype_filter: Optional genotype filter. Accepts predicate
            ``(Genotype) -> bool`` or genotype pattern string.
            If set, the rule is skipped for genotypes that do not pass.
        maternal_glab: Optional glab filter on maternal gamete (c1).
        paternal_glab: Optional glab filter on paternal gamete (c2).

    Raises:
        ValueError: If *rate* is not in [0, 1].
    """
    if not 0 <= rate <= 1:
        raise ValueError(f"rate must be in [0, 1], got {rate}")

    self.from_allele_str = from_allele if isinstance(from_allele, str) else from_allele.name
    self.to_allele_str = to_allele if isinstance(to_allele, str) else to_allele.name
    self.from_allele = from_allele
    self.to_allele = to_allele
    self.rate = rate
    self.name = name or f"{self.from_allele_str}{self.to_allele_str}@zygote({rate})"
    self.side = side
    self.genotype_filter = genotype_filter
    self._compiled_genotype_filter: Optional[Callable[[Genotype], bool]] = None
    self.maternal_glab = maternal_glab
    self.paternal_glab = paternal_glab
applies_to_genotype
applies_to_genotype(genotype: Genotype) -> bool

Check whether this rule should be evaluated for genotype.

Source code in src/natal/modifiers/zygote_conversion.py
def applies_to_genotype(self, genotype: Genotype) -> bool:
    """Check whether this rule should be evaluated for *genotype*."""
    applies, compiled = _evaluate_genotype_filter(
        self.genotype_filter,
        genotype,
        self._compiled_genotype_filter,
    )
    self._compiled_genotype_filter = compiled
    return applies

ZygoteConversionRuleSet

ZygoteConversionRuleSet(name: str = 'ZygoteConversionRuleSet')

Manages a collection of zygote conversion rules.

Accepts both :class:ZygoteGenotypeConversionRule (genotype-level) and :class:ZygoteAlleleConversionRule (allele-level). Rules are evaluated in insertion order; the first matching rule wins for each (c1, c2) pair.

Examples:

ruleset = ZygoteConversionRuleSet()
# genotype-level
ruleset.add_convert(gt_AA, gt_Aa, rate=0.8)
# allele-level
ruleset.add_allele_convert("W", "D", rate=0.95, side="both")

zygote_mod = ruleset.to_zygote_modifier(population)
population.add_zygote_modifier(zygote_mod, name="zygote_conversions")

Initialize an empty zygote conversion rule set.

Parameters:

Name Type Description Default
name str

A human-readable name for this rule set.

'ZygoteConversionRuleSet'
Source code in src/natal/modifiers/zygote_conversion.py
def __init__(self, name: str = "ZygoteConversionRuleSet"):
    """Initialize an empty zygote conversion rule set.

    Args:
        name: A human-readable name for this rule set.
    """
    self.name = name
    self.rules: List[ZygoteConversionRuleSet._RuleType] = []
add_rule
add_rule(rule: _RuleType) -> ZygoteConversionRuleSet

Append a rule (genotype-level or allele-level). Returns self.

Source code in src/natal/modifiers/zygote_conversion.py
def add_rule(self, rule: _RuleType) -> "ZygoteConversionRuleSet":
    """Append a rule (genotype-level or allele-level).  Returns *self*."""
    assert isinstance(rule, (ZygoteGenotypeConversionRule, ZygoteAlleleConversionRule)), \
            "rule must be an instance of ZygoteGenotypeConversionRule or ZygoteAlleleConversionRule"
    self.rules.append(rule)
    return self
add_convert
add_convert(genotype_match: Union[Callable[[Genotype], bool], Genotype], to_genotype: Union[Genotype, Callable[[Genotype], Genotype]], rate: float, name: Optional[str] = None, maternal_glab: Optional[Union[str, int]] = None, paternal_glab: Optional[Union[str, int]] = None) -> ZygoteConversionRuleSet

Add a genotype-level conversion rule.

Parameters:

Name Type Description Default
genotype_match Union[Callable[[Genotype], bool], Genotype]

Match predicate / Genotype.

required
to_genotype Union[Genotype, Callable[[Genotype], Genotype]]

Replacement Genotype or callable.

required
rate float

Conversion probability.

required
name Optional[str]

Human-readable label.

None
maternal_glab Optional[Union[str, int]]

Optional gamete-label filter on the maternal gamete.

None
paternal_glab Optional[Union[str, int]]

Optional gamete-label filter on the paternal gamete.

None

Returns:

Type Description
ZygoteConversionRuleSet

self for chaining.

Source code in src/natal/modifiers/zygote_conversion.py
def add_convert(
    self,
    genotype_match: Union[Callable[[Genotype], bool], Genotype],
    to_genotype: Union[Genotype, Callable[[Genotype], Genotype]],
    rate: float,
    name: Optional[str] = None,
    maternal_glab: Optional[Union[str, int]] = None,
    paternal_glab: Optional[Union[str, int]] = None,
) -> "ZygoteConversionRuleSet":
    """Add a genotype-level conversion rule.

    Args:
        genotype_match: Match predicate / Genotype.
        to_genotype: Replacement Genotype or callable.
        rate: Conversion probability.
        name: Human-readable label.
        maternal_glab: Optional gamete-label filter on the maternal gamete.
        paternal_glab: Optional gamete-label filter on the paternal gamete.

    Returns:
        *self* for chaining.
    """
    rule = ZygoteGenotypeConversionRule(
        genotype_match,
        to_genotype,
        rate,
        name=name,
        maternal_glab=maternal_glab,
        paternal_glab=paternal_glab,
    )
    return self.add_rule(rule)
add_allele_convert
add_allele_convert(from_allele: Union[str, Gene], to_allele: Union[str, Gene], rate: float, name: Optional[str] = None, side: Literal['maternal', 'paternal', 'both'] = 'both', genotype_filter: _GenotypeFilter = None, maternal_glab: Optional[Union[str, int]] = None, paternal_glab: Optional[Union[str, int]] = None) -> ZygoteConversionRuleSet

Add an allele-level conversion rule.

Parameters:

Name Type Description Default
from_allele Union[str, Gene]

Source allele identifier or Gene.

required
to_allele Union[str, Gene]

Target allele identifier or Gene.

required
rate float

Per-copy conversion probability.

required
name Optional[str]

Human-readable label.

None
side Literal['maternal', 'paternal', 'both']

Which parental copy to attempt conversion on ("maternal", "paternal", "both").

'both'
genotype_filter _GenotypeFilter

Optional genotype filter (predicate or pattern string).

None
maternal_glab Optional[Union[str, int]]

Optional glab filter on maternal gamete.

None
paternal_glab Optional[Union[str, int]]

Optional glab filter on paternal gamete.

None

Returns:

Type Description
ZygoteConversionRuleSet

self for chaining.

Source code in src/natal/modifiers/zygote_conversion.py
def add_allele_convert(
    self,
    from_allele: Union[str, Gene],
    to_allele: Union[str, Gene],
    rate: float,
    name: Optional[str] = None,
    side: Literal["maternal", "paternal", "both"] = "both",
    genotype_filter: _GenotypeFilter = None,
    maternal_glab: Optional[Union[str, int]] = None,
    paternal_glab: Optional[Union[str, int]] = None,
) -> "ZygoteConversionRuleSet":
    """Add an allele-level conversion rule.

    Args:
        from_allele: Source allele identifier or Gene.
        to_allele: Target allele identifier or Gene.
        rate: Per-copy conversion probability.
        name: Human-readable label.
        side: Which parental copy to attempt conversion on ("maternal", "paternal", "both").
        genotype_filter: Optional genotype filter (predicate or pattern string).
        maternal_glab: Optional glab filter on maternal gamete.
        paternal_glab: Optional glab filter on paternal gamete.

    Returns:
        *self* for chaining.
    """
    rule = ZygoteAlleleConversionRule(
        from_allele,
        to_allele,
        rate,
        name=name,
        side=side,
        genotype_filter=genotype_filter,
        maternal_glab=maternal_glab,
        paternal_glab=paternal_glab,
    )
    return self.add_rule(rule)
to_zygote_modifier
to_zygote_modifier(population: BasePopulation[Any]) -> ZygoteModifier

Convert the rule-set into a ZygoteModifier.

The returned callable produces a dict understood by the existing wrap_zygote_modifier infrastructure::

{ (c1, c2): { genotype_idx: prob, ... }, ... }

Both genotype-level and allele-level rules are evaluated in insertion order. The first matching rule wins for each (c1, c2) pair.

Parameters:

Name Type Description Default
population BasePopulation[Any]

The population that will consume the modifier.

required

Returns:

Type Description
ZygoteModifier

A zero-argument callable implementing ZygoteModifier.

Source code in src/natal/modifiers/zygote_conversion.py
def to_zygote_modifier(
    self,
    population: "BasePopulation[Any]",
) -> ZygoteModifier:
    """Convert the rule-set into a ``ZygoteModifier``.

    The returned callable produces a dict understood by the existing
    ``wrap_zygote_modifier`` infrastructure::

        { (c1, c2): { genotype_idx: prob, ... }, ... }

    Both genotype-level and allele-level rules are evaluated in
    insertion order.  The first matching rule wins for each
    ``(c1, c2)`` pair.

    Args:
        population: The population that will consume the modifier.

    Returns:
        A zero-argument callable implementing ``ZygoteModifier``.
    """
    rules = self.rules

    def zygote_modifier_func(*_args: object, **_kwargs: object) -> Dict[
        Tuple[int, int], Dict[int, float]
    ]:
        """Produce a mapping of gamete-pair -> {genotype_idx: probability} from all rules."""  # noqa: D400
        diploid_genotypes = population.registry.index_to_genotype

        # Build genotype index lookup for the registered diploid set.
        genotype_index: Dict[Genotype, int] = {}
        for idx, gt in enumerate(diploid_genotypes):
            genotype_index[gt] = idx

        baseline_g2z = population.config.gametes_to_zygotes_map
        n_c = baseline_g2z.shape[0]

        # Resolve glab names to indices for all rules
        resolved_rules = _resolve_zygote_rule_glabs(rules, population)

        result: Dict[Tuple[int, int], Dict[int, float]] = {}

        for c1 in range(n_c):
            for c2 in range(n_c):
                row = baseline_g2z[c1, c2]
                if row.sum() == 0:
                    continue
                g = int(row.argmax())
                base_gt = diploid_genotypes[g]

                mat_glab = population.registry.glab_to_index[
                    population.registry.index_to_gtype[c1][1]
                ]
                pat_glab = population.registry.glab_to_index[
                    population.registry.index_to_gtype[c2][1]
                ]

                # current_freqs holds the distribution of genotypes derived from this (c1,c2) pairing.
                # Initially, 100% of the zygotes form the `base_gt` (the normal Mendelian union).
                current_freqs: Dict[Genotype, float] = {base_gt: 1.0}

                # Evaluate rules sequentially (Cascade pipeline).
                # Each rule receives the entire probability distribution from the previous rule,
                # splitting it further based on its conversion rates.
                for rule, mat_glab_req, pat_glab_req in resolved_rules:
                    assert isinstance(rule, (ZygoteGenotypeConversionRule, ZygoteAlleleConversionRule)), \
                    "Resolved rules must be instances of ZygoteGenotypeConversionRule or ZygoteAlleleConversionRule"
                    # glab filters on maternal (c1) and/or paternal (c2) gamete tags
                    if mat_glab_req is not None and mat_glab != mat_glab_req:
                        continue
                    if pat_glab_req is not None and pat_glab != pat_glab_req:
                        continue

                    # next_freqs accumulates the genotypes formed after THIS rule applies.
                    next_freqs: Dict[Genotype, float] = {}
                    for gt, prob in current_freqs.items():
                        if prob <= 1e-12:
                            continue

                        # ----- Genotype-level rule -----
                        if isinstance(rule, ZygoteGenotypeConversionRule):
                            if rule.matches(gt):
                                # The genotype matches the rule. Split the probability:
                                # - (1 - rate) fails conversion and remains unchanged.
                                # - (rate) succeeds and alters the genotype entirely.
                                replacement_gt = rule.replacement(gt)
                                next_freqs[gt] = next_freqs.get(gt, 0.0) + prob * (1.0 - rule.rate)
                                next_freqs[replacement_gt] = next_freqs.get(replacement_gt, 0.0) + prob * rule.rate
                            else:
                                # Rule does not match; genotype passes through untouched.
                                next_freqs[gt] = next_freqs.get(gt, 0.0) + prob

                        # ----- Allele-level rule -----
                        else:
                            # The rule targets specific alleles. It will mathematically expand all combinations
                            # of allele conversions based on diploid zygosity (homozygous/heterozygous).
                            if rule.applies_to_genotype(gt):
                                outcomes = _convert_diploid_genotype_to_gts(gt, rule)
                                if outcomes is not None:
                                    for out_gt, out_prob in outcomes.items():
                                        # Multiply the current branch probability by the rule's outcome probability
                                        next_freqs[out_gt] = next_freqs.get(out_gt, 0.0) + prob * out_prob
                                else:
                                    # No valid targets found for this allele rule inside the genotype
                                    next_freqs[gt] = next_freqs.get(gt, 0.0) + prob
                            else:
                                next_freqs[gt] = next_freqs.get(gt, 0.0) + prob

                    # The output of this rule becomes the input for the next rule.
                    # This enables tracking sequences like: Embyro edits -> CRISPR cutting -> NHEJ resistance.
                    current_freqs = next_freqs

                # Clean up and map the final Genotype objects back to integer indices for the C-core array.
                final_dist: Dict[int, float] = {}
                base_idx = genotype_index[base_gt]

                for gt, prob in current_freqs.items():
                    if prob > 1e-12:
                        idx = genotype_index.get(gt)
                        if idx is not None:
                            final_dist[idx] = final_dist.get(idx, 0.0) + prob

                # If there's a difference from pure baseline genotype
                if not (len(final_dist) == 1 and final_dist.get(base_idx) == 1.0):
                    result[(c1, c2)] = final_dist

        return result

    return zygote_modifier_func

ZygoteGenotypeConversionRule

ZygoteGenotypeConversionRule(genotype_match: Union[Callable[[Genotype], bool], Genotype], to_genotype: Union[Genotype, Callable[[Genotype], Genotype]], rate: float, name: Optional[str] = None, maternal_glab: Optional[Union[str, int]] = None, paternal_glab: Optional[Union[str, int]] = None)

Defines a single zygote-level genotype conversion rule.

A rule specifies that when a zygote's resulting diploid genotype satisfies genotype_match, it may be converted to to_genotype with the given rate.

The genotype_match predicate operates on the Genotype that would normally result from fertilization before the modifier is applied.

Examples:

# Whenever the zygote would be AA, convert to Aa with 80% probability
rule = ZygoteGenotypeConversionRule(
    genotype_match=lambda g: g.is_homozygous_at(locus_A),
    to_genotype=Aa_genotype,
    rate=0.8,
)

Initialise a zygote conversion rule.

Parameters:

Name Type Description Default
genotype_match Union[Callable[[Genotype], bool], Genotype]

Either a specific Genotype instance (matched by identity) or a callable (Genotype) -> bool predicate.

required
to_genotype Union[Genotype, Callable[[Genotype], Genotype]]

The replacement genotype. May be a concrete Genotype or a callable (original_genotype) -> Genotype for dynamic replacement.

required
rate float

Probability of conversion, in [0, 1].

required
name Optional[str]

Human-readable label.

None
maternal_glab Optional[Union[str, int]]

Optional gamete-label filter on the maternal gamete (c1). If specified, the rule only fires when the maternal gamete carries this glab (str name or int index). None means no filtering on the maternal side.

None
paternal_glab Optional[Union[str, int]]

Optional gamete-label filter on the paternal gamete (c2). Same semantics as maternal_glab but for the paternal contribution.

None

Raises:

Type Description
ValueError

If rate is not in [0, 1].

Source code in src/natal/modifiers/zygote_conversion.py
def __init__(
    self,
    genotype_match: Union[Callable[[Genotype], bool], Genotype],
    to_genotype: Union[Genotype, Callable[[Genotype], Genotype]],
    rate: float,
    name: Optional[str] = None,
    maternal_glab: Optional[Union[str, int]] = None,
    paternal_glab: Optional[Union[str, int]] = None,
):
    """Initialise a zygote conversion rule.

    Args:
        genotype_match: Either a specific ``Genotype`` instance (matched
            by identity) or a callable ``(Genotype) -> bool`` predicate.
        to_genotype: The replacement genotype.  May be a concrete
            ``Genotype`` or a callable ``(original_genotype) -> Genotype``
            for dynamic replacement.
        rate: Probability of conversion, in [0, 1].
        name: Human-readable label.
        maternal_glab: Optional gamete-label filter on the *maternal*
            gamete (c1).  If specified, the rule only fires when the
            maternal gamete carries this glab (str name or int index).
            ``None`` means no filtering on the maternal side.
        paternal_glab: Optional gamete-label filter on the *paternal*
            gamete (c2).  Same semantics as *maternal_glab* but for
            the paternal contribution.

    Raises:
        ValueError: If *rate* is not in [0, 1].
    """
    if not 0 <= rate <= 1:
        raise ValueError(f"rate must be in [0, 1], got {rate}")

    if isinstance(genotype_match, Genotype):
        _gt = genotype_match
        self._match_fn: Callable[[Genotype], bool] = lambda g, _gt=_gt: g is _gt
    elif callable(genotype_match):
        self._match_fn = genotype_match
    else:
        raise TypeError(
            "genotype_match must be a Genotype instance or a callable"
        )

    if isinstance(to_genotype, Genotype):
        _tg = to_genotype
        self._replacement_fn: Callable[[Genotype], Genotype] = lambda g, _tg=_tg: _tg
    elif callable(to_genotype):
        self._replacement_fn = to_genotype
    else:
        raise TypeError(
            "to_genotype must be a Genotype instance or a callable"
        )

    self.genotype_match = genotype_match
    self.to_genotype = to_genotype
    self.rate = rate
    self.name = name or f"ZygoteConversion(rate={rate})"
    self.maternal_glab = maternal_glab
    self.paternal_glab = paternal_glab
matches
matches(genotype: Genotype) -> bool

Return True if genotype satisfies this rule's match predicate.

Source code in src/natal/modifiers/zygote_conversion.py
def matches(self, genotype: Genotype) -> bool:
    """Return True if *genotype* satisfies this rule's match predicate."""
    return self._match_fn(genotype)
replacement
replacement(genotype: Genotype) -> Genotype

Return the replacement Genotype for a matched original.

Source code in src/natal/modifiers/zygote_conversion.py
def replacement(self, genotype: Genotype) -> Genotype:
    """Return the replacement Genotype for a matched original."""
    return self._replacement_fn(genotype)

GeneticPreset

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

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.

None
priority int

Execution order — lower values apply first. Same priority uses registration order (stable sort).

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

    Args:
        name: Optional human-readable name for the preset.
        species: Optional species bound at construction time.
        priority: Execution order — lower values apply first.
            Same priority uses registration order (stable sort).
    """
    self.name = name or self.__class__.__name__
    self.priority = priority
    self.hook_id: Optional[int] = None
    self._bound_species: Optional[Species] = species
    self._custom_fitness_patch: Optional[Callable[[], Optional[PresetFitnessPatch]]] = None
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/presets/_base.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, ztype_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/presets/_base.py
@abstractmethod
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Return a gamete modifier or None.

    The modifier should return:

        Dict[(sex_idx, ztype_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/presets/_base.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/presets/_base.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/presets/_base.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/presets/_base.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.population.base.BasePopulation.apply_preset - Preferred modern API

Source code in src/natal/presets/_base.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.population.base.BasePopulation.apply_preset` - Preferred modern API
    """
    apply_preset_to_population(population, self)

CytoplasmicPreset

CytoplasmicPreset(name: str = '', species: Optional[Species] = None, priority: int = 0)

Bases: GeneticPreset

Base class for maternally-inherited cytoplasmic elements.

Child slab = mother slab regardless of father. The mechanism: 1. Gamete tagging happens externally during slab expansion (via build_population_config) — the non-default glab/slab pairs are auto-detected by convention. gamete_modifier returns None (no per-modifier tagging). 2. apply_zygote_redirect (called during zygote map expansion) redirects tagged gamete pairs from slab-0 to the correct child slab.

Subclasses must provide _maternal_map — a dict mapping {maternal_slab_name: glab_name}. Each maternal slab that should be heritable gets a unique glab for tagging.

Example (Wolbachia): _maternal_map = {"infected": "wolbachia"}

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

    Args:
        name: Optional human-readable name for the preset.
        species: Optional species bound at construction time.
        priority: Execution order — lower values apply first.
            Same priority uses registration order (stable sort).
    """
    self.name = name or self.__class__.__name__
    self.priority = priority
    self.hook_id: Optional[int] = None
    self._bound_species: Optional[Species] = species
    self._custom_fitness_patch: Optional[Callable[[], Optional[PresetFitnessPatch]]] = None
gamete_modifier
gamete_modifier(population: BasePopulation[Any]) -> Optional[GameteModifier]

Deferred — tagging handled in build_population_config expansion.

Source code in src/natal/presets/cytoplasmic.py
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Deferred — tagging handled in build_population_config expansion."""
    return None
zygote_modifier
zygote_modifier(population: BasePopulation[Any]) -> Optional[ZygoteModifier]

Return no zygote-level modifier — redirection is done post-expansion.

Source code in src/natal/presets/cytoplasmic.py
def zygote_modifier(self, population: 'BasePopulation[Any]') -> Optional[ZygoteModifier]:
    """Return no zygote-level modifier — redirection is done post-expansion."""
    return None
apply_zygote_redirect staticmethod
apply_zygote_redirect(z2g_expanded: NDArray[float64], glab_name: str, slab_name: str, gamete_labels: List[str], somatic_labels: List[str], n_slabs: int, n_genotypes_raw: int, n_hg: int, n_glabs: int) -> None

Redirect zygote columns: glab-tagged maternal gametes → target slab.

Looks up glab_name and slab_name in the label lists (not the registry — this runs in build_population_config which has no registry access). No-op if either label is missing.

Source code in src/natal/presets/cytoplasmic.py
@staticmethod
def apply_zygote_redirect(
    z2g_expanded: NDArray[np.float64],
    glab_name: str,
    slab_name: str,
    gamete_labels: List[str],
    somatic_labels: List[str],
    n_slabs: int,
    n_genotypes_raw: int,
    n_hg: int,
    n_glabs: int,
) -> None:
    """Redirect zygote columns: glab-tagged maternal gametes → target slab.

    Looks up *glab_name* and *slab_name* in the label lists (not
    the registry — this runs in build_population_config which has
    no registry access).  No-op if either label is missing.
    """
    if glab_name not in gamete_labels or slab_name not in somatic_labels:
        return
    glab_idx = gamete_labels.index(glab_name)
    slab_idx = somatic_labels.index(slab_name)
    for g_raw in range(n_genotypes_raw):
        z_dst = g_raw * n_slabs + slab_idx
        z_src = g_raw * n_slabs + 0
        for hg_f in range(n_hg):
            hl_f = hg_f * n_glabs + glab_idx
            for hg_m in range(n_hg):
                for gm in range(n_glabs):
                    hl_m = hg_m * n_glabs + gm
                    val = z2g_expanded[hl_f, hl_m, z_src]
                    if val > 0:
                        z2g_expanded[hl_f, hl_m, z_dst] += val
                        z2g_expanded[hl_f, hl_m, z_src] = 0.0
tag_maternal_gametes staticmethod
tag_maternal_gametes(z2g_expanded: NDArray[float64], gamete_labels: List[str], somatic_labels: List[str], n_genotypes: int, n_gtypes: int, n_glabs: int, n_slabs: int) -> None

Tag maternal gametes so non-default glabs are inherited maternally.

Operates on the slab-expanded z2g map (post-expansion, shape (2, G×S, HL)). For each matching glab/slab index pair (glab[i] ↔ slab[i] for i >= 1), copies the default-glab female gamete probabilities to the non-default glab column — and zeros out the original — so that only mothers carrying the matching slab produce gametes with the non-default glab.

This method should be called after slab expansion and before redirect_zygotes.

Parameters:

Name Type Description Default
z2g_expanded NDArray[float64]

Slab-expanded genotype-to-gamete map, shape (2, G×S, HL). Modified in-place.

required
gamete_labels List[str]

Ordered gamete label names.

required
somatic_labels List[str]

Ordered somatic label names.

required
n_genotypes int

Pre-expansion genotype count G.

required
n_gtypes int

Number of haploid genotype types (HL / n_glabs).

required
n_glabs int

Number of gamete label types.

required
n_slabs int

Number of somatic label types.

required
Source code in src/natal/presets/cytoplasmic.py
@staticmethod
def tag_maternal_gametes(
    z2g_expanded: NDArray[np.float64],
    gamete_labels: List[str],
    somatic_labels: List[str],
    n_genotypes: int,
    n_gtypes: int,
    n_glabs: int,
    n_slabs: int,
) -> None:
    """Tag maternal gametes so non-default glabs are inherited maternally.

    Operates on the **slab-expanded** z2g map (post-expansion, shape
    ``(2, G×S, HL)``).  For each matching glab/slab index pair
    (glab[i] ↔ slab[i] for i >= 1), copies the default-glab female
    gamete probabilities to the non-default glab column — and zeros
    out the original — so that only mothers carrying the matching
    slab produce gametes with the non-default glab.

    This method should be called **after** slab expansion and **before**
    redirect_zygotes.

    Args:
        z2g_expanded: Slab-expanded genotype-to-gamete map, shape
            ``(2, G×S, HL)``.  Modified in-place.
        gamete_labels: Ordered gamete label names.
        somatic_labels: Ordered somatic label names.
        n_genotypes: Pre-expansion genotype count ``G``.
        n_gtypes: Number of haploid genotype types (HL / n_glabs).
        n_glabs: Number of gamete label types.
        n_slabs: Number of somatic label types.
    """
    if not gamete_labels or not somatic_labels or n_glabs < 2 or n_slabs < 2:
        return
    for idx in range(1, min(n_slabs, n_glabs)):
        if idx >= len(gamete_labels) or idx >= len(somatic_labels):
            continue
        for g_raw in range(n_genotypes):
            z_target = g_raw * n_slabs + idx
            for hg_idx in range(n_gtypes):
                src = hg_idx * n_glabs + 0   # default glab
                dst = hg_idx * n_glabs + idx
                z2g_expanded[0, z_target, dst] = z2g_expanded[0, z_target, src]
                z2g_expanded[0, z_target, src] = 0.0
redirect_zygotes staticmethod
redirect_zygotes(g2z_expanded: NDArray[float64], gamete_labels: List[str], somatic_labels: List[str], n_genotypes: int, n_gtypes: int, n_glabs: int, n_slabs: int) -> None

Redirect zygote columns: glab-tagged gamete pairs → matching child slab.

Iterates matching glab/slab index pairs (glab[i] ↔ slab[i] for i >= 1) and calls :meth:apply_zygote_redirect for each pair. That method moves zygote probabilities from the default slab-0 column to the matching child slab column when the maternal gamete carries the glab tag — enforcing strict maternal inheritance.

This method should be called after slab expansion and tag_maternal_gametes.

Parameters:

Name Type Description Default
g2z_expanded NDArray[float64]

Slab-expanded gametes-to-zygote map, shape (HL, HL, G×S). Modified in-place.

required
gamete_labels List[str]

Ordered gamete label names.

required
somatic_labels List[str]

Ordered somatic label names.

required
n_genotypes int

Pre-expansion genotype count G.

required
n_gtypes int

Number of haploid genotype types (HL / n_glabs).

required
n_glabs int

Number of gamete label types.

required
n_slabs int

Number of somatic label types.

required
Source code in src/natal/presets/cytoplasmic.py
@staticmethod
def redirect_zygotes(
    g2z_expanded: NDArray[np.float64],
    gamete_labels: List[str],
    somatic_labels: List[str],
    n_genotypes: int,
    n_gtypes: int,
    n_glabs: int,
    n_slabs: int,
) -> None:
    """Redirect zygote columns: glab-tagged gamete pairs → matching child slab.

    Iterates matching glab/slab index pairs (glab[i] ↔ slab[i] for
    i >= 1) and calls :meth:`apply_zygote_redirect` for each pair.
    That method moves zygote probabilities from the default slab-0
    column to the matching child slab column when the maternal gamete
    carries the glab tag — enforcing strict maternal inheritance.

    This method should be called **after** slab expansion and
    ``tag_maternal_gametes``.

    Args:
        g2z_expanded: Slab-expanded gametes-to-zygote map, shape
            ``(HL, HL, G×S)``.  Modified in-place.
        gamete_labels: Ordered gamete label names.
        somatic_labels: Ordered somatic label names.
        n_genotypes: Pre-expansion genotype count ``G``.
        n_gtypes: Number of haploid genotype types (HL / n_glabs).
        n_glabs: Number of gamete label types.
        n_slabs: Number of somatic label types.
    """
    if not gamete_labels or not somatic_labels or n_glabs < 2 or n_slabs < 2:
        return
    for idx in range(1, min(n_slabs, n_glabs)):
        if idx >= len(gamete_labels) or idx >= len(somatic_labels):
            continue
        CytoplasmicPreset.apply_zygote_redirect(
            g2z_expanded, gamete_labels[idx], somatic_labels[idx],
            gamete_labels, somatic_labels,
            n_slabs, n_genotypes,
            n_gtypes, n_glabs,
        )

TransgenicBackground

TransgenicBackground(name: str, tg_slab: str, wt_slab: str = 'WT_bg', fecundity_scaling: float = 1.0, viability_scaling: Optional[float] = None, species: Optional[Species] = None, priority: int = 0)

Bases: GeneticPreset

Fitness scaling for a transgenic background slab.

Applies fecundity and/or viability scaling to individuals carrying the tg_slab somatic label. Does NOT implement outcrossing clearance — that requires a separate inheritance mechanism.

Initialize a TransgenicBackground preset.

Parameters:

Name Type Description Default
name str

Preset name.

required
tg_slab str

Somatic slab label for transgenic individuals.

required
wt_slab str

Somatic slab label for wild-type background.

'WT_bg'
fecundity_scaling float

Fecundity multiplier for transgenic carriers.

1.0
viability_scaling Optional[float]

Optional viability multiplier for transgenic carriers. None means no viability effect.

None
species Optional[Species]

Optional species for validation.

None
priority int

Modifier and fitness application priority.

0
Source code in src/natal/presets/cytoplasmic.py
def __init__(
    self,
    name: str,
    tg_slab: str,
    wt_slab: str = "WT_bg",
    fecundity_scaling: float = 1.0,
    viability_scaling: Optional[float] = None,
    species: Optional[Species] = None,
    priority: int = 0,
):
    """Initialize a TransgenicBackground preset.

    Args:
        name: Preset name.
        tg_slab: Somatic slab label for transgenic individuals.
        wt_slab: Somatic slab label for wild-type background.
        fecundity_scaling: Fecundity multiplier for transgenic carriers.
        viability_scaling: Optional viability multiplier for transgenic
            carriers. ``None`` means no viability effect.
        species: Optional species for validation.
        priority: Modifier and fitness application priority.
    """
    super().__init__(name=name, species=species, priority=priority)
    self.tg_slab = tg_slab
    self.wt_slab = wt_slab
    self.fecundity_scaling = fecundity_scaling
    self.viability_scaling = viability_scaling
gamete_modifier
gamete_modifier(population: BasePopulation[Any]) -> Optional[GameteModifier]

Return no gamete modifier — transgenic background is slab-only.

Source code in src/natal/presets/cytoplasmic.py
def gamete_modifier(self, population: 'BasePopulation[Any]') -> Optional[GameteModifier]:
    """Return no gamete modifier — transgenic background is slab-only."""
    return None
zygote_modifier
zygote_modifier(population: BasePopulation[Any]) -> Optional[ZygoteModifier]

Return no zygote modifier — transgenic background is slab-only.

Source code in src/natal/presets/cytoplasmic.py
def zygote_modifier(self, population: 'BasePopulation[Any]') -> Optional[ZygoteModifier]:
    """Return no zygote modifier — transgenic background is slab-only."""
    return None
fitness_patch
fitness_patch() -> PresetFitnessPatch

Build fitness patch applying fecundity and optional viability scaling.

Returns:

Type Description
PresetFitnessPatch

A fitness patch dict with fecundity_per_slab and optionally

PresetFitnessPatch

viability_per_slab entries for the transgenic slab.

Source code in src/natal/presets/cytoplasmic.py
def fitness_patch(self) -> PresetFitnessPatch:
    """Build fitness patch applying fecundity and optional viability scaling.

    Returns:
        A fitness patch dict with ``fecundity_per_slab`` and optionally
        ``viability_per_slab`` entries for the transgenic slab.
    """
    patch: PresetFitnessPatch = {}
    patch['fecundity_per_slab'] = {self.tg_slab: self.fecundity_scaling}
    if self.viability_scaling is not None:
        patch['viability_per_slab'] = {self.tg_slab: self.viability_scaling}
    return patch

Wolbachia

Wolbachia(name: str, infected_slab: str = 'infected', normal_slab: str = 'normal', viability_scaling: float = 1.0, fecundity_scaling: Optional[float] = None, species: Optional[Species] = None, priority: int = 0)

Bases: CytoplasmicPreset

Maternally-inherited endosymbiont. Infected mothers pass the infection to all offspring regardless of the father.

Requires Species with
  • gamete_labels including "wolbachia"
  • somatic_labels including "normal", "infected"

Initialize a Wolbachia cytoplasmic preset.

Parameters:

Name Type Description Default
name str

Preset name.

required
infected_slab str

Somatic slab label for infected individuals.

'infected'
normal_slab str

Somatic slab label for uninfected individuals.

'normal'
viability_scaling float

Viability multiplier for infected carriers.

1.0
fecundity_scaling Optional[float]

Fecundity multiplier for infected carriers. None means no fecundity effect.

None
species Optional[Species]

Optional species for validation.

None
priority int

Modifier and fitness application priority.

0
Source code in src/natal/presets/cytoplasmic.py
def __init__(
    self,
    name: str,
    infected_slab: str = "infected",
    normal_slab: str = "normal",
    viability_scaling: float = 1.0,
    fecundity_scaling: Optional[float] = None,
    species: Optional[Species] = None,
    priority: int = 0,
):
    """Initialize a Wolbachia cytoplasmic preset.

    Args:
        name: Preset name.
        infected_slab: Somatic slab label for infected individuals.
        normal_slab: Somatic slab label for uninfected individuals.
        viability_scaling: Viability multiplier for infected carriers.
        fecundity_scaling: Fecundity multiplier for infected carriers.
            ``None`` means no fecundity effect.
        species: Optional species for validation.
        priority: Modifier and fitness application priority.
    """
    super().__init__(name=name, species=species, priority=priority)
    self._maternal_map = {infected_slab: "wolbachia"}
    self.infected_slab = infected_slab
    self.normal_slab = normal_slab
    self.viability_scaling = viability_scaling
    self.fecundity_scaling = fecundity_scaling
fitness_patch
fitness_patch() -> PresetFitnessPatch

Build fitness patch applying viability and fecundity scaling.

Returns:

Type Description
PresetFitnessPatch

A fitness patch dict with optional viability_per_slab and

PresetFitnessPatch

fecundity_per_slab entries for the infected slab.

Source code in src/natal/presets/cytoplasmic.py
def fitness_patch(self) -> PresetFitnessPatch:
    """Build fitness patch applying viability and fecundity scaling.

    Returns:
        A fitness patch dict with optional ``viability_per_slab`` and
        ``fecundity_per_slab`` entries for the infected slab.
    """
    patch: PresetFitnessPatch = {}
    patch['viability_per_slab'] = {self.infected_slab: self.viability_scaling}
    if self.fecundity_scaling is not None:
        patch['fecundity_per_slab'] = {self.infected_slab: self.fecundity_scaling}
    return patch

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[Any] = None, priority: int = 0, 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/presets/homing.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[Any] = None,
    priority: int = 0,
    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, priority=priority)
drive_allele property
drive_allele: Gene

Gene: The drive allele (e.g. the Cas9/gRNA construct).

target_allele property
target_allele: Gene

Gene: The wild-type allele targeted for cleavage.

resistance_genotype property
resistance_genotype: Gene

Gene: The resistance allele formed by NHEJ repair.

Raises:

Type Description
ValueError

If no resistance allele was configured.

functional_resistance_allele property
functional_resistance_allele: Optional[Gene]

Gene or None: The functional resistance allele, if configured.

cas9_allele property
cas9_allele: Optional[Gene]

Gene or None: The Cas9 source allele, if different from drive_allele.

fitness_patch
fitness_patch() -> PresetFitnessPatch

Return declarative fitness patch for homing drive scaling configs.

Source code in src/natal/presets/homing.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/presets/homing.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:
        """Return True if the genotype carries both drive and Cas9 alleles."""
        from natal.presets._types 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/presets/homing.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).

        Args:
            gt: The zygote genotype to check.

        Returns:
            True if the zygote carries cas9_allele or drive_allele.
        """
        from natal.presets._types 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[Any] = None, priority: int = 0, 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[Any]

Optional species to bind at construction.

None
use_paternal_deposition bool

Whether to enable paternal Cas9 deposition.

False
Source code in src/natal/presets/toxin_antidote.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[Any] = None,
    priority: int = 0,
    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, priority=priority)
drive_allele property
drive_allele: Gene

Gene: The drive allele carrying the toxin-antidote construct.

target_allele property
target_allele: Gene

Gene: The wild-type allele targeted for disruption.

disrupted_allele property
disrupted_allele: Gene

Gene: The disrupted (cleaved) allele produced by target disruption.

fitness_patch
fitness_patch() -> PresetFitnessPatch

Return declarative fitness patch for the disrupted allele.

Source code in src/natal/presets/toxin_antidote.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/presets/toxin_antidote.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 True if the genotype carries at least one drive allele."""
        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/presets/toxin_antidote.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 True if the zygote carries at least one drive allele."""
        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/presets/_base.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
    """
    from natal.fitness._patch import apply_preset_fitness_patch

    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)

apply_preset_fitness_patch

apply_preset_fitness_patch(deps: FitnessPopulationView, patch: PresetFitnessPatch) -> None

Apply a declarative preset fitness patch to population config tensors.

Patch schema (all keys optional): - viability: Dict[genotype_selector, _ViabilityScalingConfig] - fecundity: Dict[genotype_selector, _FecundityScalingConfig] - sexual_selection: Dict[female_selector, Union[float, Dict[male_selector, float]]]

Source code in src/natal/fitness/_patch.py
def apply_preset_fitness_patch(deps: FitnessPopulationView, patch: PresetFitnessPatch) -> None:
    """Apply a declarative preset fitness patch to population config tensors.

    Patch schema (all keys optional):
    - viability: Dict[genotype_selector, _ViabilityScalingConfig]
    - fecundity: Dict[genotype_selector, _FecundityScalingConfig]
    - sexual_selection: Dict[female_selector, Union[float, Dict[male_selector, float]]]
    """
    if not patch:
        return

    all_genotypes = deps.index_registry.index_to_genotype

    # ----------------------------------------------------------------------
    # 1) Selector-based viability patch
    #
    # Input examples:
    # - {"A1|A1": 0.8}
    # - {"A1|A1": {0: 0.9, 1: 0.8}}
    # - {"A1|A1": {"female": 0.9, "male": {0: 0.95}}}
    # ----------------------------------------------------------------------
    viability_patch = patch.get('viability', {})
    for selector, config in viability_patch.items():
        matched = deps.species.resolve_genotype_selectors(
            selector=selector,
            all_genotypes=all_genotypes,
            context='preset.viability',
        )
        for genotype in matched:
            z_indices = deps.index_registry.ztype_indices_for(genotype)

            # scalar: both sexes at default viability age
            if isinstance(config, (int, float)):
                for z_idx in z_indices:
                    deps.config.set_viability_fitness(0, z_idx, float(config))
                    deps.config.set_viability_fitness(1, z_idx, float(config))
                continue

            # age-specific for both sexes: {age: scale}
            config_map = cast(Mapping[object, object], config)
            if _is_simple_age_scale_map(config_map):
                for z_idx in z_indices:
                    for age, scale in config_map.items():
                        deps.config.set_viability_fitness(0, z_idx, float(scale), int(age))
                        deps.config.set_viability_fitness(1, z_idx, float(scale), int(age))
                continue

            # sex-specific: {sex: float | {age: scale}}
            for sex_key, sex_config in config_map.items():
                sex_idx = _normalize_sex_key(_coerce_sex_specifier(sex_key))
                if isinstance(sex_config, (int, float)):
                    for z_idx in z_indices:
                        deps.config.set_viability_fitness(sex_idx, z_idx, float(sex_config))
                elif isinstance(sex_config, Mapping):
                    sex_age_map = cast(Mapping[object, object], sex_config)
                    for z_idx in z_indices:
                        for age, scale in sex_age_map.items():
                            if not isinstance(age, int) or not isinstance(scale, (int, float)):
                                raise TypeError(
                                    f"Invalid viability sex-age config for selector '{selector}', sex '{sex_key}'"
                                )
                            deps.config.set_viability_fitness(sex_idx, z_idx, float(scale), int(age))
                else:
                    raise TypeError(
                        f"Invalid viability sex config for selector '{selector}', sex '{sex_key}': "
                        f"{type(sex_config).__name__}"
                    )

    # ----------------------------------------------------------------------
    # 2) Selector-based fecundity patch
    #
    # Input examples:
    # - {"A1|A1": 0.8}
    # - {"A1|A1": {"female": 0.9, "male": 0.7}}
    # ----------------------------------------------------------------------
    fecundity_patch = patch.get('fecundity', {})
    for selector, config in fecundity_patch.items():
        matched = deps.species.resolve_genotype_selectors(
            selector=selector,
            all_genotypes=all_genotypes,
            context='preset.fecundity',
        )
        for genotype in matched:
            z_indices = deps.index_registry.ztype_indices_for(genotype)

            if isinstance(config, (int, float)):
                for z_idx in z_indices:
                    deps.config.set_fecundity_fitness(0, z_idx, float(config))
                    deps.config.set_fecundity_fitness(1, z_idx, float(config))
                continue

            config_map = cast(Mapping[object, object], config)
            for sex_key, scale in config_map.items():
                sex_idx = _normalize_sex_key(_coerce_sex_specifier(sex_key))
                if not isinstance(scale, (int, float)):
                    raise TypeError(
                        f"Invalid fecundity sex scale for selector '{selector}', sex '{sex_key}'"
                    )
                for z_idx in z_indices:
                    deps.config.set_fecundity_fitness(sex_idx, z_idx, float(scale))

    # ----------------------------------------------------------------------
    # 3) Selector-based sexual-selection patch
    #
    # Input examples:
    # - {"female_selector": 0.9}  # shorthand for all males
    # - {"female_selector": {"male_selector": 1.2}}
    # ----------------------------------------------------------------------
    sexual_selection_patch = patch.get('sexual_selection', {})
    for female_selector, male_config in sexual_selection_patch.items():
        female_matched = deps.species.resolve_genotype_selectors(
            selector=female_selector,
            all_genotypes=all_genotypes,
            context='preset.sexual_selection(female)',
        )

        # Allow shorthand: female_selector -> scalar means all-male targets
        if isinstance(male_config, (int, float)):
            male_map = {'*': float(male_config)}
        else:
            male_map = cast(Mapping[object, object], male_config)

        for male_selector, scale in male_map.items():
            if not isinstance(scale, (int, float)):
                raise TypeError(
                    f"Invalid sexual_selection scale for female selector '{female_selector}'"
                )
            male_matched = deps.species.resolve_genotype_selectors(
                selector=_coerce_selector(male_selector),
                all_genotypes=all_genotypes,
                context='preset.sexual_selection(male)',
            )
            for f_genotype in female_matched:
                f_z_indices = deps.index_registry.ztype_indices_for(f_genotype)
                for m_genotype in male_matched:
                    m_z_indices = deps.index_registry.ztype_indices_for(m_genotype)
                    for f_z in f_z_indices:
                        for m_z in m_z_indices:
                            deps.config.set_sexual_selection_fitness(f_z, m_z, float(scale))

    # ----------------------------------------------------------------------
    # 4) Allele-based patches
    #
    # This layer expands allele-centric config into genotype-level writes:
    # 1) resolve allele name(s) to Gene objects
    # 2) count target copies per genotype (0/1/2)
    # 3) convert copies -> factor according to mode
    # 4) multiply corresponding tensor cells
    # ----------------------------------------------------------------------
    viability_per_allele_patch = patch.get('viability_per_allele', {})
    for allele_name, val in viability_per_allele_patch.items():
        config, mode = _split_config_mode(val)
        if not _is_viability_scaling_config(config):
            raise TypeError(f"Invalid viability_per_allele config for '{allele_name}'")
        _apply_viability_allele_scaling(deps, all_genotypes, allele_name, config, mode)

    fecundity_per_allele_patch = patch.get('fecundity_per_allele', {})
    for allele_name, val in fecundity_per_allele_patch.items():
        config, mode = _split_config_mode(val)
        if not _is_fecundity_scaling_config(config):
            raise TypeError(f"Invalid fecundity_per_allele config for '{allele_name}'")
        _apply_fecundity_allele_scaling(deps, all_genotypes, allele_name, config, mode)

    sexual_selection_per_allele_patch = patch.get('sexual_selection_per_allele', {})
    for allele_name, val in sexual_selection_per_allele_patch.items():
        config, mode = _split_config_mode(val)
        if not _is_sexual_selection_scaling_config(config):
            raise TypeError(f"Invalid sexual_selection_per_allele config for '{allele_name}'")
        _apply_sexual_selection_allele_scaling(deps, all_genotypes, allele_name, config, mode)

    # 5) Zygote fitness patch
    zygote_patch = patch.get('zygote', {})
    for selector, config in zygote_patch.items():
        matched = deps.species.resolve_genotype_selectors(
            selector=selector,
            all_genotypes=all_genotypes,
            context='preset.zygote',
        )
        for genotype in matched:
            z_indices = deps.index_registry.ztype_indices_for(genotype)
            if isinstance(config, (int, float)):
                for z_idx in z_indices:
                    deps.config.set_zygote_viability_fitness(0, z_idx, float(config))
                    deps.config.set_zygote_viability_fitness(1, z_idx, float(config))
            elif isinstance(config, Mapping):
                config_map = cast(Mapping[object, object], config)
                for sex_key, sex_config in config_map.items():
                    sex_idx = _normalize_sex_key(_coerce_sex_specifier(sex_key))
                    if isinstance(sex_config, (int, float)):
                        for z_idx in z_indices:
                            deps.config.set_zygote_viability_fitness(sex_idx, z_idx, float(sex_config))

    # 6) Zygote allele-based fitness patch
    zygote_per_allele_patch = patch.get('zygote_per_allele', {})
    for allele_name, val in zygote_per_allele_patch.items():
        config, mode = _split_config_mode(val)
        if not _is_zygote_viability_scaling_config(config):
            raise TypeError(f"Invalid zygote_per_allele config for '{allele_name}'")
        _apply_zygote_viability_allele_scaling(deps, all_genotypes, allele_name, config, mode)

    # 7) Slab-based fitness patches (per_slab keys — symmetric with per_allele)
    if patch.get('viability_per_slab'):
        _apply_viability_slab_scaling(deps, all_genotypes, patch)
    if patch.get('fecundity_per_slab'):
        _apply_fecundity_slab_scaling(deps, all_genotypes, patch)
    if patch.get('sexual_selection_per_slab'):
        _apply_sexual_selection_slab_scaling(deps, all_genotypes, patch)
    if patch.get('zygote_per_slab'):
        _apply_zygote_slab_scaling(deps, all_genotypes, patch)