Skip to content

patterns Module

Genetic pattern matching and filtering.

Overview

The patterns subpackage implements the syntax and logic for matching genotypes and haploid genomes against symbolic patterns.

Complete Module Reference

natal.patterns

Pattern matching system for genotypes and haploid genomes.

Provides regex-like pattern matching for genetic sequences: - PatternElement: Base class for allele-level matching - HaplotypePath: Pattern for a single DNA strand of one chromosome - ChromosomePairPattern: Pattern for a pair of homologous chromosomes - GenotypePattern: Pattern for a complete diploid genotype - HaploidGenomePattern: Pattern for a complete haploid genome - GenotypePatternParser: Parser for pattern syntax strings - GenotypeSelector: Unified genotype selector for observation/filtering

PatternParseError

Bases: Exception

Error raised during genotype pattern parsing.

Indicates that a pattern string could not be parsed due to invalid syntax, unsupported constructs, or species context mismatches.

LabPattern

LabPattern(lab: Optional[str] = None, negate: bool = False, lab_set: Optional[Set[str]] = None)

Pattern for matching gamete / somatic label names.

Supports the same syntax as allele patterns
  • @cas9_high — exact match
  • @!cas9_high — any label except "cas9_high"
  • @{cas9_high,cas9_low} — any label in the set
  • @!{cas9_high,cas9_low} — any label NOT in the set

When lab and lab_set are both None the pattern matches any label (equivalent to omitting the @ suffix entirely).

Attributes:

Name Type Description
lab Optional[str]

Exact label to match, or None.

negate bool

If True, negate the matching condition.

lab_set Optional[Set[str]]

Set of labels for set matching.

Initialize a LabPattern.

Parameters:

Name Type Description Default
lab Optional[str]

Exact label name to match, or None for wildcard.

None
negate bool

If True, match the complement of the constraint.

False
lab_set Optional[Set[str]]

Set of label names for set matching.

None
Source code in src/natal/patterns/elements/atom.py
def __init__(
    self,
    lab: Optional[str] = None,
    negate: bool = False,
    lab_set: Optional[Set[str]] = None,
):
    """Initialize a LabPattern.

    Args:
        lab: Exact label name to match, or ``None`` for wildcard.
        negate: If True, match the complement of the constraint.
        lab_set: Set of label names for set matching.
    """
    self.lab = lab
    self.negate = negate
    self.lab_set = lab_set
matches
matches(value: str) -> bool

Return True if value satisfies this pattern.

Source code in src/natal/patterns/elements/atom.py
def matches(self, value: str) -> bool:
    """Return True if *value* satisfies this pattern."""
    if self.lab_set is not None:
        result = value in self.lab_set
        return not result if self.negate else result
    if self.lab is not None:
        result = value == self.lab
        return not result if self.negate else result
    return True  # wildcard — matches anything
is_wildcard
is_wildcard() -> bool

True if this pattern matches any label (no constraint).

Source code in src/natal/patterns/elements/atom.py
def is_wildcard(self) -> bool:
    """True if this pattern matches any label (no constraint)."""
    return self.lab is None and self.lab_set is None
parse staticmethod
parse(lab_str: str) -> LabPattern

Parse a @lab suffix string into a LabPattern.

Returns a wildcard (matches-everything) pattern for "*" or the empty string.

Source code in src/natal/patterns/elements/atom.py
@staticmethod
def parse(lab_str: str) -> LabPattern:
    """Parse a ``@lab`` suffix string into a ``LabPattern``.

    Returns a wildcard (matches-everything) pattern for ``"*"``
    or the empty string.
    """
    from natal.utils.helpers import validate_name

    s = lab_str.strip()
    if not s or s == "*":
        return LabPattern()

    negate = False
    if s.startswith("!"):
        negate = True
        s = s[1:].strip()

    if s.startswith("{") and s.endswith("}"):
        inner = s[1:-1].strip()
        if not inner:
            raise PatternParseError("Empty lab set {}")
        names = {n.strip() for n in inner.split(",")}
        for name in names:
            if not validate_name(name):
                raise PatternParseError(
                    f"Invalid lab name {name!r} in set. "
                    f"Lab names must match [A-Za-z0-9_]+."
                )
        return LabPattern(lab_set=names, negate=negate)

    # Single name or comma-separated (set without braces)
    if "," in s:
        names = {n.strip() for n in s.split(",")}
        for name in names:
            if not validate_name(name):
                raise PatternParseError(
                    f"Invalid lab name {name!r}. "
                    f"Lab names must match [A-Za-z0-9_]+."
                )
        return LabPattern(lab_set=names, negate=negate)

    if not validate_name(s):
        raise PatternParseError(
            f"Invalid lab name {s!r}. "
            f"Lab names must match [A-Za-z0-9_]+."
        )
    return LabPattern(lab=s, negate=negate)

