Skip to content

genetic_structures Module

Core genetic architecture definitions for the simulation.

Overview

The genetic_structures module defines the immutable genetic architecture of the simulation, including chromosomes, loci, species, and genome templates.

Complete Module Reference

natal.genetic_structures

Defines the immutable genetic architecture of the simulation.

This module defines static, model-level genetic elements (loci, chromosomes, species) that serve as the authoritative blueprint for creating and validating genetic entities.

This module is responsible for: - Representing static, model-level genetic elements (loci, chromosomes, species) - Storing configuration and rules such as locus order, recombination rates, and chromosome IDs - Serving as the authoritative blueprint for creating and validating genetic entities - Optionally tracking bound entities via internal registration mechanisms

Note: - No runtime dependency on genetic_entities to avoid circular imports - Modifications to a structure after binding entities are discouraged

SexChromosomeType

Bases: Enum

Sex chromosome type enumeration.

Defines common sex chromosome categories and inheritance constraints used by chromosome-level sex-system logic.

Attributes:

Name Type Description
AUTOSOME SexChromosomeType

Autosome not involved in sex determination.

X SexChromosomeType

X chromosome in the XY system; can come from either parent.

Y SexChromosomeType

Y chromosome in the XY system; paternal only.

Z SexChromosomeType

Z chromosome in the ZW system; can come from either parent.

W SexChromosomeType

W chromosome in the ZW system; maternal only.

is_sex_chromosome property
is_sex_chromosome: bool

Whether this is a sex chromosome

sex_system property
sex_system: Optional[str]

Returns the sex determination system this chromosome belongs to

maternal_only property
maternal_only: bool

Whether it can only be inherited from mother

paternal_only property
paternal_only: bool

Whether it can only be inherited from father

RegistryBase

RegistryBase(expected_type: Optional[type[GeneticStructure[E]]] = None)

Bases: ABC, Generic[T]

Base class for registries.

Provides the common interface for register/unregister operations while delegating storage semantics to subclass hooks.

Attributes:

Name Type Description
_expected_type Optional[type[GeneticStructure[E]]]

Runtime type used to validate registry items when provided.

Source code in src/natal/genetic_structures.py
def __init__(self, expected_type: Optional[type[GeneticStructure[E]]] = None):
    self._expected_type = expected_type
register
register(item_or_items: Union[T, List[T], Tuple[T, ...], Set[T]]) -> None

Register one or more items.

Source code in src/natal/genetic_structures.py
def register(self, item_or_items: Union[T, List[T], Tuple[T, ...], Set[T]]) -> None:
    """Register one or more items."""
    # GeneticStructure is iterable (yields children) but should be registered as single item
    # Use explicit list/tuple/set check instead of Iterable to avoid this.
    # (str / custom iterable objects should not be silently treated as batch input.)
    if isinstance(item_or_items, list):
        item_or_items = cast(List[T], item_or_items)
        for item in item_or_items:
            if self._is_valid_item_type(item):
                self._single_register(item)
            else:
                raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    elif isinstance(item_or_items, tuple):
        item_or_items = cast(Tuple[T, ...], item_or_items)
        for item in item_or_items:
            if self._is_valid_item_type(item):
                self._single_register(item)
            else:
                raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    elif isinstance(item_or_items, set):
        item_or_items = cast(Set[T], item_or_items)
        for item in item_or_items:
            if self._is_valid_item_type(item):
                self._single_register(item)
            else:
                raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    else:
        if self._is_valid_item_type(item_or_items):
            self._single_register(item_or_items)
        else:
            raise TypeError(f"Expected registry item type, got {type(item_or_items).__name__}")
unregister
unregister(item_or_items: Union[T, str, List[Union[T, str]], Tuple[Union[T, str], ...], Set[Union[T, str]]]) -> None

Unregister one or more items (by key or item object).

Source code in src/natal/genetic_structures.py
def unregister(
    self,
    item_or_items: Union[T, str, List[Union[T, str]], Tuple[Union[T, str], ...], Set[Union[T, str]]]
) -> None:
    """Unregister one or more items (by key or item object)."""
    # unregister supports mixed batch input: [item, "name", item, ...]
    # where str means key-based removal and non-str means object removal.
    if isinstance(item_or_items, list):
        item_or_items = cast(List[Union[T, str]], item_or_items)
        for item in item_or_items:
            if isinstance(item, str):
                self._single_unregister_by_key(item)
            else:
                if self._is_valid_item_type(item):
                    self._single_unregister(item)
                else:
                    raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    elif isinstance(item_or_items, tuple):
        item_or_items = cast(Tuple[Union[T, str], ...], item_or_items)
        for item in item_or_items:
            if isinstance(item, str):
                self._single_unregister_by_key(item)
            else:
                if self._is_valid_item_type(item):
                    self._single_unregister(item)
                else:
                    raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    elif isinstance(item_or_items, set):
        item_or_items = cast(Set[Union[T, str]], item_or_items)
        for item in item_or_items:
            if isinstance(item, str):
                self._single_unregister_by_key(item)
            else:
                if self._is_valid_item_type(item):
                    self._single_unregister(item)
                else:
                    raise TypeError(f"Expected registry item type, got {type(item).__name__}")
    elif isinstance(item_or_items, str):
        self._single_unregister_by_key(item_or_items)
    else:
        if self._is_valid_item_type(item_or_items):
            self._single_unregister(item_or_items)
        else:
            raise TypeError(f"Expected registry item type, got {type(item_or_items).__name__}")

EntityRegistry

EntityRegistry(expected_type: Optional[type] = None)

Bases: RegistryBase[E]

Registry for entity objects. Deduplication by object identity.

Attributes:

Name Type Description
_storage List[E]

Ordered storage for deterministic iteration.

_set Set[E]

Identity-based membership set for O(1) deduplication checks.

Source code in src/natal/genetic_structures.py
def __init__(self, expected_type: Optional[type] = None):
    super().__init__(expected_type)
    self._storage: List[E] = []
    self._set: Set[E] = set()
all property
all: List[E]

Returns all registered entities.

ChildStructureRegistry

ChildStructureRegistry(owner: GeneticStructure[Any], expected_type: Optional[type[GeneticStructure[E]]] = None)

Bases: RegistryBase[S]

Registry for child structures. Keyed by name, preserves insertion order. Supports both register (existing) and add (create + register).

Attributes:

Name Type Description
_owner GeneticStructure[Any]

Parent structure that owns this registry.

_storage Dict[str, S]

Name-to-child mapping for registered structures.

Source code in src/natal/genetic_structures.py
def __init__(
    self,
    owner: GeneticStructure[Any],
    expected_type: Optional[type[GeneticStructure[E]]] = None
):
    super().__init__(expected_type)
    self._owner = owner  # The parent structure that owns this registry
    self._storage: Dict[str, S] = {}
all property
all: List[S]

Returns all registered child structures.

add
add(name: str, **kwargs: Any) -> S

Create a new child structure and register it. This is a convenience method: create + register.

If a child with the same name already exists in this registry, the cached instance is returned immediately. This makes add idempotent and consistent with GeneticStructure.__new__, which also returns cached instances rather than creating duplicates.

Uses Species-level caching to ensure uniqueness within the same Species.

Source code in src/natal/genetic_structures.py
def add(self, name: str, **kwargs: Any) -> S:
    """
    Create a new child structure and register it.
    This is a convenience method: create + register.

    If a child with the same *name* already exists in this registry, the
    cached instance is returned immediately.  This makes ``add`` idempotent
    and consistent with ``GeneticStructure.__new__``, which also returns
    cached instances rather than creating duplicates.

    Uses Species-level caching to ensure uniqueness within the same Species.
    """
    assert isinstance(name, str), "Child structure name must be a string."
    if not name.strip():
        raise ValueError("Child structure name must be a non-empty string.")
    if name in self._storage:
        return self._storage[name]
    expected_type = self._expected_type
    if expected_type is None:
        raise ValueError("expected_type not set, cannot construct child structure.")

    # Get the Species from the owner
    species: Optional[Species] = self._owner.species

    # Check if structure already exists in cache (Species-scoped or global)
    if species is not None:
        # Preferred path: Species-scoped cache (isolation between species).
        if expected_type not in species.structure_cache:
            species.structure_cache[expected_type] = {}

        cache = species.structure_cache[expected_type]
        if name in cache:
            # Return cached instance
            cached_child = cache[name]
            if self._is_expected_child(cached_child):
                # Still register it in this owner's registry if not already there
                if name not in self._storage:
                    self._storage[name] = cached_child
                return cached_child
            raise TypeError(
                f"Cached structure for '{name}' has wrong type: "
                f"expected {expected_type.__name__}, got {type(cached_child).__name__}"
            )

        # Create new child with parent (species is inherited automatically)
        created_child = expected_type(name, parent=self._owner, **kwargs)
        if not self._is_expected_child(created_child):
            raise TypeError(
                f"Created structure for '{name}' has wrong type: "
                f"expected {expected_type.__name__}, got {type(created_child).__name__}"
            )
        child = created_child

        # Cache it in the Species
        cache[name] = child
    else:
        # Fallback path for backward compatibility with structures not yet
        # bound into a Species context.
        if expected_type not in _GLOBAL_STRUCTURE_CACHE:
            _GLOBAL_STRUCTURE_CACHE[expected_type] = {}

        cache = _GLOBAL_STRUCTURE_CACHE[expected_type]
        if name in cache:
            # Return cached instance
            cached_child = cache[name]
            if self._is_expected_child(cached_child):
                # Still register it in this owner's registry if not already there
                if name not in self._storage:
                    self._storage[name] = cached_child
                return cached_child
            raise TypeError(
                f"Cached structure for '{name}' has wrong type: "
                f"expected {expected_type.__name__}, got {type(cached_child).__name__}"
            )

        # Create new child with parent (no species means orphan)
        created_child = expected_type(name, parent=self._owner, **kwargs)
        if not self._is_expected_child(created_child):
            raise TypeError(
                f"Created structure for '{name}' has wrong type: "
                f"expected {expected_type.__name__}, got {type(created_child).__name__}"
            )
        child = created_child

        # Cache it globally
        cache[name] = child

    return child
get
get(name: str) -> S

Get a child structure by name.

Source code in src/natal/genetic_structures.py
def get(self, name: str) -> S:
    """Get a child structure by name."""
    if name not in self._storage:
        raise KeyError(f"No child structure named '{name}' found.")
    return self._storage[name]
clear
clear() -> None

Clear all registered child structures.

Source code in src/natal/genetic_structures.py
def clear(self) -> None:
    """Clear all registered child structures."""
    self._storage.clear()

GeneticStructure

GeneticStructure(name: str, parent: Optional[GeneticStructure[Any]] = None, species: Optional[Species] = None)

Bases: Generic[E]

Base class for genetic structures.

Structure uniqueness is now scoped to a Species, not globally. Within the same Species, structures of the same type must have unique names.

Attributes:

Name Type Description
child_structure_type Optional[type[GeneticStructure[Any]]]

Child structure class used by subclasses, or None when no child structures are supported.

name str

Structure identifier unique within the same structure type and species.

species Optional[Species]

Species this structure is currently bound to.

all_entities List[E]

Snapshot of currently registered runtime entities.

Examples:

>>> species1 = Species("Species1")
>>> locus1 = Locus("A", species=species1)
>>> locus2 = Locus("A", species=species1)
>>> assert locus1 is locus2  # Same object within species1
>>>
>>> species2 = Species("Species2")
>>> locus3 = Locus("A", species=species2)
>>> assert locus1 is not locus3  # Different speciess allow same name
Source code in src/natal/genetic_structures.py
def __init__(
    self,
    name: str,
    parent: Optional[GeneticStructure[Any]] = None,
    species: Optional[Species] = None
):
    # Prevent re-initialization of cached instances
    if hasattr(self, "_initialized") and self._initialized:
        return

    assert isinstance(name, str), "Structure name must be a string."
    if name.strip() == "":
        raise ValueError("Structure name cannot be empty.")

    # Registry wiring:
    # - _entities tracks runtime-bound entity instances (Gene/Haplotype/...)
    # - child_structures (if enabled by subclass) tracks structural children
    #   (Locus under Chromosome, Chromosome under Species, etc.)
    #
    # entity_type remains a subclass property to support lazy import and avoid
    # circular imports with natal.genetic_entities.
    self.name = name
    self._entities: EntityRegistry[E] = EntityRegistry()

    # Track the root Species for this structure
    if species is not None:
        self._species = species
    elif parent is not None:
        # Inherit species from parent
        self._species = parent.species
    else:
        # This is a Species itself
        self._species = None

    # Initialize child structures registry if applicable
    cls = self.__class__
    if cls.child_structure_type:
        self.child_structures = ChildStructureRegistry[cls.child_structure_type](
            owner=self,
            expected_type=cls.child_structure_type
        )

    # Strict constraint: must be added to a parent unless top-level
    if parent is not None:
        assert isinstance(parent, GeneticStructure), \
            "parent must be a GeneticStructure instance."
        # Register this structure as a child of the parent
        assert parent.child_structures is not None, \
            f"Parent {parent.__class__.__name__} does not support child structures."
        parent.child_structures.register(self)

    # Mark as initialized, avoiding re-initialization when created from cache
    self._initialized = True

    # Cache the instance AFTER successful initialization
    self._add_to_cache(self._species)