ZygoteTypePattern

ZygoteTypePattern(genotype: GenotypePattern, slab: Optional[LabPattern] = None)

Pattern for a zygote (diploid genotype) with a slab (somatic) label.

A zygote type pairs a :class:GenotypePattern with an optional :class:LabPattern parsed from the @slab suffix (e.g. A|a@infected). This is the slab-aware equivalent of GenotypePattern — it resolves to a (genotype_index, slab_index) pair used for ZType indexing in config arrays.

Supports both string and tuple construction::

ZygoteTypePattern.parse("A|a@infected", species)
ZygoteTypePattern.from_pair(genotype_obj, "infected", species)

Initialize a ZygoteTypePattern.

Parameters:

Name Type Description Default
genotype GenotypePattern

The genotype pattern to match.

required
slab Optional[LabPattern]

Optional somatic-label pattern parsed from the @slab suffix.

None
Source code in src/natal/patterns/elements/diploid.py
def __init__(
    self,
    genotype: GenotypePattern,
    slab: Optional[LabPattern] = None,
):
    """Initialize a ZygoteTypePattern.

    Args:
        genotype: The genotype pattern to match.
        slab: Optional somatic-label pattern parsed from the ``@slab``
            suffix.
    """
    self.genotype = genotype
    self.slab: Optional[LabPattern] = slab
parse staticmethod
parse(pattern_str: str, species: Species) -> ZygoteTypePattern

Parse a ZType pattern string like "A|a@infected".

The @slab suffix is extracted; everything before it is parsed as a :class:GenotypePattern.

Source code in src/natal/patterns/elements/diploid.py
@staticmethod
def parse(pattern_str: str, species: Species) -> ZygoteTypePattern:
    """Parse a ZType pattern string like ``"A|a@infected"``.

    The ``@slab`` suffix is extracted; everything before it is parsed
    as a :class:`GenotypePattern`.
    """
    from natal.patterns.parser import GenotypePatternParser

    parser = GenotypePatternParser(species)
    # Inline _strip_lab logic to avoid protected-access warning.
    lab: Optional[LabPattern] = None
    base = pattern_str
    if "@" in pattern_str:
        idx = pattern_str.rindex("@")
        base = pattern_str[:idx].strip()
        suffix = pattern_str[idx + 1:].strip()
        if suffix:
            lab = LabPattern.parse(suffix)
    genotype = parser.parse(base)
    return ZygoteTypePattern(genotype, lab)
from_pair staticmethod
from_pair(genotype: Genotype, slab: str, species: Species) -> ZygoteTypePattern

Build a ZygoteTypePattern from a (Genotype, slab_name) pair.

Parameters:

Name Type Description Default
genotype Genotype

A Genotype instance.

required
slab str

Somatic label name.

required
species Species

Species for genotype-string resolution.

required

Returns:

Type Description
ZygoteTypePattern

A ZygoteTypePattern matching the given genotype and slab.

Source code in src/natal/patterns/elements/diploid.py
@staticmethod
def from_pair(
    genotype: Genotype,
    slab: str,
    species: Species,
) -> ZygoteTypePattern:
    """Build a ZygoteTypePattern from a (Genotype, slab_name) pair.

    Args:
        genotype: A Genotype instance.
        slab: Somatic label name.
        species: Species for genotype-string resolution.

    Returns:
        A ZygoteTypePattern matching the given genotype and slab.
    """
    from natal.patterns.parser import GenotypePatternParser

    parser = GenotypePatternParser(species)
    pattern = parser.parse(str(genotype))
    return ZygoteTypePattern(pattern, LabPattern(lab=slab))
from_slab_key staticmethod
from_slab_key(key: str, species: Species) -> ZygoteTypePattern

Parse a genotype key that may include an @slab suffix.

Splits at @, canonicalizes the genotype part via species.get_genotype_from_str, and creates a ZygoteTypePattern with the resolved slab suffix.

Parameters:

Name Type Description Default
key str

Genotype key like "A|a" or "A|a@infected".