species property
species: Optional[Species]

Public accessor for the bound Species.

entity_type property
entity_type: Optional[type]

Override in subclass to specify the entity type. Using property allows lazy import to avoid circular dependencies.

children property
children: List[GeneticStructure[Any]]

Returns all child structures.

all_entities property
all_entities: List[E]

Returns a list of all entities currently registered to this structure.

clear_cache classmethod
clear_cache() -> None

Deprecated: Caching is now managed by Species. This method does nothing but is kept for backward compatibility.

Source code in src/natal/genetic_structures.py
@classmethod
def clear_cache(cls) -> None:
    """
    Deprecated: Caching is now managed by Species.
    This method does nothing but is kept for backward compatibility.
    """
    pass
clear_all_caches
clear_all_caches() -> None

Clear all caches including: - Global fallback cache (for structures without Species) - All Species-specific caches are cleared via Species.clear_all_caches()

This method is primarily for testing and cleanup.

Source code in src/natal/genetic_structures.py
def clear_all_caches(self) -> None:
    """
    Clear all caches including:
    - Global fallback cache (for structures without Species)
    - All Species-specific caches are cleared via Species.clear_all_caches()

    This method is primarily for testing and cleanup.
    """
    global _GLOBAL_STRUCTURE_CACHE
    _GLOBAL_STRUCTURE_CACHE.clear()
add
add(name_or_specs: Union[str, List[str], List[Tuple[str, Dict[str, Any]]]], **kwargs: Any) -> Union[GeneticStructure[Any], List[GeneticStructure[Any]]]

Add child structure(s) to this structure.

Parameters:

Name Type Description Default
name_or_specs Union[str, List[str], List[Tuple[str, Dict[str, Any]]]]

Can be: - str: Single child name - List[str]: List of child names - List[Tuple[str, Dict]]: List of (name, kwargs) tuples

required
**kwargs Any

Additional keyword arguments for single child creation.

{}

Returns:

Type Description
Union[GeneticStructure[Any], List[GeneticStructure[Any]]]

Single child structure or list of child structures.

Examples:

>>> linkage.add("LocusA", location=100)  # Single child
>>> linkage.add(["LocusA", "LocusB"])    # Multiple children
>>> linkage.add([("LocusA", {"location": 100}), ("LocusB", {"location": 200})])
Source code in src/natal/genetic_structures.py
def add(
    self,
    name_or_specs: Union[str, List[str], List[Tuple[str, Dict[str, Any]]]],
    **kwargs: Any,
) -> Union[GeneticStructure[Any], List[GeneticStructure[Any]]]:
    """
    Add child structure(s) to this structure.

    Args:
        name_or_specs: Can be:
            - str: Single child name
            - List[str]: List of child names
            - List[Tuple[str, Dict]]: List of (name, kwargs) tuples
        **kwargs: Additional keyword arguments for single child creation.

    Returns:
        Single child structure or list of child structures.

    Examples:
        >>> linkage.add("LocusA", location=100)  # Single child
        >>> linkage.add(["LocusA", "LocusB"])    # Multiple children
        >>> linkage.add([("LocusA", {"location": 100}), ("LocusB", {"location": 200})])
    """
    child_registry = self._requirechild_structures_registry()

    assert isinstance(name_or_specs, (str, list)), \
        f"Expected str, List[str], or List[Tuple[str, Dict]], got {type(name_or_specs).__name__}"

    # Single name
    if isinstance(name_or_specs, str):
        return child_registry.add(name_or_specs, **kwargs)

    # List of names or (name, kwargs) tuples
    else:
        results: List[GeneticStructure[Any]] = []
        for item in name_or_specs:
            if isinstance(item, str):
                results.append(child_registry.add(item, **kwargs))
            elif len(item) == 2:
                name, child_kwargs = item
                merged_kwargs = {**kwargs, **child_kwargs}
                results.append(child_registry.add(name, **merged_kwargs))
            else:
                raise TypeError(f"Invalid item in list: {item}. Expected str or (str, dict) tuple.")
        return results
remove
remove(name_or_child: Union[str, GeneticStructure[Any], List[Union[str, GeneticStructure[Any]]]]) -> None

Remove child structure(s) from this structure.

Parameters:

Name Type Description Default
name_or_child Union[str, GeneticStructure[Any], List[Union[str, GeneticStructure[Any]]]]

Can be: - str: Child name to remove - GeneticStructure: Child instance to remove - List: List of names or instances to remove

required

Examples:

>>> linkage.remove("LocusA")           # Remove by name
>>> linkage.remove(locus_a)            # Remove by instance
>>> linkage.remove(["LocusA", "LocusB"])  # Remove multiple
Source code in src/natal/genetic_structures.py
def remove(
    self,
    name_or_child: Union[str, GeneticStructure[Any], List[Union[str, GeneticStructure[Any]]]],
) -> None:
    """
    Remove child structure(s) from this structure.

    Args:
        name_or_child: Can be:
            - str: Child name to remove
            - GeneticStructure: Child instance to remove
            - List: List of names or instances to remove

    Examples:
        >>> linkage.remove("LocusA")           # Remove by name
        >>> linkage.remove(locus_a)            # Remove by instance
        >>> linkage.remove(["LocusA", "LocusB"])  # Remove multiple
    """
    child_registry = self._requirechild_structures_registry()

    # Delegate to registry - it handles both str and object
    child_registry.unregister(name_or_child)
get_child
get_child(name: str) -> GeneticStructure[Any]

Get a child structure by name.

Parameters:

Name Type Description Default
name str

Name of the child structure.

required

Returns:

Type Description
GeneticStructure[Any]

The child structure instance.

Raises:

Type Description
KeyError

If no child with that name exists.

Source code in src/natal/genetic_structures.py
def get_child(self, name: str) -> GeneticStructure[Any]:
    """
    Get a child structure by name.

    Args:
        name: Name of the child structure.

    Returns:
        The child structure instance.

    Raises:
        KeyError: If no child with that name exists.
    """
    child_registry = self._requirechild_structures_registry()
    return child_registry.get(name)
register
register(entity_or_entities: Union[E, List[E], Tuple[E, ...], Set[E]]) -> GeneticStructure[E]

Register a single entity or an iterable of entities with this structure.

EntityRegistry performs runtime type validation based on the expected type provided at construction.

Parameters:

Name Type Description Default
entity_or_entities Union[E, List[E], Tuple[E, ...], Set[E]]

Single entity or iterable of entities to register.

required

Returns:

Type Description
GeneticStructure[E]

The GeneticStructure instance (for chaining).

Source code in src/natal/genetic_structures.py
def register(
    self,
    entity_or_entities: Union[E, List[E], Tuple[E, ...], Set[E]]
) -> GeneticStructure[E]:
    """
    Register a single entity or an iterable of entities with this structure.

    EntityRegistry performs runtime type validation based on the expected type provided at construction.

    Args:
        entity_or_entities: Single entity or iterable of entities to register.

    Returns:
        The GeneticStructure instance (for chaining).
    """
    # Delegate single/batch normalization and strict type-checking to EntityRegistry.
    self._entities.register(entity_or_entities)
    return self
unregister
unregister(entity_or_entities: Union[E, str, List[Union[E, str]], Tuple[Union[E, str], ...], Set[Union[E, str]]]) -> GeneticStructure[E]

Unregister a single entity or an iterable of entities from this structure.

Parameters:

Name Type Description Default
entity_or_entities Union[E, str, List[Union[E, str]], Tuple[Union[E, str], ...], Set[Union[E, str]]]

Single entity or iterable of entities to unregister.

required

Returns:

Type Description
GeneticStructure[E]

The GeneticStructure instance (for chaining).

Source code in src/natal/genetic_structures.py
def unregister(
    self,
    entity_or_entities: Union[E, str, List[Union[E, str]], Tuple[Union[E, str], ...], Set[Union[E, str]]]
) -> GeneticStructure[E]:
    """
    Unregister a single entity or an iterable of entities from this structure.

    Args:
        entity_or_entities: Single entity or iterable of entities to unregister.

    Returns:
        The GeneticStructure instance (for chaining).
    """
    self._entities.unregister(entity_or_entities)
    return self
with_entities classmethod
with_entities(name: str, entity_ids: Union[str, Iterable[str]], **entity_kwargs: Any) -> GeneticStructure[E]

Factory method to create a GeneticStructure instance and register entities by their identifiers.

Parameters:

Name Type Description Default
name str

Name of the genetic structure.

required
entity_ids str | Iterable[str]

Single identifier or iterable of identifiers for entities to register.

required
**entity_kwargs Any

Additional keyword arguments to pass to the entity constructor.

{}
Source code in src/natal/genetic_structures.py
@classmethod
def with_entities(
    cls,
    name: str,
    entity_ids: Union[str, Iterable[str]],
    **entity_kwargs: Any
) -> GeneticStructure[E]:
    """
    Factory method to create a GeneticStructure instance and register entities by their identifiers.

    Args:
        name (str): Name of the genetic structure.
        entity_ids (str | Iterable[str]): Single identifier or iterable of identifiers for entities to register.
        **entity_kwargs: Additional keyword arguments to pass to the entity constructor.
    """
    structure = cls(name)
    entity_type = structure.entity_type
    if entity_type is None:
        raise TypeError(f"{cls.__name__} has no entity type defined.")

    if isinstance(entity_ids, str):
        entity_ids = [entity_ids]

    entities = [entity_type(name=en, **entity_kwargs) for en in entity_ids]
    structure.register(entities)
    return structure

Locus

Locus(name: str, position: Optional[Union[int, float]] = None, chromosome: Optional[Chromosome] = None, parent: Optional[Chromosome] = None, **kwargs: Any)

Bases: GeneticStructure['Gene']

Represents a genetic locus with its name.

A Locus is a blueprint for a genetic position. Multiple Gene entities (alleles) can be bound to a single Locus.

Attributes:

Name Type Description
position Union[int, float]

The linear position on the chromosome. Used for defining recombination rates. If not specified, defaults to max(position) + 1 among existing loci in the parent Linkage, or 0 if no parent.

alleles List[Gene]

Registered allele entities bound to this locus.

Source code in src/natal/genetic_structures.py
def __init__(
    self,
    name: str,
    position: Optional[Union[int, float]] = None,
    chromosome: Optional[Chromosome] = None,
    parent: Optional[Chromosome] = None,
    **kwargs: Any
):
    # Check if already initialized (cached instance)
    if hasattr(self, '_initialized') and self._initialized:
        return

    # Parent is alias for chromosome
    if chromosome is None:
        chromosome = parent

    # Save parent reference for cache invalidation
    self._parent_chromosome = chromosome

    # Compute default position before super().__init__
    # (since parent.register may be called)
    if position is None:
        if chromosome is not None and hasattr(chromosome, 'child_structures') and len(chromosome.child_structures) > 0:
            # Default: max position in parent + 1
            max_pos = max(
                (loc.position for loc in chromosome.child_structures),
                default=-1
            )
            position = max_pos + 1
        else:
            position = 0

    self._position = position

    # Store custom parameters as attributes
    for key, value in kwargs.items():
        setattr(self, key, value)

    # Locus's species is automatically inherited from parent Chromosome
    super().__init__(name, parent=chromosome)
position property writable
position: Union[int, float]

The linear position on the chromosome.

entity_type property
entity_type

Lazy import to avoid circular dependency.

alleles property
alleles: List[Gene]

Alias for all_entities - returns all registered alleles (genes).

register
register(entity_or_entities: Union[Gene, List[Gene], Tuple[Gene, ...], Set[Gene]]) -> Locus

Register gene entities and invalidate species gene index cache.

Source code in src/natal/genetic_structures.py
def register(
    self,
    entity_or_entities: Union[Gene, List[Gene], Tuple[Gene, ...], Set[Gene]]
) -> Locus:
    """
    Register gene entities and invalidate species gene index cache.
    """
    super().register(entity_or_entities)
    if self._species is not None:
        self._species.invalidate_gene_index_cache()
    return self