required
species Species

Species definition for genotype resolution.

required

Returns:

Type Description
ZygoteTypePattern

A ZygoteTypePattern with the canonical genotype and slab.

Source code in src/natal/patterns/elements/diploid.py
@staticmethod
def from_slab_key(key: str, species: Species) -> ZygoteTypePattern:
    """Parse a genotype key that may include an ``@slab`` suffix.

    Splits at ``@``, canonicalizes the genotype part via
    ``species.get_genotype_from_str``, and creates a ZygoteTypePattern
    with the resolved slab suffix.

    Args:
        key: Genotype key like ``"A|a"`` or ``"A|a@infected"``.
        species: Species definition for genotype resolution.

    Returns:
        A ZygoteTypePattern with the canonical genotype and slab.
    """
    if "@" in key:
        idx = key.rindex("@")
        gt_part = key[:idx]
        slab_part = key[idx:]
    else:
        gt_part = key
        slab_part = ""
    gt = species.get_genotype_from_str(gt_part)
    return ZygoteTypePattern.parse(str(gt) + slab_part, species)
matches
matches(genotype: Genotype, slab_label: str = 'default') -> bool

Check if this pattern matches a (genotype, slab_label) pair.

Source code in src/natal/patterns/elements/diploid.py
def matches(self, genotype: Genotype, slab_label: str = "default") -> bool:
    """Check if this pattern matches a (genotype, slab_label) pair."""
    if not self.genotype.matches(genotype):
        return False
    if self.slab is not None:
        return self.slab.matches(slab_label)
    return True

GameteTypePattern

GameteTypePattern(haplotype_path: HaplotypePath, lab: Optional[LabPattern] = None)

Pattern for a gamete (haploid genome) with optional label constraint.

A gamete type pairs a :class:HaplotypePath (the genetic content across all chromosomes) with an optional :class:LabPattern parsed from the @lab suffix (e.g. A1/B1; C1@cas9_deposited).

Label matching is the caller's responsibility — this class simply stores both components so the parser doesn't silently discard the label.

Initialize a GameteTypePattern.

Parameters:

Name Type Description Default
haplotype_path HaplotypePath

HaplotypePath pattern for the genetic content.

required
lab Optional[LabPattern]

Optional gamete-label constraint.

None
Source code in src/natal/patterns/elements/haploid.py
def __init__(
    self,
    haplotype_path: HaplotypePath,
    lab: Optional[LabPattern] = None,
):
    """Initialize a GameteTypePattern.

    Args:
        haplotype_path: HaplotypePath pattern for the genetic content.
        lab: Optional gamete-label constraint.
    """
    self.haplotype_path = haplotype_path
    self.lab: Optional[LabPattern] = lab

GenotypePatternParser

GenotypePatternParser(species: Species)

Parses genotype pattern strings into GenotypePattern objects.

Parses flexible pattern syntax including wildcards (*), set patterns ({A,B}), negation (!A), unordered pairs (::), bracketed groupings (()), and label suffixes (@lab).

Results are cached per (species, pattern_string) pair for performance.

Initialize parser for a specific species.

Parameters:

Name Type Description Default
species Species

The Species object to use for validation and context.

required
Source code in src/natal/patterns/parser.py
def __init__(self, species: Species):
    """Initialize parser for a specific species.

    Args:
        species: The Species object to use for validation and context.
    """
    self.species = species
parse
parse(pattern_str: str) -> GenotypePattern

Parse a pattern string into a GenotypePattern.

Supported syntax includes
  • ; separates chromosomes (outside parentheses)
  • | separates maternal (left) and paternal (right)
  • / separates loci within a chromosome
  • * matches any allele
  • {A,B,C} matches any allele in the set
  • !A matches any allele except A
  • :: matches unordered pair (A::B matches A|B or B|A)
  • () groups loci within a chromosome
  • @lab suffix selects a somatic label (ZType constraint), e.g. A|a@cas9_high
  • Omitted chromosomes default to wildcard matching (optional)

Parameters:

Name Type Description Default
pattern_str str

The pattern string to parse.

required

Returns:

Type Description
GenotypePattern

A GenotypePattern object.

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/patterns/parser.py
def parse(self, pattern_str: str) -> GenotypePattern:
    """Parse a pattern string into a GenotypePattern.

    Supported syntax includes:
        - ``;`` separates chromosomes (outside parentheses)
        - ``|`` separates maternal (left) and paternal (right)
        - ``/`` separates loci within a chromosome
        - ``*`` matches any allele
        - ``{A,B,C}`` matches any allele in the set
        - ``!A`` matches any allele except A
        - ``::`` matches unordered pair (A::B matches A|B or B|A)
        - ``()`` groups loci within a chromosome
        - ``@lab`` suffix selects a somatic label (ZType constraint),
          e.g. ``A|a@cas9_high``
        - Omitted chromosomes default to wildcard matching (optional)

    Args:
        pattern_str: The pattern string to parse.

    Returns:
        A GenotypePattern object.

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    original = pattern_str.strip()
    pattern_str, lab = self._strip_lab(original)

    # Check cache — use the original string (before @lab stripping) as
    # the cache key so that "A|a" and "A|a@cas9_high" are distinct.
    cache_key = (id(self.species), original)
    if cache_key in self._pattern_cache:
        return self._pattern_cache[cache_key]

    try:
        # Split by semicolon, respecting parentheses
        chr_pattern_strs = self._split_by_semicolon_respecting_parens(pattern_str)

        chromosome_patterns: List[Union[ChromosomePairPattern, Literal["WILDCARD_CHROMOSOME"]]] = []
        for chr_str in chr_pattern_strs:
            chr_pattern = self._parse_chromosome_pair(chr_str)
            chromosome_patterns.append(chr_pattern)

        # Handle wildcard chromosome markers and fill remaining chromosomes
        final_patterns: List[Optional[ChromosomePairPattern]] = []
        for i, pattern in enumerate(chromosome_patterns):
            if pattern == "WILDCARD_CHROMOSOME":
                # Create a fully wildcard pattern for this chromosome
                if i < len(self.species.chromosomes):
                    chromosome = self.species.chromosomes[i]
                    num_loci = len(chromosome.loci)
                    wildcard_patterns = [WildcardPattern() for _ in range(num_loci)]
                    maternal_path = HaplotypePath(wildcard_patterns)
                    paternal_path = HaplotypePath(wildcard_patterns.copy())
                    final_patterns.append(ChromosomePairPattern(maternal_path, paternal_path))
                else:
                    final_patterns.append(None)
            else:
                final_patterns.append(pattern)

        # Fill remaining chromosomes with None
        while len(final_patterns) < len(self.species.chromosomes):
            final_patterns.append(None)

        result = GenotypePattern(final_patterns, lab=lab)
        self._pattern_cache[cache_key] = result
        return result

    except PatternParseError:
        raise
    except Exception as e:
        raise PatternParseError(f"Failed to parse pattern '{pattern_str}'") from e
parse_haplotype_pattern
parse_haplotype_pattern(pattern_str: str) -> GameteTypePattern

Parse a complete haplotype pattern.

Parameters:

Name Type Description Default
pattern_str str

Pattern string for a single haplotype (e.g. "A1/B1; C1" or "A1/B1@cas9_deposited").

required

Returns:

Type Description
GameteTypePattern

GameteTypePattern with haplotype path and optional lab constraint.

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/patterns/parser.py
def parse_haplotype_pattern(self, pattern_str: str) -> GameteTypePattern:
    """Parse a complete haplotype pattern.

    Args:
        pattern_str: Pattern string for a single haplotype
            (e.g. ``"A1/B1; C1"`` or ``"A1/B1@cas9_deposited"``).

    Returns:
        GameteTypePattern with haplotype path and optional lab constraint.

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    pattern_str, lab = self._strip_lab(pattern_str.strip())

    try:
        # Split by semicolon to get loci from all chromosomes
        chr_strs = [s.strip() for s in pattern_str.split(";") if s.strip()]

        all_locus_patterns: List[PatternElement] = []
        for chr_str in chr_strs:
            subbandloci = chr_str.split("/")
            for locus_str in subbandloci:
                pattern_elem = self._parse_allele_element(locus_str.strip())
                all_locus_patterns.append(pattern_elem)

        return GameteTypePattern(HaplotypePath(all_locus_patterns), lab)

    except PatternParseError:
        raise
    except Exception as e:
        raise PatternParseError(f"Failed to parse haplotype pattern '{pattern_str}'") from e
parse_haploid_genome_pattern
parse_haploid_genome_pattern(pattern_str: str) -> HaploidGenomePattern

Parse a haploid genome pattern (single DNA strand of individual).