unregister
unregister(entity_or_entities: Union[Gene, str, List[Union[Gene, str]], Tuple[Union[Gene, str], ...], Set[Union[Gene, str]]]) -> Locus

Unregister gene entities and invalidate species gene index cache.

Source code in src/natal/genetic_structures.py
def unregister(
    self,
    entity_or_entities: Union[Gene, str, List[Union[Gene, str]], Tuple[Union[Gene, str], ...], Set[Union[Gene, str]]]
) -> Locus:
    """
    Unregister gene entities and invalidate species gene index cache.
    """
    super().unregister(entity_or_entities)
    if self._species is not None:
        self._species.invalidate_gene_index_cache()
    return self
register_allele
register_allele(gene: Gene) -> Locus

Alias for register - register a single allele.

Source code in src/natal/genetic_structures.py
def register_allele(self, gene: Gene) -> Locus:
    """Alias for register - register a single allele."""
    return self.register(gene)
unregister_allele
unregister_allele(gene: Gene) -> Locus

Alias for unregister - unregister a single allele.

Source code in src/natal/genetic_structures.py
def unregister_allele(self, gene: Gene) -> Locus:
    """Alias for unregister - unregister a single allele."""
    return self.unregister(gene)
add_alleles
add_alleles(alleles_or_allele_names: Union[List[Union[Gene, str]], Gene, str]) -> Locus

Add one or more alleles (genes) to this locus.

Parameters:

Name Type Description Default
alleles_or_allele_names Union[List[Union[Gene, str]], Gene, str]

Single Gene instance, single allele name (str), or list of Gene instances and/or allele names (str).

required

Returns: The Locus instance (for chaining). Note that for other structure-level add methods, the return type is the child structure(s) added. But here we return self for consistency with the register_allele/unregister_allele methods.

Source code in src/natal/genetic_structures.py
def add_alleles(
    self,
    alleles_or_allele_names: Union[List[Union[Gene, str]], Gene, str],
) -> Locus:
    """
    Add one or more alleles (genes) to this locus.

    Args:
        alleles_or_allele_names: Single Gene instance, single allele name (str),
            or list of Gene instances and/or allele names (str).
    Returns:
        The Locus instance (for chaining). Note that for other structure-level add methods,
        the return type is the child structure(s) added. But here we return self for consistency
        with the register_allele/unregister_allele methods.
    """
    from natal.genetic_entities import Gene

    if isinstance(alleles_or_allele_names, (Gene, str)):
        alleles_or_allele_names = [alleles_or_allele_names]

    for item in alleles_or_allele_names:
        assert isinstance(item, (Gene, str)), f"Expected Gene or str, got {type(item).__name__} instead."
        if isinstance(item, Gene):
            self.register(item)
        else:
            Gene(item, locus=self)  # Auto-registers via Gene.__init__

    return self
with_alleles classmethod
with_alleles(name: str, alleles_or_allele_names: Union[List[Union[Gene, str]], Gene, str], position: Optional[Union[int, float]] = None) -> Locus

Factory method to create a Locus and register alleles (genes) by names.

Parameters:

Name Type Description Default
name str

Name of the locus.

required
alleles_or_allele_names Union[List[Union[Gene, str]], Gene, str]

Single Gene instance, single allele name (str), or list of Gene instances and/or allele names (str).

required
position Optional[Union[int, float]]

Optional position on the chromosome.

None

Returns:

Type Description
Locus

Locus instance with registered alleles.

Examples:

>>> locus = Locus.with_alleles("A", ["A1", "A2", "A3"])
>>> locus.alleles  # -> [Gene("A1"), Gene("A2"), Gene("A3")]
Source code in src/natal/genetic_structures.py
@classmethod
def with_alleles(
    cls,
    name: str,
    alleles_or_allele_names: Union[List[Union[Gene, str]], Gene, str],
    position: Optional[Union[int, float]] = None
) -> Locus:
    """
    Factory method to create a Locus and register alleles (genes) by names.

    Args:
        name: Name of the locus.
        alleles_or_allele_names: Single Gene instance, single allele name (str),
            or list of Gene instances and/or allele names (str).
        position: Optional position on the chromosome.

    Returns:
        Locus instance with registered alleles.

    Examples:
        >>> locus = Locus.with_alleles("A", ["A1", "A2", "A3"])
        >>> locus.alleles  # -> [Gene("A1"), Gene("A2"), Gene("A3")]
    """
    return cls(name, position=position).add_alleles(alleles_or_allele_names)

Chromosome

Chromosome(name: str, loci: Optional[List[Locus]] = None, species: Optional[Species] = None, parent: Optional[Species] = None, recombination_rates: Optional[Union[List[float], ndarray]] = None, sex_type: Optional[Union[SexChromosomeType, str]] = None)

Bases: GeneticStructure['Haplotype']

Represents a chromosome structure with linkage information among loci.

A Chromosome groups multiple Loci that are physically linked on the same chromosome. It also stores the recombination rates between loci.

Attributes:

Name Type Description
sex_type SexChromosomeType

Sex chromosome type. - None or 'autosome': Autosome (default) - 'X': X chromosome in XY system - 'Y': Y chromosome in XY system (paternal only) - 'Z': Z chromosome in ZW system - 'W': W chromosome in ZW system (maternal only)

loci List[Locus]

Child loci sorted by position.

recombination_map RecombinationMap

Adjacent-locus recombination map.

recombination_matrix RecombinationMap

Backward-compatible alias of recombination_map.

is_sex_chromosome bool

Whether this chromosome participates in sex determination.

is_autosome bool

Whether this chromosome is an autosome.

sex_system Optional[str]

Sex system inferred from sex_type ('XY', 'ZW', or None).

Examples:

>>> chr_x = Chromosome('X', sex_type='X')
>>> chr_y = Chromosome('Y', sex_type='Y')
>>> print(chr_x.is_sex_chromosome)  # True
>>> print(chr_y.sex_type.paternal_only)  # True

This class is also exported as Linkage for backward compatibility.

Source code in src/natal/genetic_structures.py
def __init__(
    self,
    name: str,
    loci: Optional[List[Locus]] = None,
    species: Optional[Species] = None,
    parent: Optional[Species] = None,
    recombination_rates: Optional[Union[List[float], np.ndarray]] = None,
    sex_type: Optional[Union[SexChromosomeType, str]] = None,
):
    # Initialize placeholders BEFORE super().__init__
    # because __iter__ may be called during parent registration
    if not hasattr(self, '_recombination_map'):
        self._recombination_map: Optional[Chromosome.RecombinationMap] = None
        self._sorted_loci_cache: Optional[List[Locus]] = None  # Cache for sorted loci
        self._sex_type: SexChromosomeType = SexChromosomeType.AUTOSOME

    # Check if already initialized (cached instance)
    if hasattr(self, '_initialized') and self._initialized:
        return

    # Parent is alias for species
    if species is None:
        species = parent

    # Set sex chromosome type
    self._set_sex_type(sex_type)

    # Chromosome's species is automatically inherited from parent Species
    super().__init__(name, parent=species)

    if loci:
        for locus in loci:
            self.add_locus(locus)

    # Initialize recombination map
    self._invalidate_recombination_map_cache()

    # Set recombination rates if provided
    if recombination_rates is not None:
        if len(self.loci) < 2:
            raise ValueError("Cannot set recombination rates with less than 2 loci.")
        if len(recombination_rates) != len(self.loci) - 1:
            raise ValueError(
                f"Expected {len(self.loci) - 1} recombination rates for {len(self.loci)} loci, "
                f"got {len(recombination_rates)} instead."
            )
        for i, rate in enumerate(recombination_rates):
            self.recombination_map[i] = rate
sex_type property writable
sex_type: SexChromosomeType

Returns the sex chromosome type

is_sex_chromosome property
is_sex_chromosome: bool

Whether this is a sex chromosome

is_autosome property
is_autosome: bool

Whether this is an autosome

sex_system property
sex_system: Optional[str]

Returns the sex determination system this chromosome belongs to ('XY', 'ZW', or None)

entity_type property
entity_type

Lazy import to avoid circular dependency.

loci property
loci: List[Locus]

Returns the list of loci in this chromosome, sorted by position (cached).

recombination_map property
recombination_map: RecombinationMap

Returns the recombination map for this chromosome.

The recombination map stores recombination rates between adjacent loci. For n loci, the map has n-1 entries where entry i is the recombination rate between locus i and locus i+1.

recombination_matrix property
recombination_matrix: RecombinationMap

Deprecated: Use recombination_map instead.

RecombinationMap
RecombinationMap(loci: Optional[List[Locus]] = None, rates: Optional[ndarray] = None)

A 1D container storing recombination rates between adjacent loci.

For n loci, the map has n-1 entries where entry i is the recombination rate between locus i and locus i+1 (in sorted order by position).

Attributes:

Name Type Description
loci_names List[str]

Locus names in sorted positional order.

_rates ndarray

Adjacent-locus recombination rates with length n-1.

Examples:

For loci [A, B, C, D], the map is [r(A,B), r(B,C), r(C,D)] where index i = rate between locus i and locus i+1.

Source code in src/natal/genetic_structures.py
def __init__(
    self,
    loci: Optional[List[Locus]] = None,
    rates: Optional[np.ndarray] = None
) -> None:
    size = len(loci) - 1 if loci and len(loci) > 1 else 0
    if size <= 0:
        raise ValueError("RecombinationMap requires at least 2 loci.")

    self._rates = np.zeros(size, dtype=np.float64)
    self.loci_names = [locus.name for locus in loci] if loci else []

    if rates is not None:
        if len(rates) != size:
            raise ValueError(f"Expected {size} rates, got {len(rates)}")
        self._rates[:] = rates
name_to_index
name_to_index(name: str) -> int

Public wrapper for converting locus name to locus index.

Source code in src/natal/genetic_structures.py
def name_to_index(self, name: str) -> int:
    """Public wrapper for converting locus name to locus index."""
    return self._name_to_index(name)
validate
validate() -> Tuple[bool, str]

Validate the recombination map.

Source code in src/natal/genetic_structures.py
def validate(self) -> Tuple[bool, str]:
    """Validate the recombination map."""
    if np.any(self._rates < 0) or np.any(self._rates > 0.5):
        return False, "Values out of range [0, 0.5]."
    return True, "Map is valid."
get_adjacent_loci
get_adjacent_loci(index: int) -> Tuple[str, str]

Get the names of the two loci at the given rate index.

Source code in src/natal/genetic_structures.py
def get_adjacent_loci(self, index: int) -> Tuple[str, str]:
    """Get the names of the two loci at the given rate index."""
    if index < 0 or index >= len(self):
        raise IndexError(f"Index {index} out of range for map of size {len(self)}")
    return (self.loci_names[index], self.loci_names[index + 1])
add_locus
add_locus(locus_or_name: Union[Locus, str], position: Optional[Union[int, float]] = None, recombination_rate_with_previous: float = 0.0, **kwargs: Any) -> Locus

Add a locus to this chromosome.

When inserting a new locus: - If it's the first locus of the chromosome: the recombination rate parameter sets the rate with the next (second) locus. - Otherwise: the recombination rate parameter sets the rate with the previous locus, and the rate with the next locus is inherited from the old rate between the previous and next loci.

Parameters:

Name Type Description Default
locus_or_name Union[Locus, str]

Either a Locus instance or a name to create a new Locus.

required
position Optional[Union[int, float]]

Optional position (only used when creating new Locus by name). If not specified, defaults to max(position) + 1 among existing loci.

None
recombination_rate_with_previous float

Recombination rate with the adjacent locus. Defaults to 0 (complete linkage). If the first locus of the chromosome, sets the rate with the second locus; otherwise sets the rate with the previous locus.

0.0
**kwargs Any

Additional custom parameters to pass to the Locus constructor.

{}

Returns:

Type Description
Locus

The added Locus instance.

Source code in src/natal/genetic_structures.py
def add_locus(
    self,
    locus_or_name: Union[Locus, str],
    position: Optional[Union[int, float]] = None,
    recombination_rate_with_previous: float = 0.0,
    **kwargs: Any
) -> Locus:
    """
    Add a locus to this chromosome.

    When inserting a new locus:
    - If it's the first locus of the chromosome: the recombination rate parameter
      sets the rate with the next (second) locus.
    - Otherwise: the recombination rate parameter sets the rate with the previous
      locus, and the rate with the next locus is inherited from the old rate
      between the previous and next loci.

    Args:
        locus_or_name: Either a Locus instance or a name to create a new Locus.
        position: Optional position (only used when creating new Locus by name).
            If not specified, defaults to max(position) + 1 among existing loci.
        recombination_rate_with_previous: Recombination rate with the adjacent locus.
            Defaults to 0 (complete linkage). If the first locus of the chromosome,
            sets the rate with the second locus; otherwise sets the rate with the
            previous locus.
        **kwargs: Additional custom parameters to pass to the Locus constructor.

    Returns:
        The added Locus instance.
    """
    # Get current sorted loci and old map before adding
    old_sorted_loci = self.loci.copy() if self._sorted_loci_cache else []
    old_map = self._recombination_map

    assert isinstance(locus_or_name, (Locus, str)), \
        f"Expected Locus instance or str, got {type(locus_or_name).__name__}"
    if isinstance(locus_or_name, str):
        # Create new Locus via base class add method with kwargs
        created = self.add(locus_or_name, position=position, **kwargs)
        assert isinstance(created, Locus), \
            f"Expected add() to return Locus, got {type(created).__name__}"
        locus = created
    else:
        locus = locus_or_name
        # Register existing Locus if not already in registry
        if locus.name not in self.child_structures:
            self.child_structures.register(locus)

    # Invalidate cache and update recombination map with insertion handling
    self._sorted_loci_cache = None
    self._update_recombination_map_on_insert(
        locus, old_sorted_loci, old_map, recombination_rate_with_previous
    )
    if self._species is not None:
        self._species.invalidate_gene_index_cache()
    return locus
remove_locus
remove_locus(locus_or_name: Union[Locus, str]) -> None

Remove a locus from this chromosome.

When removing a locus, the recombination rates are adjusted to maintain connectivity between the remaining loci.

Parameters:

Name Type Description Default
locus_or_name Union[Locus, str]

Either a Locus instance or a name.

required
Source code in src/natal/genetic_structures.py
def remove_locus(self, locus_or_name: Union[Locus, str]) -> None:
    """
    Remove a locus from this chromosome.

    When removing a locus, the recombination rates are adjusted to maintain
    connectivity between the remaining loci.

    Args:
        locus_or_name: Either a Locus instance or a name.
    """
    if isinstance(locus_or_name, str):
        name = locus_or_name
    else:
        name = locus_or_name.name

    if name in self.child_structures:
        # Get old state
        old_sorted_loci = self.loci.copy()
        old_map = self._recombination_map

        # Find the index of the locus to remove
        locus_to_remove = self.child_structures.get(name)
        remove_idx = old_sorted_loci.index(locus_to_remove)

        # Unregister the locus
        self.child_structures.unregister(name)
        self._sorted_loci_cache = None

        # Update recombination map
        self._update_recombination_map_on_remove(remove_idx, old_map)
        if self._species is not None:
            self._species.invalidate_gene_index_cache()
get_locus
get_locus(name: str) -> Optional[Locus]

Get a locus by name.

Parameters:

Name Type Description Default
name str

Name of the locus.

required

Returns:

Type Description
Optional[Locus]

The Locus instance or None if not found.

Source code in src/natal/genetic_structures.py
def get_locus(self, name: str) -> Optional[Locus]:
    """
    Get a locus by name.

    Args:
        name: Name of the locus.

    Returns:
        The Locus instance or None if not found.
    """
    if name in self.child_structures:
        return self.child_structures.get(name)
    return None
invalidate_recombination_map_cache
invalidate_recombination_map_cache() -> None

Public wrapper for recombination-map cache invalidation.

Source code in src/natal/genetic_structures.py
def invalidate_recombination_map_cache(self) -> None:
    """Public wrapper for recombination-map cache invalidation."""
    self._invalidate_recombination_map_cache()
get_locus_index
get_locus_index(name: str) -> int

Get the index of a locus by name in the sorted loci list.

Source code in src/natal/genetic_structures.py
def get_locus_index(self, name: str) -> int:
    """Get the index of a locus by name in the sorted loci list."""
    return self.recombination_map.name_to_index(name)
set_recombination
set_recombination(locus_a: Union[Locus, str], locus_b: Union[Locus, str], rate: float)

Set the recombination rate between two adjacent loci.

Parameters:

Name Type Description Default
locus_a Union[Locus, str]

First locus (by name or Locus object)

required
locus_b Union[Locus, str]

Second locus (by name or Locus object)

required
rate float

Recombination rate (must be in [0, 0.5])

required

Raises:

Type Description
KeyError

If the loci are not adjacent

ValueError

If rate is out of range or fewer than 2 loci

Source code in src/natal/genetic_structures.py
def set_recombination(self, locus_a: Union[Locus, str], locus_b: Union[Locus, str], rate: float):
    """
    Set the recombination rate between two adjacent loci.

    Args:
        locus_a: First locus (by name or Locus object)
        locus_b: Second locus (by name or Locus object)
        rate: Recombination rate (must be in [0, 0.5])

    Raises:
        KeyError: If the loci are not adjacent
        ValueError: If rate is out of range or fewer than 2 loci
    """
    if self._recombination_map is None:
        raise ValueError("Cannot set recombination rate with fewer than 2 loci.")
    self.recombination_map[locus_a, locus_b] = rate
set_recombination_bulk
set_recombination_bulk(settings: Dict[Tuple[Union[Locus, str], Union[Locus, str]], float])

Bulk set recombination rates between adjacent loci.

Parameters:

Name Type Description Default
settings Dict[Tuple[Union[Locus, str], Union[Locus, str]], float]

Dictionary of {(locus_a, locus_b): rate}

required
Source code in src/natal/genetic_structures.py
def set_recombination_bulk(self, settings: Dict[Tuple[Union[Locus, str], Union[Locus, str]], float]):
    """
    Bulk set recombination rates between adjacent loci.

    Args:
        settings: Dictionary of {(locus_a, locus_b): rate}
    """
    for (a, b), rate in settings.items():
        self.set_recombination(a, b, rate)
set_recombination_all
set_recombination_all(value: float)

Set all recombination rates to the same value.

Parameters:

Name Type Description Default
value float

Recombination rate (must be in [0, 0.5])

required
Source code in src/natal/genetic_structures.py
def set_recombination_all(self, value: float):
    """
    Set all recombination rates to the same value.

    Args:
        value: Recombination rate (must be in [0, 0.5])
    """
    if self._recombination_map is not None:
        self._recombination_map[:] = value
set_recombination_default
set_recombination_default(value: float)

Deprecated: Use set_recombination_all instead.

Source code in src/natal/genetic_structures.py
def set_recombination_default(self, value: float):
    """Deprecated: Use set_recombination_all instead."""
    self.set_recombination_all(value)
set_recombination_rate
set_recombination_rate(locus_a: Union[Locus, str], locus_b: Union[Locus, str], rate: float)

Deprecated: Use set_recombination instead.

Source code in src/natal/genetic_structures.py
def set_recombination_rate(self, locus_a: Union[Locus, str], locus_b: Union[Locus, str], rate: float):
    """
    Deprecated: Use set_recombination instead.
    """
    self.set_recombination(locus_a, locus_b, rate)
set_recombination_rates
set_recombination_rates(settings: Dict[Tuple[Union[Locus, str], Union[Locus, str]], float])

Deprecated: Use set_recombination_bulk instead.

Source code in src/natal/genetic_structures.py
def set_recombination_rates(self, settings: Dict[Tuple[Union[Locus, str], Union[Locus, str]], float]):
    """
    Deprecated: Use set_recombination_bulk instead.
    """
    self.set_recombination_bulk(settings)

Species

Species(name: str, chromosomes: Optional[List[Chromosome]] = None, gamete_labels: Optional[list[str]] = None)

Bases: GeneticStructure['HaploidGenome']

Represents the complete genetic architecture defined by chromosomes.

A Species is the top-level structure that contains multiple Chromosomes, each representing a chromosome with its loci and recombination rates.

Attributes:

Name Type Description
child_structures ChildStructureRegistry[Chromosome]

Registry of chromosomes.

structure_cache Dict[type, Dict[str, GeneticStructure[Any]]]

Species-scoped structure cache grouped by structure type.

gamete_labels List[str]

Optional labels used to identify gamete categories.

This class is also exported as GenomeTemplate for backward compatibility.

Source code in src/natal/genetic_structures.py
def __init__(
    self,
    name: str,
    chromosomes: Optional[List[Chromosome]] = None,
    gamete_labels: Optional[list[str]] = None
):
    # Initialize structure caches for this Species
    # Format: {structure_type: {name: instance}}
    self._structure_cache: Dict[type, Dict[str, GeneticStructure[Any]]] = {}
    self._gene_index_cache: Optional[Dict[str, Gene]] = None

    super().__init__(name, parent=None, species=None)  # Species is top-level, no parent

    # Set self as the species
    self._species = self

    # Add initial chromosomes if provided
    if chromosomes:
        for chrom in chromosomes:
            self.add_chromosome(chrom)

    # Gamete labels for this species
    if gamete_labels is not None:
        self._gamete_labels = list(gamete_labels)
    else:
        self._gamete_labels = []
gamete_labels property writable
gamete_labels: List[str]

Return the list of gamete labels for this species.

entity_type property
entity_type

Lazy import to avoid circular dependency.

structure_cache property
structure_cache: Dict[type, Dict[str, GeneticStructure[Any]]]

Public accessor for species-scoped structure caches.

chromosomes property
chromosomes: List[Chromosome]

Returns the list of chromosomes in this species.

linkages property
linkages: List[Chromosome]

Alias for chromosomes (backward compatibility).

sex_chromosomes property
sex_chromosomes: List[Chromosome]

Returns all sex chromosomes

autosomes property
autosomes: List[Chromosome]

Returns all autosomes

sex_system property
sex_system: Optional[str]

Returns the sex determination system ('XY', 'ZW', or None).

Automatically inferred from Chromosome.sex_type. Raises an error if multiple systems are found.

gene_index property
gene_index: Dict[str, Gene]

Returns a mapping from gene names to gene instances.

clear_structure_cache
clear_structure_cache() -> None

Clear all structure caches for this Species. This removes all cached Structure instances (Locus, Chromosome) within this Species.

Source code in src/natal/genetic_structures.py
def clear_structure_cache(self) -> None:
    """
    Clear all structure caches for this Species.
    This removes all cached Structure instances (Locus, Chromosome) within this Species.
    """
    self._structure_cache.clear()
    self._invalidate_gene_index_cache()
clear_entity_cache
clear_entity_cache() -> None

Clear all entity caches for this Species. This removes all cached Entity instances (Gene, Haplotype, etc.) within this Species.

Source code in src/natal/genetic_structures.py
def clear_entity_cache(self) -> None:
    """
    Clear all entity caches for this Species.
    This removes all cached Entity instances (Gene, Haplotype, etc.) within this Species.
    """
    from natal.genetic_entities import GeneticEntity
    GeneticEntity.clear_species_cache(id(self))
clear_all_caches
clear_all_caches() -> None

Clear both structure and entity caches for this Species.

Source code in src/natal/genetic_structures.py
def clear_all_caches(self) -> None:
    """
    Clear both structure and entity caches for this Species.
    """
    self.clear_structure_cache()
    self.clear_entity_cache()
invalidate_gene_index_cache
invalidate_gene_index_cache() -> None

Public wrapper for invalidating the species gene-index cache.

Source code in src/natal/genetic_structures.py
def invalidate_gene_index_cache(self) -> None:
    """Public wrapper for invalidating the species gene-index cache."""
    self._invalidate_gene_index_cache()
add_chromosome
add_chromosome(chrom_or_name: Union[Chromosome, str], loci: Optional[List[Locus]] = None, sex_type: Optional[Union[SexChromosomeType, str]] = None) -> Chromosome

Add a chromosome to this species.

Parameters:

Name Type Description Default
chrom_or_name Union[Chromosome, str]

Either a Chromosome instance or a name to create a new one.

required
loci Optional[List[Locus]]

Optional list of loci (only used when creating new Chromosome by name).

None
sex_type Optional[Union[SexChromosomeType, str]]