For haploid genomes: - ; at top level separates different chromosomes - () brackets represent a single haplotype (one DNA strand) - Inside brackets, ; separates different loci on that strand - / is not used inside brackets for haploid (it's only for diploid)

Parameters:

Name Type Description Default
pattern_str str

Pattern string (e.g., "A1/B1; C1" or "(A1; B1); C1")

required

Returns:

Type Description
HaploidGenomePattern

HaploidGenomePattern object.

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/patterns/parser.py
def parse_haploid_genome_pattern(self, pattern_str: str) -> HaploidGenomePattern:
    """Parse a haploid genome pattern (single DNA strand of individual).

    For haploid genomes:
    - `;` at top level separates different chromosomes
    - `()` brackets represent a single haplotype (one DNA strand)
    - Inside brackets, `;` separates different loci on that strand
    - `/` is not used inside brackets for haploid (it's only for diploid)

    Args:
        pattern_str: Pattern string (e.g., "A1/B1; C1" or "(A1; B1); C1")

    Returns:
        HaploidGenomePattern object.

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    pattern_str = pattern_str.strip()

    try:
        # Split by semicolon, respecting parentheses
        chr_strs = self._split_by_semicolon_respecting_parens(pattern_str)

        haplotype_patterns: List[Optional[Union[HaplotypePath, Literal["WILDCARD_CHROMOSOME"]]]] = []
        for chr_str in chr_strs:
            if chr_str == "*":
                # Wildcard chromosome - will be expanded later
                haplotype_patterns.append("WILDCARD_CHROMOSOME")
            elif chr_str.startswith("(") and chr_str.endswith(")"):
                # Bracketed haplotype for this chromosome
                inner = chr_str[1:-1].strip()
                haplotype_path = self._parse_bracketed_haplotype_path(inner)
                haplotype_patterns.append(haplotype_path)
            else:
                # Standard form: A1/B1/C1
                haplotype_path = self._parse_haplotype_path(chr_str)
                haplotype_patterns.append(haplotype_path)

        # Handle wildcard markers and expand
        final_haplotype_patterns: List[Optional[HaplotypePath]] = []
        for i, pattern in enumerate(haplotype_patterns):
            if pattern == "WILDCARD_CHROMOSOME":
                # Create wildcard pattern for this chromosome
                if i < len(self.species.chromosomes):
                    chromosome = self.species.chromosomes[i]
                    num_loci = len(chromosome.loci)
                    wildcard_patterns = [WildcardPattern() for _ in range(num_loci)]
                    final_haplotype_patterns.append(HaplotypePath(wildcard_patterns))
                else:
                    final_haplotype_patterns.append(None)
            else:
                final_haplotype_patterns.append(pattern)

        # Fill remaining chromosomes with None
        while len(final_haplotype_patterns) < len(self.species.chromosomes):
            final_haplotype_patterns.append(None)

        return HaploidGenomePattern(final_haplotype_patterns)

    except PatternParseError:
        raise
    except Exception as e:
        raise PatternParseError(f"Failed to parse haploid genome pattern '{pattern_str}'") from e
get_allowed_alleles
get_allowed_alleles(pattern_element: PatternElement) -> List[str]

Get all allowed allele names for a pattern element.

Parameters:

Name Type Description Default
pattern_element PatternElement

The PatternElement to analyze.

required

Returns:

Type Description
List[str]

List of allowed allele names.

Source code in src/natal/patterns/parser.py
def get_allowed_alleles(self, pattern_element: PatternElement) -> List[str]:
    """Get all allowed allele names for a pattern element.

    Args:
        pattern_element: The PatternElement to analyze.

    Returns:
        List of allowed allele names.
    """
    if isinstance(pattern_element, AllelePattern):
        return [pattern_element.allele_name]
    elif isinstance(pattern_element, WildcardPattern):
        return self._get_all_allele_names()
    elif isinstance(pattern_element, SetPattern):
        if pattern_element.negate:
            all_alleles = set(self._get_all_allele_names())
            return list(all_alleles - pattern_element.alleles)
        else:
            return list(pattern_element.alleles)
    else:
        raise ValueError(f"Unknown pattern element type: {type(pattern_element)}")

GenotypeSelector

GenotypeSelector(species: Species)

Unified genotype selector for observation and filtering.

Provides a unified interface for selecting genotypes using various input formats (integers, strings, pattern strings, or Genotype objects), leveraging the existing pattern matching system.

Initialize a GenotypeSelector for a specific species.

Parameters:

Name Type Description Default
species Species

The Species object to use for pattern parsing and genotype resolution.

required
Source code in src/natal/patterns/selector.py
def __init__(self, species: Species):
    """Initialize a GenotypeSelector for a specific species.

    Args:
        species: The Species object to use for pattern parsing and
            genotype resolution.
    """
    self.species = species
    self.parser = GenotypePatternParser(species)
resolve_genotype_indices
resolve_genotype_indices(gen_spec: Optional[Iterable[Any]], diploid_genotypes: Optional[Sequence[Any]], unordered: bool = False) -> List[int]

Resolve genotype selectors into a list of indices.

This method provides the same functionality as observation.py's _resolve_genotype_list() but uses the pattern matching system.

Parameters:

Name Type Description Default
gen_spec Optional[Iterable[Any]]

Genotype selector specification. Can be: - None: select all genotypes - int: genotype index - str: genotype pattern string - Genotype: genotype object - Iterable of any of the above

required
diploid_genotypes Optional[Sequence[Any]]

Sequence of diploid genotypes for resolution.

required
unordered bool

Whether to treat genotypes as unordered (A|a == a|A).

False

Returns:

Type Description
List[int]

List of resolved genotype indices.

Raises:

Type Description
ValueError

If diploid_genotypes is required but missing.

Source code in src/natal/patterns/selector.py
def resolve_genotype_indices(
    self,
    gen_spec: Optional[Iterable[Any]],
    diploid_genotypes: Optional[Sequence[Any]],
    unordered: bool = False,
) -> List[int]:
    """Resolve genotype selectors into a list of indices.

    This method provides the same functionality as observation.py's
    _resolve_genotype_list() but uses the pattern matching system.

    Args:
        gen_spec: Genotype selector specification. Can be:
            - None: select all genotypes
            - int: genotype index
            - str: genotype pattern string
            - Genotype: genotype object
            - Iterable of any of the above
        diploid_genotypes: Sequence of diploid genotypes for resolution.
        unordered: Whether to treat genotypes as unordered (A|a == a|A).

    Returns:
        List of resolved genotype indices.

    Raises:
        ValueError: If diploid_genotypes is required but missing.
    """
    if gen_spec is None:
        if diploid_genotypes is None:
            raise ValueError("diploid_genotypes required to enumerate genotypes")
        return list(range(len(diploid_genotypes)))

    # Handle single item vs iterable
    if not isinstance(gen_spec, (list, tuple, set)):
        gen_spec = [gen_spec]

    resolved_indices: Set[int] = set()

    for selector in gen_spec:
        if isinstance(selector, int):
            # Direct index
            resolved_indices.add(selector)
        elif isinstance(selector, str):
            # Pattern string - use pattern matching system
            pattern = self.parser.parse(selector)
            if diploid_genotypes is None:
                raise ValueError("diploid_genotypes required for pattern matching")

            for i, genotype in enumerate(diploid_genotypes):
                if pattern.matches(genotype):
                    resolved_indices.add(i)
        else:
            # Assume it's a Genotype object or similar
            if diploid_genotypes is None:
                raise ValueError("diploid_genotypes required for genotype matching")

            for i, genotype in enumerate(diploid_genotypes):
                if self._genotypes_equal(selector, genotype, unordered):
                    resolved_indices.add(i)

    return sorted(resolved_indices)
create_filter_function
create_filter_function(gen_spec: Optional[Iterable[Any]], unordered: bool = False) -> Callable[[Any], bool]

Create a filter function for genotype selection.

NOTE: Dead code as of 2026-06 — zero callers in the codebase. The active path uses ZygoteTypePattern and resolve_zygote_type directly. Keep for reference.

Parameters:

Name Type Description Default
gen_spec Optional[Iterable[Any]]

Genotype selector specification.

required
unordered bool

Whether to use unordered matching.

False

Returns:

Type Description
Callable[[Any], bool]

A callable that takes a genotype and returns True if it matches.

Source code in src/natal/patterns/selector.py
def create_filter_function(
    self,
    gen_spec: Optional[Iterable[Any]],
    unordered: bool = False
) -> Callable[[Any], bool]:
    """Create a filter function for genotype selection.

    NOTE: **Dead code** as of 2026-06 — zero callers in the codebase.
    The active path uses ``ZygoteTypePattern`` and ``resolve_zygote_type``
    directly. Keep for reference.

    Args:
        gen_spec: Genotype selector specification.
        unordered: Whether to use unordered matching.

    Returns:
        A callable that takes a genotype and returns True if it matches.
    """
    if gen_spec is None:
        # Match all genotypes
        return lambda genotype: True

    # Handle single item vs iterable
    if not isinstance(gen_spec, (list, tuple, set)):
        gen_spec = [gen_spec]

    # Create pattern-based filters for string selectors
    pattern_filters: List[Callable[[Any], bool]] = []
    other_selectors: List[Any] = []

    for selector in gen_spec:
        if isinstance(selector, str):
            pattern = self.parser.parse(selector)
            pattern_filters.append(pattern.to_filter())
        else:
            other_selectors.append(selector)

    def filter_func(genotype: int) -> bool:  # genotype index in registry
        # Check pattern filters
        for pattern_filter in pattern_filters:
            if pattern_filter(genotype):
                return True

        # Check other selectors
        for selector in other_selectors:
            if self._genotypes_equal(selector, genotype, unordered):
                return True

        return False

    return filter_func
get_pattern_for_selector
get_pattern_for_selector(selector: Any) -> Optional[GenotypePattern]

Convert a selector to a GenotypePattern if possible.

Parameters:

Name Type Description Default
selector Any

Genotype selector.

required

Returns:

Type Description
Optional[GenotypePattern]

GenotypePattern if selector can be converted, None otherwise.

Source code in src/natal/patterns/selector.py
def get_pattern_for_selector(self, selector: Any) -> Optional[GenotypePattern]:  # accepts str or GenotypePattern
    """Convert a selector to a GenotypePattern if possible.

    Args:
        selector: Genotype selector.

    Returns:
        GenotypePattern if selector can be converted, None otherwise.
    """
    if isinstance(selector, str):
        return self.parser.parse(selector)
    elif isinstance(selector, GenotypePattern):
        return selector
    else:
        return None

resolve_zygote_type

resolve_zygote_type(spec: str, species: Species, index_registry: IndexRegistry) -> list[int]

Resolve a genotype string to ZType indices, with species-appropriate matching.

For unordered species, auto-promotes | to :: so that "A|a" matches both ordered and unordered (canonicalized) registrations. This mirrors the canonicalization logic in :meth:genetic_structures.Species.resolve_genotype_selectors.

For ordered species (e.g. sex chromosomes), | is treated strictly — "a|A" and "A|a" are distinct genotypes and will each only match their exact ordering.

Does NOT perform the reversed-maternal/paternal fallback (that would be a bug for ordered species).

Parameters:

Name Type Description Default
spec str

Genotype selector string (e.g. "A|A", "Drive|WT", "*", "A@exposed").

required
species Species

Species for genotype-resolution context.

required
index_registry IndexRegistry

Registry for ZType index resolution.

required

Returns:

Type Description
list[int]

List of matching ZType indices (may be empty if nothing matches).

Source code in src/natal/patterns/selector.py
def resolve_zygote_type(
    spec: str,
    species: Species,
    index_registry: IndexRegistry,
) -> list[int]:
    """Resolve a genotype string to ZType indices, with species-appropriate matching.

    For unordered species, auto-promotes ``|`` to ``::`` so that ``"A|a"``
    matches both ordered and unordered (canonicalized) registrations.  This
    mirrors the canonicalization logic in
    :meth:`genetic_structures.Species.resolve_genotype_selectors`.

    For ordered species (e.g. sex chromosomes), ``|`` is treated strictly —
    ``"a|A"`` and ``"A|a"`` are distinct genotypes and will each only match
    their exact ordering.

    Does NOT perform the reversed-maternal/paternal fallback (that would be
    a bug for ordered species).

    Args:
        spec: Genotype selector string (e.g. ``"A|A"``, ``"Drive|WT"``,
            ``"*"``, ``"A@exposed"``).
        species: Species for genotype-resolution context.
        index_registry: Registry for ZType index resolution.

    Returns:
        List of matching ZType indices (may be empty if nothing matches).
    """
    # Canonicalize | → :: for unordered species only (same pattern as
    # Species._resolve_single_genotype_selector in genetic_structures.py).
    # The \x00 trick preserves any :: the user already wrote.
    if species.unordered:
        spec = spec.replace("::", "\x00").replace("|", "::").replace("\x00", "::")

    pattern = ZygoteTypePattern.parse(spec, species)
    return index_registry.resolve_ztype_indices(pattern)