Optional sex chromosome type (X', 'Y', 'Z', 'W', None).

None

Returns:

Type Description
Chromosome

The added Chromosome instance.

Source code in src/natal/genetic_structures.py
def add_chromosome(
    self,
    chrom_or_name: Union[Chromosome, str],
    loci: Optional[List[Locus]] = None,
    sex_type: Optional[Union[SexChromosomeType, str]] = None
) -> Chromosome:
    """
    Add a chromosome to this species.

    Args:
        chrom_or_name: Either a Chromosome instance or a name to create a new one.
        loci: Optional list of loci (only used when creating new Chromosome by name).
        sex_type: Optional sex chromosome type (X', 'Y', 'Z', 'W', None).

    Returns:
        The added Chromosome instance.
    """
    assert isinstance(chrom_or_name, (Chromosome, str)), \
        f"Expected Chromosome instance or str, got {type(chrom_or_name).__name__}"

    if isinstance(chrom_or_name, str):
        # Create new Chromosome via base class add method
        created = self.add(chrom_or_name, loci=loci, sex_type=sex_type)
        assert isinstance(created, Chromosome), \
            f"Expected add() to return Chromosome, got {type(created).__name__}"
        chrom = created
    else:
        chrom = chrom_or_name
        # Update sex_type if provided
        if sex_type is not None:
            chrom.sex_type = sex_type
        # Register existing Chromosome if not already in registry
        if chrom.name not in self.child_structures:
            self.child_structures.register(chrom)

    self.invalidate_gene_index_cache()
    return chrom
add_linkage
add_linkage(linkage_or_name: Union[Chromosome, str], loci: Optional[List[Locus]] = None) -> Chromosome

Alias for add_chromosome (backward compatibility).

Source code in src/natal/genetic_structures.py
def add_linkage(
    self,
    linkage_or_name: Union[Chromosome, str],
    loci: Optional[List[Locus]] = None
) -> Chromosome:
    """Alias for add_chromosome (backward compatibility)."""
    return self.add_chromosome(linkage_or_name, loci=loci)
remove_chromosome
remove_chromosome(chrom_or_name: Union[Chromosome, str]) -> None

Remove a chromosome from this species.

Parameters:

Name Type Description Default
chrom_or_name Union[Chromosome, str]

Either a Chromosome instance or a name.

required
Source code in src/natal/genetic_structures.py
def remove_chromosome(self, chrom_or_name: Union[Chromosome, str]) -> None:
    """
    Remove a chromosome from this species.

    Args:
        chrom_or_name: Either a Chromosome instance or a name.
    """
    if isinstance(chrom_or_name, str):
        name = chrom_or_name
    else:
        name = chrom_or_name.name

    if name in self.child_structures:
        self.child_structures.unregister(name)
        self._invalidate_gene_index_cache()
remove_linkage
remove_linkage(linkage_or_name: Union[Chromosome, str]) -> None

Alias for remove_chromosome (backward compatibility).

Source code in src/natal/genetic_structures.py
def remove_linkage(self, linkage_or_name: Union[Chromosome, str]) -> None:
    """Alias for remove_chromosome (backward compatibility)."""
    return self.remove_chromosome(linkage_or_name)
get_all_loci
get_all_loci() -> List[Locus]

Returns all loci across all chromosomes.

Source code in src/natal/genetic_structures.py
def get_all_loci(self) -> List[Locus]:
    """Returns all loci across all chromosomes."""
    all_loci: List[Locus] = []
    for chrom in self.chromosomes:
        all_loci.extend(chrom.loci)
    return all_loci
from_dict classmethod
from_dict(name: str, structure: Dict[str, Union[List[str], Dict[str, List[str]], Dict[str, Any]]], gamete_labels: Optional[List[str]] = None) -> Species

Create a Species with complete hierarchy from a dictionary specification.

Parameters:

Name Type Description Default
name str

Name of the species.

required
structure Dict[str, Union[List[str], Dict[str, List[str]], Dict[str, Any]]]

Dictionary defining the structure. Format: { 'ChromName': ['Locus1', 'Locus2', ...], # Simple: locus names only # OR 'ChromName': { 'Locus1': ['allele1', 'allele2'], # With alleles 'Locus2': ['allele1', 'allele2'], } # OR 'ChromName': { 'sex_type': 'X', 'loci': { 'Locus1': ['allele1', 'allele2'], }, } }

required

Returns:

Type Description
Species

Species instance with all Chromosomes and Loci created.

Examples:

>>> # Simple: just loci names
>>> species = Species.from_dict('Species', {
...     'Chr1': ['LocusA', 'LocusB'],
...     'Chr2': ['LocusC']
... })
>>>
>>> # With alleles
>>> species = Species.from_dict('Species', {
...     'Chr1': {
...         'LocusA': ['A1', 'A2'],
...         'LocusB': ['B1', 'B2', 'B3']
...     },
...     'Chr2': {
...         'LocusC': ['C1', 'C2']
...     }
... })
Source code in src/natal/genetic_structures.py
@classmethod
def from_dict(
    cls,
    name: str,
    structure: Dict[str, Union[List[str], Dict[str, List[str]], Dict[str, Any]]],
    gamete_labels: Optional[List[str]] = None
) -> Species:
    """Create a Species with complete hierarchy from a dictionary specification.

    Args:
        name: Name of the species.
        structure: Dictionary defining the structure. Format:
            {
                'ChromName': ['Locus1', 'Locus2', ...],  # Simple: locus names only
                # OR
                'ChromName': {
                    'Locus1': ['allele1', 'allele2'],  # With alleles
                    'Locus2': ['allele1', 'allele2'],
                }
                # OR
                'ChromName': {
                    'sex_type': 'X',
                    'loci': {
                        'Locus1': ['allele1', 'allele2'],
                    },
                }
            }

    Returns:
        Species instance with all Chromosomes and Loci created.

    Examples:
        >>> # Simple: just loci names
        >>> species = Species.from_dict('Species', {
        ...     'Chr1': ['LocusA', 'LocusB'],
        ...     'Chr2': ['LocusC']
        ... })
        >>>
        >>> # With alleles
        >>> species = Species.from_dict('Species', {
        ...     'Chr1': {
        ...         'LocusA': ['A1', 'A2'],
        ...         'LocusB': ['B1', 'B2', 'B3']
        ...     },
        ...     'Chr2': {
        ...         'LocusC': ['C1', 'C2']
        ...     }
        ... })
    """
    from natal.genetic_entities import Gene

    species = cls(name, gamete_labels=gamete_labels)

    for chrom_name, loci_spec in structure.items():
        sex_type: Optional[Union[SexChromosomeType, str]] = None
        normalized_loci_spec: Union[List[str], Dict[str, List[str]]]

        # Extended chromosome config format:
        # {
        #   "sex_type": "X" | "Y" | "Z" | "W" | "autosome",
        #   "loci": [...]/{...}
        # }
        if isinstance(loci_spec, dict) and ("loci" in loci_spec or "sex_type" in loci_spec):
            raw_loci = loci_spec.get("loci")
            if raw_loci is None:
                raw_loci = []
            assert isinstance(raw_loci, (list, dict)), \
                f"Invalid loci specification for chromosome '{chrom_name}'. " \
                f"Expected list or dict, got {type(raw_loci).__name__}"
            normalized_loci_spec = cast(Union[List[str], Dict[str, List[str]]], raw_loci)

            raw_sex_type = loci_spec.get("sex_type")
            if raw_sex_type is not None:
                assert isinstance(raw_sex_type, (SexChromosomeType, str)), \
                    f"Invalid sex_type for chromosome '{chrom_name}'. " \
                    f"Expected SexChromosomeType or str, got {type(raw_sex_type).__name__}"
                sex_type = raw_sex_type
        else:
            assert isinstance(loci_spec, (list, dict)), \
                f"Invalid loci specification for chromosome '{chrom_name}'. " \
                f"Expected list or dict, got {type(loci_spec).__name__}"
            normalized_loci_spec = cast(Union[List[str], Dict[str, List[str]]], loci_spec)

        chrom = species.add_chromosome(chrom_name, sex_type=sex_type)

        if isinstance(normalized_loci_spec, list):
            # Simple format: list of locus names
            for locus_name in normalized_loci_spec:
                chrom.add_locus(locus_name)
        else: # Detailed format: {locus_name: [allele_names]}
            for locus_name, allele_names in normalized_loci_spec.items():
                locus = chrom.add_locus(locus_name)
                # Create alleles (genes)
                for allele_name in allele_names:
                    Gene(allele_name, locus=locus)

    return species
get_locus
get_locus(name: str) -> Optional[Locus]

Get a locus by name across all chromosomes.

Parameters:

Name Type Description Default
name str

Name of the locus.

required

Returns:

Type Description
Optional[Locus]

The Locus instance or None if not found.

Source code in src/natal/genetic_structures.py
def get_locus(self, name: str) -> Optional[Locus]:
    """
    Get a locus by name across all chromosomes.

    Args:
        name: Name of the locus.

    Returns:
        The Locus instance or None if not found.
    """
    for chrom in self.chromosomes:
        for locus in chrom.loci:
            if locus.name == name:
                return locus
    return None
get_chromosome
get_chromosome(name: str) -> Optional[Chromosome]

Get a chromosome by name.

Parameters:

Name Type Description Default
name str

Name of the chromosome.

required

Returns:

Type Description
Optional[Chromosome]

The Chromosome instance or None if not found.

Source code in src/natal/genetic_structures.py
def get_chromosome(self, name: str) -> Optional[Chromosome]:
    """
    Get a chromosome by name.

    Args:
        name: Name of the chromosome.

    Returns:
        The Chromosome instance or None if not found.
    """
    if name in self.child_structures:
        return self.child_structures.get(name)
    return None
get_gene
get_gene(name: str) -> Optional[Gene]

Get a gene by name across all loci.

Parameters:

Name Type Description Default
name str

Name of the gene.

required

Returns:

Type Description
Optional[Gene]

The Gene instance or None if not found.

Raises:

Type Description
ValueError

If duplicate gene names exist in the species.

Source code in src/natal/genetic_structures.py
def get_gene(self, name: str) -> Optional[Gene]:
    """
    Get a gene by name across all loci.

    Args:
        name: Name of the gene.

    Returns:
        The Gene instance or None if not found.

    Raises:
        ValueError: If duplicate gene names exist in the species.
    """
    try:
        gene_index = self._build_gene_index()
        return gene_index.get(name)
    except ValueError:
        # Re-raise the duplicate gene error
        raise
has_gene
has_gene(name: str) -> bool

Check if a gene with the given name exists in the species.

Parameters:

Name Type Description Default
name str

Name of the gene to check.

required

Returns:

Type Description
bool

True if the gene exists, False otherwise.

Raises:

Type Description
ValueError

If duplicate gene names exist in the species.

Source code in src/natal/genetic_structures.py
def has_gene(self, name: str) -> bool:
    """
    Check if a gene with the given name exists in the species.

    Args:
        name: Name of the gene to check.

    Returns:
        True if the gene exists, False otherwise.

    Raises:
        ValueError: If duplicate gene names exist in the species.
    """
    try:
        gene_index = self._build_gene_index()
        return name in gene_index
    except ValueError:
        # Re-raise the duplicate gene error
        raise
get_linkage
get_linkage(name: str) -> Optional[Chromosome]

Alias for get_chromosome (backward compatibility).

Source code in src/natal/genetic_structures.py
def get_linkage(self, name: str) -> Optional[Chromosome]:
    """Alias for get_chromosome (backward compatibility)."""
    return self.get_chromosome(name)
get_haploid_genome_from_str
get_haploid_genome_from_str(haploid_str: str) -> HaploidGenome

Create or retrieve a HaploidGenome from a string representation.

Supported syntax includes
  • Semicolon (;) separates different chromosomes
  • Slash (/) separates genes within a chromosome
  • If all genes are single characters, slash can be omitted

Parameters:

Name Type Description Default
haploid_str str

String like "ABC;XY" or "a1/b1/c1;x1/y1"

required

Returns:

Type Description
HaploidGenome

HaploidGenome instance

Examples:

>>> species = Species.from_dict("Test", {
...     "Chr1": {"A": ["A", "a"], "B": ["B", "b"], "C": ["C", "c"]},
...     "Chr2": {"X": ["X", "x"], "Y": ["Y", "y"]}
... })
>>> hap = species.get_haploid_genome_from_str("ABC;XY")
>>> hap = species.get_haploid_genome_from_str("a/b/c;x/y")  # equivalent
Source code in src/natal/genetic_structures.py
def get_haploid_genome_from_str(self, haploid_str: str) -> HaploidGenome:
    """
    Create or retrieve a HaploidGenome from a string representation.

    Supported syntax includes:
        - Semicolon (;) separates different chromosomes
        - Slash (/) separates genes within a chromosome
        - If all genes are single characters, slash can be omitted

    Args:
        haploid_str: String like "ABC;XY" or "a1/b1/c1;x1/y1"

    Returns:
        HaploidGenome instance

    Examples:
        >>> species = Species.from_dict("Test", {
        ...     "Chr1": {"A": ["A", "a"], "B": ["B", "b"], "C": ["C", "c"]},
        ...     "Chr2": {"X": ["X", "x"], "Y": ["Y", "y"]}
        ... })
        >>> hap = species.get_haploid_genome_from_str("ABC;XY")
        >>> hap = species.get_haploid_genome_from_str("a/b/c;x/y")  # equivalent
    """
    from natal.genetic_entities import HaploidGenome, Haplotype

    gene_index = self._build_gene_index()

    # Split by semicolon for different chromosomes
    hap_strs = [s.strip() for s in haploid_str.split(';') if s.strip()]

    # Allow sex chromosome groups: exactly one chromosome per group is required
    sex_chr_groups = getattr(self, '_sex_chromosome_groups', None)
    if sex_chr_groups:
        # Expected segments = autosomes count + number of sex groups
        autosome_count = 0
        for chrom in self.chromosomes:
            # A chromosome is considered part of a sex group if it appears in any group list
            in_sex_group = any(chrom in group for group in sex_chr_groups.values())
            if not in_sex_group:
                autosome_count += 1
        expected_segments = autosome_count + len(sex_chr_groups)
    else:
        expected_segments = len(self.chromosomes)

    if len(hap_strs) != expected_segments:
        raise ValueError(
            f"Expected {expected_segments} haplotype segments (one per chromosome; "
            f"for sex groups: one per group), got {len(hap_strs)}. "
            f"Chromosomes: {[c.name for c in self.chromosomes]}"
        )

    # Parse each haplotype segment
    haplotypes: List[Haplotype] = []
    chroms_used: Set[Chromosome] = set()

    for hap_str in hap_strs:
        chrom, genes = self._parse_haplotype_segment_str(hap_str, gene_index)

        if chrom in chroms_used:
            raise ValueError(
                f"Chromosome '{chrom.name}' appears multiple times in haploid genome string"
            )
        chroms_used.add(chrom)

        hap = Haplotype(chromosome=chrom, genes=genes)
        haplotypes.append(hap)

    # Sort haplotypes by chromosome order in species
    chrom_order = {chrom: i for i, chrom in enumerate(self.chromosomes)}
    haplotypes_sorted = sorted(haplotypes, key=lambda h: chrom_order[h.chromosome])

    return HaploidGenome(species=self, haplotypes=haplotypes_sorted)
get_haploid_genotype_from_str
get_haploid_genotype_from_str(haplotype_str: str) -> HaploidGenome

Alias for get_haploid_genome_from_str.

Source code in src/natal/genetic_structures.py
def get_haploid_genotype_from_str(self, haplotype_str: str) -> HaploidGenome:
    """Alias for get_haploid_genome_from_str."""
    return self.get_haploid_genome_from_str(haplotype_str)
get_genotype_from_str
get_genotype_from_str(genotype_str: str) -> Genotype

Create or retrieve a Genotype from a string representation.

Supported syntax includes
  • Pipe (|) separates maternal (left) and paternal (right) haploid genomes
  • Semicolon (;) separates different chromosomes
  • Slash (/) separates genes within a chromosome
  • If all genes are single characters, slash can be omitted

The order of chromosomes in the string does not need to match the internal chromosome order - matching is done by gene names.

Parameters:

Name Type Description Default
genotype_str str

String like "ABC|abc" or "a1/b1/c1|a2/b2/c2;X/Y|x/y"

required

Returns:

Type Description
Genotype

Genotype instance

Examples:

>>> species = Species.from_dict("Test", {
...     "Chr1": {"A": ["A", "a"], "B": ["B", "b"], "C": ["C", "c"]},
...     "Chr2": {"X": ["X", "x"], "Y": ["Y", "y"]}
... })
>>>
>>> # Simple single-char genes
>>> gt = species.get_genotype_from_str("ABC|abc;XY|xy")
>>>
>>> # Multi-char genes with slash separator
>>> gt = species.get_genotype_from_str("A1/B1/C1|A2/B2/C2;X1/Y1|X2/Y2")
>>>
>>> # Order doesn't matter (unordered matching)
>>> gt = species.get_genotype_from_str("XY|xy;ABC|abc")  # Same result
Source code in src/natal/genetic_structures.py
def get_genotype_from_str(self, genotype_str: str) -> Genotype:
    """
    Create or retrieve a Genotype from a string representation.

    Supported syntax includes:
        - Pipe (|) separates maternal (left) and paternal (right) haploid genomes
        - Semicolon (;) separates different chromosomes
        - Slash (/) separates genes within a chromosome
        - If all genes are single characters, slash can be omitted

    The order of chromosomes in the string does not need to match
    the internal chromosome order - matching is done by gene names.

    Args:
        genotype_str: String like "ABC|abc" or "a1/b1/c1|a2/b2/c2;X/Y|x/y"

    Returns:
        Genotype instance

    Examples:
        >>> species = Species.from_dict("Test", {
        ...     "Chr1": {"A": ["A", "a"], "B": ["B", "b"], "C": ["C", "c"]},
        ...     "Chr2": {"X": ["X", "x"], "Y": ["Y", "y"]}
        ... })
        >>>
        >>> # Simple single-char genes
        >>> gt = species.get_genotype_from_str("ABC|abc;XY|xy")
        >>>
        >>> # Multi-char genes with slash separator
        >>> gt = species.get_genotype_from_str("A1/B1/C1|A2/B2/C2;X1/Y1|X2/Y2")
        >>>
        >>> # Order doesn't matter (unordered matching)
        >>> gt = species.get_genotype_from_str("XY|xy;ABC|abc")  # Same result
    """
    from natal.genetic_entities import Genotype

    genotype_str = genotype_str.strip()

    # Split by semicolon first (different chromosomes)
    chrom_segments = [s.strip() for s in genotype_str.split(';') if s.strip()]

    # Allow sex chromosome groups: exactly one chromosome per group is required
    sex_chr_groups = getattr(self, '_sex_chromosome_groups', None)
    if sex_chr_groups:
        autosome_count = 0
        for chrom in self.chromosomes:
            in_sex_group = any(chrom in group for group in sex_chr_groups.values())
            if not in_sex_group:
                autosome_count += 1
        expected_segments = autosome_count + len(sex_chr_groups)
    else:
        expected_segments = len(self.chromosomes)

    if len(chrom_segments) != expected_segments:
        raise ValueError(
            f"Expected {expected_segments} chromosome segments (separated by ;, and one per sex group when defined), "
            f"got {len(chrom_segments)}. Chromosomes: {[c.name for c in self.chromosomes]}"
        )

    # For each chromosome segment, split by | to get maternal/paternal
    maternal_hap_strs: List[str] = []
    paternal_hap_strs: List[str] = []

    for segment in chrom_segments:
        parts = segment.split('|')
        if len(parts) != 2:
            raise ValueError(
                f"Each chromosome segment must have exactly 2 parts separated by '|'. "
                f"Got: '{segment}'"
            )
        maternal_hap_strs.append(parts[0].strip())
        paternal_hap_strs.append(parts[1].strip())

    # Build haploid genome strings and parse
    maternal_str = ';'.join(maternal_hap_strs)
    paternal_str = ';'.join(paternal_hap_strs)

    maternal = self.get_haploid_genome_from_str(maternal_str)
    paternal = self.get_haploid_genome_from_str(paternal_str)

    return Genotype(species=self, maternal=maternal, paternal=paternal)
resolve_genotype_selectors
resolve_genotype_selectors(selector: Union[Genotype, str, Tuple[Union[Genotype, str], ...]], all_genotypes: Optional[Iterable[Genotype]] = None, context: str = 'selector') -> List[Genotype]

Resolve one or more genotype selectors into concrete Genotype objects.

Parameters:

Name Type Description Default
selector Union[Genotype, str, Tuple[Union[Genotype, str], ...]]

Selector expression to resolve. Supported forms: - Genotype: treated as an exact selector. - str: resolved with exact-genotype parsing first; if exact parsing fails, falls back to genotype-pattern parsing. - tuple of Genotype/str: union semantics. Each atom is resolved independently, then merged with de-duplication while preserving first-seen order.

required
all_genotypes Optional[Iterable[Genotype]]

Optional candidate genotype iterable used by pattern matching. If None, all genotypes of the species are used.

None
context str

Human-readable context label used in error messages (for example "viability" or "sexual_selection").

'selector'

Returns:

Type Description
List[Genotype]

A list of resolved Genotype objects.

List[Genotype]
  • For a single selector atom, returns all matches from that atom.
List[Genotype]
  • For tuple selectors, returns the de-duplicated union of all atom matches.

Raises:

Type Description
TypeError

If a selector atom is neither Genotype nor str.

ValueError

If the selector is invalid, if pattern parsing fails, if a pattern matches no genotypes, or if a tuple selector is empty.

Source code in src/natal/genetic_structures.py
def resolve_genotype_selectors(
    self,
    selector: Union[Genotype, str, Tuple[Union[Genotype, str], ...]],
    all_genotypes: Optional[Iterable[Genotype]] = None,
    context: str = 'selector'
) -> List[Genotype]:
    """Resolve one or more genotype selectors into concrete ``Genotype`` objects.

    Args:
        selector: Selector expression to resolve. Supported forms:
            - ``Genotype``: treated as an exact selector.
            - ``str``: resolved with exact-genotype parsing first; if exact
              parsing fails, falls back to genotype-pattern parsing.
            - ``tuple`` of ``Genotype``/``str``: union semantics. Each atom
              is resolved independently, then merged with de-duplication
              while preserving first-seen order.
        all_genotypes: Optional candidate genotype iterable used by pattern
            matching. If ``None``, all genotypes of the species are used.
        context: Human-readable context label used in error messages (for
            example ``"viability"`` or ``"sexual_selection"``).

    Returns:
        A list of resolved ``Genotype`` objects.

        - For a single selector atom, returns all matches from that atom.
        - For tuple selectors, returns the de-duplicated union of all atom matches.

    Raises:
        TypeError: If a selector atom is neither ``Genotype`` nor ``str``.
        ValueError: If the selector is invalid, if pattern parsing fails, if
            a pattern matches no genotypes, or if a tuple selector is empty.
    """
    if all_genotypes is None:
        all_genotypes = self.get_all_genotypes()

    if isinstance(selector, tuple):
        if len(selector) == 0:
            raise ValueError(f"{context} selector tuple cannot be empty")

        merged: List[Genotype] = []
        for atom in selector:
            matches = self._resolve_single_genotype_selector(
                selector=atom,
                all_genotypes=all_genotypes,
                context=context,
            )
            for gt in matches:
                if gt not in merged:
                    merged.append(gt)
        return merged

    return self._resolve_single_genotype_selector(
        selector=selector,
        all_genotypes=all_genotypes,
        context=context,
    )
parse_genotype_pattern
parse_genotype_pattern(pattern: str) -> Callable[[Genotype], bool]

Parse a genotype pattern string and return a filter function.

Supports regex-like syntax for flexible pattern matching
  • ; separates chromosomes
  • | 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)
  • () explicitly groups chromosome loci
  • Omitted chromosomes default to wildcard matching

Parameters:

Name Type Description Default
pattern str

Pattern string, e.g. "A1/B1|A2/B2; C1/C2"

required

Returns:

Type Description
Callable[[Genotype], bool]

A filter function that takes a Genotype and returns bool.

Examples:

>>> filter_func = species.parse_genotype_pattern("A1/B1|A2/B2; C1::*")
>>> genotypes = [gt for gt in pop.genotypes if filter_func(gt)]

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/genetic_structures.py
def parse_genotype_pattern(self, pattern: str) -> Callable[[Genotype], bool]:
    """
    Parse a genotype pattern string and return a filter function.

    Supports regex-like syntax for flexible pattern matching:
        - ; separates chromosomes
        - | 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)
        - () explicitly groups chromosome loci
        - Omitted chromosomes default to wildcard matching

    Args:
        pattern: Pattern string, e.g. "A1/B1|A2/B2; C1/C2"

    Returns:
        A filter function that takes a Genotype and returns bool.

    Examples:
        >>> filter_func = species.parse_genotype_pattern("A1/B1|A2/B2; C1::*")
        >>> genotypes = [gt for gt in pop.genotypes if filter_func(gt)]

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    from natal.genetic_patterns import GenotypePatternParser
    parser = GenotypePatternParser(self)
    pattern_obj = parser.parse(pattern)
    return pattern_obj.to_filter()
filter_genotypes_by_pattern
filter_genotypes_by_pattern(genotypes: Iterable[Genotype], pattern: str) -> List[Genotype]

Filter a collection of genotypes by a pattern string.

Parameters:

Name Type Description Default
genotypes Iterable[Genotype]

Iterable of Genotype objects to filter.

required
pattern str

Pattern string (see parse_genotype_pattern for syntax).

required

Returns:

Type Description
List[Genotype]

List of genotypes that match the pattern.

Examples:

>>> matched = species.filter_genotypes_by_pattern(pop.genotypes, "A1/*|A2/B2")
Source code in src/natal/genetic_structures.py
def filter_genotypes_by_pattern(
    self,
    genotypes: Iterable[Genotype],
    pattern: str
) -> List[Genotype]:
    """
    Filter a collection of genotypes by a pattern string.

    Args:
        genotypes: Iterable of Genotype objects to filter.
        pattern: Pattern string (see parse_genotype_pattern for syntax).

    Returns:
        List of genotypes that match the pattern.

    Examples:
        >>> matched = species.filter_genotypes_by_pattern(pop.genotypes, "A1/*|A2/B2")
    """
    pattern_filter = self.parse_genotype_pattern(pattern)
    return [gt for gt in genotypes if pattern_filter(gt)]
enumerate_genotypes_matching_pattern
enumerate_genotypes_matching_pattern(pattern: str, max_count: Optional[int] = None) -> Iterable[Genotype]

Enumerate all genotypes matching a pattern.

Yields all possible genotype combinations that satisfy the pattern. Uses the pattern's built-in matching logic to filter candidates, avoiding complex combination generation.

Parameters:

Name Type Description Default
pattern str

Pattern string (see parse_genotype_pattern for syntax).

required
max_count Optional[int]

Maximum number of genotypes to yield (prevents explosion). If None, yields all possible genotypes.

None

Yields:

Type Description
Iterable[Genotype]

Genotype objects matching the pattern.

Examples:

>>> for gt in species.enumerate_genotypes_matching_pattern("A1/*|A2/*; C1/C1"):
...     print(gt)

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/genetic_structures.py
def enumerate_genotypes_matching_pattern(
    self,
    pattern: str,
    max_count: Optional[int] = None
) -> Iterable[Genotype]:
    """
    Enumerate all genotypes matching a pattern.

    Yields all possible genotype combinations that satisfy the pattern.
    Uses the pattern's built-in matching logic to filter candidates,
    avoiding complex combination generation.

    Args:
        pattern: Pattern string (see parse_genotype_pattern for syntax).
        max_count: Maximum number of genotypes to yield (prevents explosion).
                  If None, yields all possible genotypes.

    Yields:
        Genotype objects matching the pattern.

    Examples:
        >>> for gt in species.enumerate_genotypes_matching_pattern("A1/*|A2/*; C1/C1"):
        ...     print(gt)

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    from natal.genetic_patterns import GenotypePatternParser

    parser = GenotypePatternParser(self)
    pattern_obj = parser.parse(pattern)

    count = 0
    for genotype in self.iter_genotypes():
        if pattern_obj.matches(genotype):
            if max_count is not None and count >= max_count:
                return
            yield genotype
            count += 1
parse_haploid_genome_pattern
parse_haploid_genome_pattern(pattern: str) -> Callable[[HaploidGenome], bool]

Parse a haploid genome pattern string and return a filter function.

Supports regex-like syntax for flexible pattern matching of haploid genomes. A HaploidGenome represents one complete DNA strand (all haplotypes). Uses same syntax as Genotype patterns but applies to single haplotypes: - ; separates chromosomes - / separates loci within a chromosome - * matches any allele - {A,B,C} matches any allele in the set - !A matches any allele except A - () explicitly groups chromosome loci - Omitted chromosomes default to wildcard matching

Parameters:

Name Type Description Default
pattern str

Pattern string, e.g. "A1/B1; C1/C2"

required

Returns:

Type Description
Callable[[HaploidGenome], bool]

A filter function that takes a HaploidGenome and returns bool.

Examples:

>>> filter_func = species.parse_haploid_genome_pattern("A1/B1; C1")
>>> haploid_genomes = [hg for hg in pop.haploid_genomes if filter_func(hg)]

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/genetic_structures.py
def parse_haploid_genome_pattern(self, pattern: str) -> Callable[[HaploidGenome], bool]:
    """
    Parse a haploid genome pattern string and return a filter function.

    Supports regex-like syntax for flexible pattern matching of haploid genomes.
    A HaploidGenome represents one complete DNA strand (all haplotypes).
    Uses same syntax as Genotype patterns but applies to single haplotypes:
        - ; separates chromosomes
        - / separates loci within a chromosome
        - * matches any allele
        - {A,B,C} matches any allele in the set
        - !A matches any allele except A
        - () explicitly groups chromosome loci
        - Omitted chromosomes default to wildcard matching

    Args:
        pattern: Pattern string, e.g. "A1/B1; C1/C2"

    Returns:
        A filter function that takes a HaploidGenome and returns bool.

    Examples:
        >>> filter_func = species.parse_haploid_genome_pattern("A1/B1; C1")
        >>> haploid_genomes = [hg for hg in pop.haploid_genomes if filter_func(hg)]

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    from natal.genetic_patterns import GenotypePatternParser
    parser = GenotypePatternParser(self)
    pattern_obj = parser.parse_haploid_genome_pattern(pattern)
    return pattern_obj.to_filter()
filter_haploid_genomes_by_pattern
filter_haploid_genomes_by_pattern(haploid_genomes: Iterable[HaploidGenome], pattern: str) -> List[HaploidGenome]

Filter a collection of haploid genomes by a pattern string.

Parameters:

Name Type Description Default
haploid_genomes Iterable[HaploidGenome]

Iterable of HaploidGenome objects to filter.

required
pattern str

Pattern string (see parse_haploid_genome_pattern for syntax).

required

Returns:

Type Description
List[HaploidGenome]

List of haploid genomes that match the pattern.

Examples:

>>> matched = species.filter_haploid_genomes_by_pattern(pop.haploid_genomes, "A1/*; C1")
Source code in src/natal/genetic_structures.py
def filter_haploid_genomes_by_pattern(
    self,
    haploid_genomes: Iterable[HaploidGenome],
    pattern: str
) -> List[HaploidGenome]:
    """
    Filter a collection of haploid genomes by a pattern string.

    Args:
        haploid_genomes: Iterable of HaploidGenome objects to filter.
        pattern: Pattern string (see parse_haploid_genome_pattern for syntax).

    Returns:
        List of haploid genomes that match the pattern.

    Examples:
        >>> matched = species.filter_haploid_genomes_by_pattern(pop.haploid_genomes, "A1/*; C1")
    """
    pattern_filter = self.parse_haploid_genome_pattern(pattern)
    return [hg for hg in haploid_genomes if pattern_filter(hg)]
enumerate_haploid_genomes_matching_pattern
enumerate_haploid_genomes_matching_pattern(pattern: str, max_count: Optional[int] = None) -> Iterable[HaploidGenome]

Enumerate all haploid genomes matching a pattern.

Yields all possible haploid genome combinations that satisfy the pattern. Uses the pattern's built-in matching logic to filter candidates, avoiding complex combination generation.

Parameters:

Name Type Description Default
pattern str

Pattern string (see parse_haploid_genome_pattern for syntax).

required
max_count Optional[int]

Maximum number of haploid genomes to yield (prevents explosion). If None, yields all possible haploid genomes.

None

Yields:

Type Description
Iterable[HaploidGenome]

HaploidGenome objects matching the pattern.

Examples:

>>> for hg in species.enumerate_haploid_genomes_matching_pattern("A1/*; C1"):
...     print(hg)

Raises:

Type Description
PatternParseError

If the pattern is invalid.

Source code in src/natal/genetic_structures.py
def enumerate_haploid_genomes_matching_pattern(
    self,
    pattern: str,
    max_count: Optional[int] = None
) -> Iterable[HaploidGenome]:
    """
    Enumerate all haploid genomes matching a pattern.

    Yields all possible haploid genome combinations that satisfy the pattern.
    Uses the pattern's built-in matching logic to filter candidates,
    avoiding complex combination generation.

    Args:
        pattern: Pattern string (see parse_haploid_genome_pattern for syntax).
        max_count: Maximum number of haploid genomes to yield (prevents explosion).
                  If None, yields all possible haploid genomes.

    Yields:
        HaploidGenome objects matching the pattern.

    Examples:
        >>> for hg in species.enumerate_haploid_genomes_matching_pattern("A1/*; C1"):
        ...     print(hg)

    Raises:
        PatternParseError: If the pattern is invalid.
    """
    from natal.genetic_patterns import GenotypePatternParser

    parser = GenotypePatternParser(self)
    pattern_obj = parser.parse_haploid_genome_pattern(pattern)

    count = 0
    for haploid_genome in self.iter_haploid_genotypes():
        if pattern_obj.matches(haploid_genome):
            if max_count is not None and count >= max_count:
                return
            yield haploid_genome
            count += 1
get_sex_chromosome_groups
get_sex_chromosome_groups() -> Optional[Dict[str, List[Chromosome]]]

Public accessor for sex chromosome group configuration.

Source code in src/natal/genetic_structures.py
def get_sex_chromosome_groups(self) -> Optional[Dict[str, List[Chromosome]]]:
    """Public accessor for sex chromosome group configuration."""
    return self._get_sex_chromosome_groups()
count_alleles
count_alleles() -> int

Count the total number of alleles across all loci.

Returns:

Type Description
int

Total allele count.

Source code in src/natal/genetic_structures.py
def count_alleles(self) -> int:
    """
    Count the total number of alleles across all loci.

    Returns:
        Total allele count.
    """
    total = 0
    for chrom in self.chromosomes:
        for locus in chrom.loci:
            total += len(locus.alleles)
    return total
count_haploid_genotypes
count_haploid_genotypes() -> int

Calculate the total number of possible haploid genomes.

For each locus with n alleles, the haploid genome count = product of allele counts at each locus. If sex chromosome groups exist, only one chromosome is selected per group.

Returns:

Type Description
int

Total number of possible haploid genomes

Source code in src/natal/genetic_structures.py
def count_haploid_genotypes(self) -> int:
    """
    Calculate the total number of possible haploid genomes.

    For each locus with n alleles, the haploid genome count = product of allele counts at each locus.
    If sex chromosome groups exist, only one chromosome is selected per group.

    Returns:
        Total number of possible haploid genomes
    """
    # Resolve sex chromosome grouping (explicit config first, otherwise inferred).
    sex_chr_groups = self._get_sex_chromosome_groups()

    # Collect all chromosomes that belong to any sex chromosome group.
    sex_chroms: Set[Chromosome] = set()
    if sex_chr_groups:
        for group_chroms in sex_chr_groups.values():
            sex_chroms.update(group_chroms)

    total = 1

    # Multiply allele counts across autosomal loci.
    for chrom in self.chromosomes:
        if chrom in sex_chroms:
            continue  # Handle sex chromosomes separately below.
        for locus in chrom.loci:
            n_alleles = len(locus.alleles)
            if n_alleles > 0:
                total *= n_alleles

    # For each sex chromosome group, select one chromosome from the group.
    # A chromosome's haplotype count is the product of allele counts at its loci.
    if sex_chr_groups:
        for group_chroms in sex_chr_groups.values():
            group_total = 0
            for chrom in group_chroms:
                chrom_total = 1
                for locus in chrom.loci:
                    n_alleles = len(locus.alleles)
                    if n_alleles > 0:
                        chrom_total *= n_alleles
                group_total += chrom_total
            total *= group_total

    return total
count_genotypes
count_genotypes() -> int

Calculate the total number of possible diploid genotypes.

If _valid_sex_genotypes is defined, only valid sex chromosome combinations are counted.

Sex chromosome system configuration: - Can be automatically inferred by setting Chromosome.sex_type - Can also be manually set via _sex_chromosome_groups and _valid_sex_genotypes

Returns:

Type Description
int

Total number of possible genotypes

Source code in src/natal/genetic_structures.py
def count_genotypes(self) -> int:
    """
    Calculate the total number of possible diploid genotypes.

    If _valid_sex_genotypes is defined, only valid sex chromosome combinations are counted.

    Sex chromosome system configuration:
    - Can be automatically inferred by setting Chromosome.sex_type
    - Can also be manually set via _sex_chromosome_groups and _valid_sex_genotypes

    Returns:
        Total number of possible genotypes
    """
    # Resolve sex-system config (explicit settings first, otherwise inferred).
    sex_chr_groups = self._get_sex_chromosome_groups()
    valid_sex_gts = self._get_valid_sex_genotypes()

    if not sex_chr_groups:
        # No sex chromosomes: diploid count is haploid_count^2.
        n_haploid = self.count_haploid_genotypes()
        return n_haploid * n_haploid

    # With sex chromosomes, count autosomal and sex-system parts separately.
    sex_chroms: Set[Chromosome] = set()
    for group_chroms in sex_chr_groups.values():
        sex_chroms.update(group_chroms)

    autosome_haploid_count = 1
    for chrom in self.chromosomes:
        if chrom in sex_chroms:
            continue
        for locus in chrom.loci:
            n_alleles = len(locus.alleles)
            if n_alleles > 0:
                autosome_haploid_count *= n_alleles

    # Autosomal diploid count = autosomal_haploid_count^2.
    autosome_genotype_count = autosome_haploid_count * autosome_haploid_count

    # Count haplotypes produced by a single chromosome.
    def count_chrom_haplotypes(chrom: Chromosome) -> int:
        count = 1
        for locus in chrom.loci:
            n_alleles = len(locus.alleles)
            if n_alleles > 0:
                count *= n_alleles
        return count

    # Count sex chromosome genotype combinations.
    if valid_sex_gts:
        # Use explicitly defined valid combinations.
        sex_genotype_count = 0
        for mat_chrom, pat_chrom in valid_sex_gts:
            n_mat = count_chrom_haplotypes(mat_chrom)
            n_pat = count_chrom_haplotypes(pat_chrom)
            sex_genotype_count += n_mat * n_pat
    else:
        # If not constrained, all maternal/paternal pairings are treated as valid.
        sex_genotype_count = 1
        for group_chroms in sex_chr_groups.values():
            group_total = 0
            for chrom in group_chroms:
                group_total += count_chrom_haplotypes(chrom)
            # Each group contributes maternal x paternal pairings.
            sex_genotype_count *= group_total * group_total

    return autosome_genotype_count * sex_genotype_count
iter_haploid_genotypes
iter_haploid_genotypes() -> Iterable[HaploidGenome]

Iterate over all possible haploid genomes (HaploidGenome).

If sex chromosome groups exist, only one chromosome is selected per group. Note: This method returns all possible haploid genotypes without distinguishing between maternal/paternal. For scenarios requiring distinction, use iter_maternal_haploid_genotypes() and iter_paternal_haploid_genotypes().

Yields:

Type Description
Iterable[HaploidGenome]

HaploidGenome instances

Examples:

>>> for hg in species.iter_haploid_genotypes():
...     print(hg)
Source code in src/natal/genetic_structures.py
def iter_haploid_genotypes(self) -> Iterable[HaploidGenome]:
    """
    Iterate over all possible haploid genomes (HaploidGenome).

    If sex chromosome groups exist, only one chromosome is selected per group.
    Note: This method returns all possible haploid genotypes without distinguishing
    between maternal/paternal. For scenarios requiring distinction, use
    iter_maternal_haploid_genotypes() and iter_paternal_haploid_genotypes().

    Yields:
        HaploidGenome instances

    Examples:
        >>> for hg in species.iter_haploid_genotypes():
        ...     print(hg)
    """
    from natal.genetic_entities import HaploidGenome, Haplotype

    sex_chr_groups = self._get_sex_chromosome_groups()

    # Collect all chromosomes that belong to any sex chromosome group.
    sex_chroms: Set[Chromosome] = set()
    if sex_chr_groups:
        for group_chroms in sex_chr_groups.values():
            sex_chroms.update(group_chroms)

    # Collect all possible haplotypes for autosomes.
    autosome_haplotypes: List[List[Haplotype]] = []
    for chrom in self.chromosomes:
        if chrom in sex_chroms:
            continue  # Handle sex chromosomes separately.

        locus_alleles = [list(locus.alleles) for locus in chrom.loci]
        if not locus_alleles or any(len(a) == 0 for a in locus_alleles):
            continue

        haps_for_chrom: List[Haplotype] = []
        for genes in itertools.product(*locus_alleles):
            hap = Haplotype(chromosome=chrom, genes=list(genes))
            haps_for_chrom.append(hap)
        autosome_haplotypes.append(haps_for_chrom)

    # Collect haplotype options for each sex chromosome group.
    # A group contains all haplotypes from all chromosomes in that group.
    sex_group_haplotypes: List[List[Haplotype]] = []
    if sex_chr_groups:
        for group_chroms in sex_chr_groups.values():
            group_haps: List[Haplotype] = []
            for chrom in group_chroms:
                locus_alleles = [list(locus.alleles) for locus in chrom.loci]
                if not locus_alleles or any(len(a) == 0 for a in locus_alleles):
                    continue
                for genes in itertools.product(*locus_alleles):
                    hap = Haplotype(chromosome=chrom, genes=list(genes))
                    group_haps.append(hap)
            if group_haps:
                sex_group_haplotypes.append(group_haps)

    # Combine autosomal and sex-group haplotype option lists.
    all_haplotype_options = autosome_haplotypes + sex_group_haplotypes

    if not all_haplotype_options:
        return

    # Convert Cartesian product combinations into HaploidGenome objects.
    for haplotype_combo in itertools.product(*all_haplotype_options):
        yield HaploidGenome(species=self, haplotypes=list(haplotype_combo))
iter_maternal_haploid_genotypes
iter_maternal_haploid_genotypes() -> Iterable[HaploidGenome]

Iterate maternal haploid genomes that can be transmitted.

Yields:

Type Description
Iterable[HaploidGenome]

HaploidGenome instances.

Source code in src/natal/genetic_structures.py
def iter_maternal_haploid_genotypes(self) -> Iterable[HaploidGenome]:
    """
    Iterate maternal haploid genomes that can be transmitted.

    Yields:
        HaploidGenome instances.
    """
    return self._iter_haploid_genotypes_for_parent(is_paternal=False)
iter_paternal_haploid_genotypes
iter_paternal_haploid_genotypes() -> Iterable[HaploidGenome]

Iterate paternal haploid genomes that can be transmitted.

Yields:

Type Description
Iterable[HaploidGenome]

HaploidGenome instances.

Source code in src/natal/genetic_structures.py
def iter_paternal_haploid_genotypes(self) -> Iterable[HaploidGenome]:
    """
    Iterate paternal haploid genomes that can be transmitted.

    Yields:
        HaploidGenome instances.
    """
    return self._iter_haploid_genotypes_for_parent(is_paternal=True)
iter_genotypes
iter_genotypes() -> Iterable[Genotype]

Iterate all possible diploid genotypes.

Maternal and paternal sides are ordered, so (A|B) and (B|A) are distinct genotypes. When _valid_sex_genotypes or Chromosome.sex_type constraints are present, only valid sex chromosome pairings are emitted.

Yields:

Type Description
Iterable[Genotype]

Genotype instances.

Examples:

>>> for gt in species.iter_genotypes():
...     print(gt)
Source code in src/natal/genetic_structures.py
def iter_genotypes(self) -> Iterable[Genotype]:
    """
    Iterate all possible diploid genotypes.

    Maternal and paternal sides are ordered, so ``(A|B)`` and ``(B|A)``
    are distinct genotypes. When ``_valid_sex_genotypes`` or
    ``Chromosome.sex_type`` constraints are present, only valid sex
    chromosome pairings are emitted.

    Yields:
        Genotype instances.

    Examples:
        >>> for gt in species.iter_genotypes():
        ...     print(gt)
    """
    from natal.genetic_entities import Genotype

    sex_chr_groups = self._get_sex_chromosome_groups()
    valid_sex_gts = self._get_valid_sex_genotypes()

    if not sex_chr_groups:
        # No sex chromosomes: simple Cartesian product.
        all_haploid_genotypes = list(self.iter_haploid_genotypes())
        for maternal, paternal in itertools.product(all_haploid_genotypes, repeat=2):
            yield Genotype(species=self, maternal=maternal, paternal=paternal)
    elif valid_sex_gts:
        # Validate pairings against the explicitly valid chromosome pairs.
        maternal_hgs = list(self.iter_maternal_haploid_genotypes())
        paternal_hgs = list(self.iter_paternal_haploid_genotypes())

        # Build a set for fast valid-pair lookup.
        valid_chrom_pairs: Set[Tuple[Chromosome, Chromosome]] = set(valid_sex_gts)

        for maternal, paternal in itertools.product(maternal_hgs, paternal_hgs):
            # Resolve selected maternal and paternal sex chromosomes.
            mat_sex_chrom = self._get_sex_chromosome(maternal, sex_chr_groups)
            pat_sex_chrom = self._get_sex_chromosome(paternal, sex_chr_groups)

            # Keep only valid pairings.
            if (mat_sex_chrom, pat_sex_chrom) in valid_chrom_pairs:
                yield Genotype(species=self, maternal=maternal, paternal=paternal)
    else:
        # With no explicit constraints, all pairings are valid.
        maternal_hgs = list(self.iter_maternal_haploid_genotypes())
        paternal_hgs = list(self.iter_paternal_haploid_genotypes())

        for maternal, paternal in itertools.product(maternal_hgs, paternal_hgs):
            yield Genotype(species=self, maternal=maternal, paternal=paternal)
get_all_haploid_genotypes
get_all_haploid_genotypes() -> List[HaploidGenome]

Get a list of all possible haploid genomes.

Returns:

Type Description
List[HaploidGenome]

List of all HaploidGenome instances.

Source code in src/natal/genetic_structures.py
def get_all_haploid_genotypes(self) -> List[HaploidGenome]:
    """
    Get a list of all possible haploid genomes.

    Returns:
        List of all HaploidGenome instances.
    """
    return list(self.iter_haploid_genotypes())
get_maternal_haploid_genotypes
get_maternal_haploid_genotypes() -> List[HaploidGenome]

Get all maternal-transmissible haploid genomes.

Returns:

Type Description
List[HaploidGenome]

List of maternal haploid genomes.

Source code in src/natal/genetic_structures.py
def get_maternal_haploid_genotypes(self) -> List[HaploidGenome]:
    """Get all maternal-transmissible haploid genomes.

    Returns:
        List of maternal haploid genomes.
    """
    return list(self.iter_maternal_haploid_genotypes())
get_paternal_haploid_genotypes
get_paternal_haploid_genotypes() -> List[HaploidGenome]

Get all paternal-transmissible haploid genomes.

Returns:

Type Description
List[HaploidGenome]

List of paternal haploid genomes.

Source code in src/natal/genetic_structures.py
def get_paternal_haploid_genotypes(self) -> List[HaploidGenome]:
    """Get all paternal-transmissible haploid genomes.

    Returns:
        List of paternal haploid genomes.
    """
    return list(self.iter_paternal_haploid_genotypes())
get_haploid_genotypes
get_haploid_genotypes(parent: Optional[Literal['maternal', 'paternal']] = None) -> List[HaploidGenome]

Get haploid genomes, optionally constrained by parent role.

Parameters:

Name Type Description Default
parent Optional[Literal['maternal', 'paternal']]

Parent role constraint. Accepted values are "maternal" and "paternal". If omitted/None, return all haploid genomes.

None

Returns:

Type Description
List[HaploidGenome]

List of haploid genomes for the requested scope.

Raises:

Type Description
ValueError

If parent is not one of supported values.

Source code in src/natal/genetic_structures.py
def get_haploid_genotypes(
    self,
    parent: Optional[Literal["maternal", "paternal"]] = None,
) -> List[HaploidGenome]:
    """Get haploid genomes, optionally constrained by parent role.

    Args:
        parent: Parent role constraint. Accepted values are ``"maternal"``
            and ``"paternal"``. If omitted/None, return all haploid genomes.

    Returns:
        List of haploid genomes for the requested scope.

    Raises:
        ValueError: If ``parent`` is not one of supported values.
    """
    if parent is None:
        return self.get_all_haploid_genotypes()

    normalized = parent.strip().lower()
    if normalized == "maternal":
        return self.get_maternal_haploid_genotypes()
    if normalized == "paternal":
        return self.get_paternal_haploid_genotypes()
    raise ValueError(
        f"Unknown parent role: {parent!r}. Expected 'maternal', 'paternal', or None."
    )
get_all_genotypes
get_all_genotypes() -> List[Genotype]

Get a list of all possible diploid genotypes.

Returns:

Type Description
List[Genotype]

List of all Genotype instances.

Source code in src/natal/genetic_structures.py
def get_all_genotypes(self) -> List[Genotype]:
    """
    Get a list of all possible diploid genotypes.

    Returns:
        List of all Genotype instances.
    """
    return list(self.iter_genotypes())

ensure_type

ensure_type(obj: Any, expected_type: type) -> None

Ensures that an object is an instance of a given class, with lazy import.

Parameters:

Name Type Description Default
obj any

The object to check

required
expected_type type

The expected class type.

required

Raises:

Type Description
TypeError

If obj is not an instance of the specified class.

Source code in src/natal/genetic_structures.py
def ensure_type(obj: Any, expected_type: type) -> None:
    """
    Ensures that an object is an instance of a given class, with lazy import.

    Args:
        obj (any): The object to check
        expected_type (type): The expected class type.

    Raises:
        TypeError: If obj is not an instance of the specified class.
    """
    module = importlib.import_module(expected_type.__module__) # Lazy import
    cls = getattr(module, expected_type.__name__)
    if not isinstance(obj, cls):
        raise TypeError(
            f"Expected {expected_type.__name__} from {expected_type.__module__}, got {type(obj).__name__} instead."
        )