Skip to content

Bio Module

Core biology simulation classes.

alienbio.bio

Bio module: core biology classes for alienbio.

This module defines the fundamental biology abstractions:

Protocols (for type hints) - from alienbio.protocols.bio: - Atom: protocol for atomic elements - Molecule: protocol for molecule entities - Reaction: protocol for reaction entities - Flow: protocol for transport between compartments - Chemistry: protocol for chemistry containers - CompartmentTree: protocol for compartment topology - WorldState: protocol for multi-compartment concentrations - State: protocol for single-compartment concentrations - Simulator: protocol for simulators

Implementations: - AtomImpl: chemical elements with symbol, name, atomic_weight - MoleculeImpl: composed of atoms with bdepth, name, derived symbol/weight - ReactionImpl: transformations between molecules with rates - Flow hierarchy: - Flow: abstract base class for all flows - MembraneFlow: transport across parent-child membrane with stoichiometry - GeneralFlow: arbitrary state modifications (placeholder, needs interpreter) - ChemistryImpl: container for atoms, molecules, and reactions - CompartmentImpl: biological compartment with flows, concentrations, reactions - CompartmentTreeImpl: hierarchical compartment topology (simulation) - WorldStateImpl: multi-compartment concentration storage (simulation) - StateImpl: single-compartment concentrations - ReferenceSimulatorImpl: basic single-compartment simulator - WorldSimulatorImpl: multi-compartment simulator with flows

Atom

Bases: Protocol

Protocol for atomic elements.

Atoms are the building blocks of molecules. Each atom has: - symbol: 1-2 letter chemical notation (e.g., "C", "H", "Na") - name: Human-readable name (e.g., "Carbon", "Hydrogen") - atomic_weight: Mass in atomic mass units

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class Atom(Protocol):
    """Protocol for atomic elements.

    Atoms are the building blocks of molecules. Each atom has:
    - symbol: 1-2 letter chemical notation (e.g., "C", "H", "Na")
    - name: Human-readable name (e.g., "Carbon", "Hydrogen")
    - atomic_weight: Mass in atomic mass units
    """

    @property
    def symbol(self) -> str:
        """Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'."""
        ...

    @property
    def name(self) -> str:
        """Human-readable name: 'Carbon', 'Hydrogen'."""
        ...

    @property
    def atomic_weight(self) -> float:
        """Atomic mass in atomic mass units."""
        ...

symbol property

Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'.

name property

Human-readable name: 'Carbon', 'Hydrogen'.

atomic_weight property

Atomic mass in atomic mass units.

Molecule

Bases: Protocol

Protocol for molecule entities.

Molecules are composed of atoms and have: - atoms: Composition as {Atom: count} - bdepth: Biosynthetic depth (0 = primitive, higher = more complex) - name: Human-readable name (e.g., "glucose", "water") - symbol: Chemical formula derived from atoms (e.g., "C6H12O6") - molecular_weight: Computed from atom weights

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class Molecule(Protocol):
    """Protocol for molecule entities.

    Molecules are composed of atoms and have:
    - atoms: Composition as {Atom: count}
    - bdepth: Biosynthetic depth (0 = primitive, higher = more complex)
    - name: Human-readable name (e.g., "glucose", "water")
    - symbol: Chemical formula derived from atoms (e.g., "C6H12O6")
    - molecular_weight: Computed from atom weights
    """

    @property
    def local_name(self) -> str:
        """The molecule's local name within its parent entity."""
        ...

    @property
    def atoms(self) -> Dict[Atom, int]:
        """Atom composition: {atom: count}."""
        ...

    @property
    def bdepth(self) -> int:
        """Biosynthetic depth (0 = primitive, 4+ = complex)."""
        ...

    @property
    def name(self) -> str:
        """Human-readable name: 'glucose', 'water'."""
        ...

    @property
    def symbol(self) -> str:
        """Chemical formula derived from atoms: 'C6H12O6', 'H2O'."""
        ...

    @property
    def molecular_weight(self) -> float:
        """Molecular mass computed from atom weights."""
        ...

local_name property

The molecule's local name within its parent entity.

atoms property

Atom composition: {atom: count}.

bdepth property

Biosynthetic depth (0 = primitive, 4+ = complex).

name property

Human-readable name: 'glucose', 'water'.

symbol property

Chemical formula derived from atoms: 'C6H12O6', 'H2O'.

molecular_weight property

Molecular mass computed from atom weights.

Reaction

Bases: Protocol

Protocol for reaction entities.

Reactions define transformations within a single compartment. Each reaction has reactants, products, and a rate.

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class Reaction(Protocol):
    """Protocol for reaction entities.

    Reactions define transformations within a single compartment.
    Each reaction has reactants, products, and a rate.
    """

    @property
    def local_name(self) -> str:
        """The reaction's local name."""
        ...

    @property
    def name(self) -> str:
        """Human-readable name."""
        ...

    @property
    def symbol(self) -> str:
        """Formula string: 'A + B -> C + D'."""
        ...

    @property
    def reactants(self) -> Dict[Molecule, float]:
        """Reactant molecules and their stoichiometric coefficients."""
        ...

    @property
    def products(self) -> Dict[Molecule, float]:
        """Product molecules and their stoichiometric coefficients."""
        ...

    @property
    def rate(self) -> Union[float, Callable]:
        """Reaction rate (constant or function of state)."""
        ...

    def get_rate(self, state: WorldState, compartment: CompartmentId) -> float:
        """Get the effective rate for a given compartment's state."""
        ...

local_name property

The reaction's local name.

name property

Human-readable name.

symbol property

Formula string: 'A + B -> C + D'.

reactants property

Reactant molecules and their stoichiometric coefficients.

products property

Product molecules and their stoichiometric coefficients.

rate property

Reaction rate (constant or function of state).

get_rate(state, compartment)

Get the effective rate for a given compartment's state.

Source code in src/alienbio/protocols/bio.py
def get_rate(self, state: WorldState, compartment: CompartmentId) -> float:
    """Get the effective rate for a given compartment's state."""
    ...

Chemistry

Bases: Protocol

Protocol for chemistry containers.

Chemistry acts as the "world" for a chemical system, holding atoms, molecules, and reactions as public dict attributes.

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class Chemistry(Protocol):
    """Protocol for chemistry containers.

    Chemistry acts as the "world" for a chemical system,
    holding atoms, molecules, and reactions as public dict attributes.
    """

    @property
    def local_name(self) -> str:
        """The chemistry's local name."""
        ...

    @property
    def atoms(self) -> Dict[str, Atom]:
        """All atoms in this chemistry (by symbol)."""
        ...

    @property
    def molecules(self) -> Dict[str, Molecule]:
        """All molecules in this chemistry (by name)."""
        ...

    @property
    def reactions(self) -> Dict[str, Reaction]:
        """All reactions in this chemistry (by name)."""
        ...

    def validate(self) -> List[str]:
        """Validate the chemistry for consistency."""
        ...

local_name property

The chemistry's local name.

atoms property

All atoms in this chemistry (by symbol).

molecules property

All molecules in this chemistry (by name).

reactions property

All reactions in this chemistry (by name).

validate()

Validate the chemistry for consistency.

Source code in src/alienbio/protocols/bio.py
def validate(self) -> List[str]:
    """Validate the chemistry for consistency."""
    ...

CompartmentTree

Bases: Protocol

Protocol for compartment topology.

Represents the hierarchical structure of compartments (organism > organ > cell). Stored separately from concentrations to allow efficient updates.

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class CompartmentTree(Protocol):
    """Protocol for compartment topology.

    Represents the hierarchical structure of compartments (organism > organ > cell).
    Stored separately from concentrations to allow efficient updates.
    """

    @property
    def num_compartments(self) -> int:
        """Total number of compartments."""
        ...

    def parent(self, child: CompartmentId) -> Optional[CompartmentId]:
        """Get parent of a compartment (None for root)."""
        ...

    def children(self, parent: CompartmentId) -> List[CompartmentId]:
        """Get children of a compartment."""
        ...

    def root(self) -> CompartmentId:
        """Get the root compartment."""
        ...

    def is_root(self, compartment: CompartmentId) -> bool:
        """Check if compartment is the root."""
        ...

num_compartments property

Total number of compartments.

parent(child)

Get parent of a compartment (None for root).

Source code in src/alienbio/protocols/bio.py
def parent(self, child: CompartmentId) -> Optional[CompartmentId]:
    """Get parent of a compartment (None for root)."""
    ...

children(parent)

Get children of a compartment.

Source code in src/alienbio/protocols/bio.py
def children(self, parent: CompartmentId) -> List[CompartmentId]:
    """Get children of a compartment."""
    ...

root()

Get the root compartment.

Source code in src/alienbio/protocols/bio.py
def root(self) -> CompartmentId:
    """Get the root compartment."""
    ...

is_root(compartment)

Check if compartment is the root.

Source code in src/alienbio/protocols/bio.py
def is_root(self, compartment: CompartmentId) -> bool:
    """Check if compartment is the root."""
    ...

WorldState

Bases: Protocol

Protocol for world concentration state.

Stores concentrations for all compartments and molecules. Dense storage: [num_compartments x num_molecules] array. Can be extended with sparse overflow for large molecule counts.

Each WorldState holds a reference to its CompartmentTree. Multiple states can share the same tree (immutable sharing). When topology changes (e.g., cell division), a new tree is created and new states point to it while historical states keep their original tree reference.

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class WorldState(Protocol):
    """Protocol for world concentration state.

    Stores concentrations for all compartments and molecules.
    Dense storage: [num_compartments x num_molecules] array.
    Can be extended with sparse overflow for large molecule counts.

    Each WorldState holds a reference to its CompartmentTree. Multiple
    states can share the same tree (immutable sharing). When topology
    changes (e.g., cell division), a new tree is created and new states
    point to it while historical states keep their original tree reference.
    """

    @property
    def tree(self) -> CompartmentTree:
        """The compartment tree this state belongs to."""
        ...

    @property
    def num_compartments(self) -> int:
        """Number of compartments."""
        ...

    @property
    def num_molecules(self) -> int:
        """Number of molecules in vocabulary."""
        ...

    def get(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
        """Get concentration of molecule in compartment."""
        ...

    def set(self, compartment: CompartmentId, molecule: MoleculeId, value: float) -> None:
        """Set concentration of molecule in compartment."""
        ...

    def get_compartment(self, compartment: CompartmentId) -> List[float]:
        """Get all concentrations for a compartment."""
        ...

    # Multiplicity methods

    def get_multiplicity(self, compartment: CompartmentId) -> float:
        """Get multiplicity (instance count) for a compartment."""
        ...

    def set_multiplicity(self, compartment: CompartmentId, value: float) -> None:
        """Set multiplicity (instance count) for a compartment."""
        ...

    def total_molecules(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
        """Get total molecules = multiplicity * concentration."""
        ...

    # Copy and array methods

    def copy(self) -> WorldState:
        """Create a copy of this state (shares tree reference)."""
        ...

    def as_array(self) -> Any:
        """Get concentrations as 2D array [compartments x molecules]."""
        ...

tree property

The compartment tree this state belongs to.

num_compartments property

Number of compartments.

num_molecules property

Number of molecules in vocabulary.

get(compartment, molecule)

Get concentration of molecule in compartment.

Source code in src/alienbio/protocols/bio.py
def get(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
    """Get concentration of molecule in compartment."""
    ...

set(compartment, molecule, value)

Set concentration of molecule in compartment.

Source code in src/alienbio/protocols/bio.py
def set(self, compartment: CompartmentId, molecule: MoleculeId, value: float) -> None:
    """Set concentration of molecule in compartment."""
    ...

get_compartment(compartment)

Get all concentrations for a compartment.

Source code in src/alienbio/protocols/bio.py
def get_compartment(self, compartment: CompartmentId) -> List[float]:
    """Get all concentrations for a compartment."""
    ...

get_multiplicity(compartment)

Get multiplicity (instance count) for a compartment.

Source code in src/alienbio/protocols/bio.py
def get_multiplicity(self, compartment: CompartmentId) -> float:
    """Get multiplicity (instance count) for a compartment."""
    ...

set_multiplicity(compartment, value)

Set multiplicity (instance count) for a compartment.

Source code in src/alienbio/protocols/bio.py
def set_multiplicity(self, compartment: CompartmentId, value: float) -> None:
    """Set multiplicity (instance count) for a compartment."""
    ...

total_molecules(compartment, molecule)

Get total molecules = multiplicity * concentration.

Source code in src/alienbio/protocols/bio.py
def total_molecules(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
    """Get total molecules = multiplicity * concentration."""
    ...

copy()

Create a copy of this state (shares tree reference).

Source code in src/alienbio/protocols/bio.py
def copy(self) -> WorldState:
    """Create a copy of this state (shares tree reference)."""
    ...

as_array()

Get concentrations as 2D array [compartments x molecules].

Source code in src/alienbio/protocols/bio.py
def as_array(self) -> Any:
    """Get concentrations as 2D array [compartments x molecules]."""
    ...

State

Bases: Protocol

Protocol for single-compartment molecule concentration state.

Simple interface for simulations with one compartment. For multi-compartment simulations, use WorldState instead.

Source code in src/alienbio/protocols/bio.py
@runtime_checkable
class State(Protocol):
    """Protocol for single-compartment molecule concentration state.

    Simple interface for simulations with one compartment.
    For multi-compartment simulations, use WorldState instead.
    """

    @property
    def chemistry(self) -> Chemistry:
        """The Chemistry this state belongs to."""
        ...

    def __getitem__(self, key: str) -> float:
        """Get concentration by molecule name."""
        ...

    def __setitem__(self, key: str, value: float) -> None:
        """Set concentration by molecule name."""
        ...

    def __contains__(self, key: str) -> bool:
        """Check if molecule exists in state."""
        ...

    def __iter__(self) -> Iterator[str]:
        """Iterate over molecule names."""
        ...

    def __len__(self) -> int:
        """Number of molecules in state."""
        ...

    def get(self, key: str, default: float = 0.0) -> float:
        """Get concentration with default."""
        ...

    def get_molecule(self, molecule: Molecule) -> float:
        """Get concentration by molecule object."""
        ...

    def set_molecule(self, molecule: Molecule, value: float) -> None:
        """Set concentration by molecule object."""
        ...

    def copy(self) -> State:
        """Create a copy of this state."""
        ...

chemistry property

The Chemistry this state belongs to.

__getitem__(key)

Get concentration by molecule name.

Source code in src/alienbio/protocols/bio.py
def __getitem__(self, key: str) -> float:
    """Get concentration by molecule name."""
    ...

__setitem__(key, value)

Set concentration by molecule name.

Source code in src/alienbio/protocols/bio.py
def __setitem__(self, key: str, value: float) -> None:
    """Set concentration by molecule name."""
    ...

__contains__(key)

Check if molecule exists in state.

Source code in src/alienbio/protocols/bio.py
def __contains__(self, key: str) -> bool:
    """Check if molecule exists in state."""
    ...

__iter__()

Iterate over molecule names.

Source code in src/alienbio/protocols/bio.py
def __iter__(self) -> Iterator[str]:
    """Iterate over molecule names."""
    ...

__len__()

Number of molecules in state.

Source code in src/alienbio/protocols/bio.py
def __len__(self) -> int:
    """Number of molecules in state."""
    ...

get(key, default=0.0)

Get concentration with default.

Source code in src/alienbio/protocols/bio.py
def get(self, key: str, default: float = 0.0) -> float:
    """Get concentration with default."""
    ...

get_molecule(molecule)

Get concentration by molecule object.

Source code in src/alienbio/protocols/bio.py
def get_molecule(self, molecule: Molecule) -> float:
    """Get concentration by molecule object."""
    ...

set_molecule(molecule, value)

Set concentration by molecule object.

Source code in src/alienbio/protocols/bio.py
def set_molecule(self, molecule: Molecule, value: float) -> None:
    """Set concentration by molecule object."""
    ...

copy()

Create a copy of this state.

Source code in src/alienbio/protocols/bio.py
def copy(self) -> State:
    """Create a copy of this state."""
    ...

Simulator

Bases: Protocol

Protocol for simulators.

A Simulator advances the state of a chemical system over time. Applies reactions within compartments and flows across membranes.

Source code in src/alienbio/protocols/bio.py
class Simulator(Protocol):
    """Protocol for simulators.

    A Simulator advances the state of a chemical system over time.
    Applies reactions within compartments and flows across membranes.
    """

    @property
    def chemistry(self) -> Chemistry:
        """The Chemistry being simulated."""
        ...

    @property
    def tree(self) -> CompartmentTree:
        """The compartment topology."""
        ...

    @property
    def dt(self) -> float:
        """Time step size."""
        ...

    @abstractmethod
    def step(self, state: WorldState) -> WorldState:
        """Advance the simulation by one time step."""
        ...

    def run(
        self,
        state: WorldState,
        steps: int,
        sample_every: Optional[int] = None,
    ) -> List[WorldState]:
        """Run simulation for multiple steps, optionally sampling history."""
        ...

chemistry property

The Chemistry being simulated.

tree property

The compartment topology.

dt property

Time step size.

step(state) abstractmethod

Advance the simulation by one time step.

Source code in src/alienbio/protocols/bio.py
@abstractmethod
def step(self, state: WorldState) -> WorldState:
    """Advance the simulation by one time step."""
    ...

run(state, steps, sample_every=None)

Run simulation for multiple steps, optionally sampling history.

Source code in src/alienbio/protocols/bio.py
def run(
    self,
    state: WorldState,
    steps: int,
    sample_every: Optional[int] = None,
) -> List[WorldState]:
    """Run simulation for multiple steps, optionally sampling history."""
    ...

MockDat

Lightweight mock DAT for hydrating entities without a real DAT.

Used when creating entities from YAML specs that don't have backing DAT files. Provides the minimal interface needed by Entity.

Source code in src/alienbio/infra/entity.py
class MockDat:
    """Lightweight mock DAT for hydrating entities without a real DAT.

    Used when creating entities from YAML specs that don't have
    backing DAT files. Provides the minimal interface needed by Entity.
    """

    def __init__(self, path: str):
        self.path = path

    def get_path_name(self) -> str:
        return self.path

    def get_path(self) -> str:
        return f"/mock/{self.path}"

AtomImpl

Implementation: A chemical element.

Atoms are the building blocks of molecules. They are essentially constants representing chemical elements with their properties.

Attributes:

Name Type Description
symbol str

Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'

name str

Human-readable name: 'Carbon', 'Hydrogen'

atomic_weight float

Atomic mass in atomic mass units

Source code in src/alienbio/bio/atom.py
class AtomImpl:
    """Implementation: A chemical element.

    Atoms are the building blocks of molecules. They are essentially constants
    representing chemical elements with their properties.

    Attributes:
        symbol: Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'
        name: Human-readable name: 'Carbon', 'Hydrogen'
        atomic_weight: Atomic mass in atomic mass units
    """

    __slots__ = ("_symbol", "_name", "_atomic_weight")

    def __init__(
        self,
        symbol: str,
        name: str,
        atomic_weight: float,
    ) -> None:
        """Initialize an atom.

        Args:
            symbol: Chemical symbol (1-2 letters)
            name: Human-readable English name
            atomic_weight: Atomic mass in atomic mass units
        """
        if not symbol or len(symbol) > 2:
            raise ValueError(f"Symbol must be 1-2 characters, got {symbol!r}")
        self._symbol = symbol
        self._name = name
        self._atomic_weight = atomic_weight

    @property
    def symbol(self) -> str:
        """Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'."""
        return self._symbol

    @property
    def name(self) -> str:
        """Human-readable name: 'Carbon', 'Hydrogen'."""
        return self._name

    @property
    def atomic_weight(self) -> float:
        """Atomic mass in atomic mass units."""
        return self._atomic_weight

    def __eq__(self, other: object) -> bool:
        """Atoms are equal if they have the same symbol."""
        if not isinstance(other, AtomImpl):
            return NotImplemented
        return self._symbol == other._symbol

    def __hash__(self) -> int:
        """Hash by symbol for use as dict key."""
        return hash(self._symbol)

    def __repr__(self) -> str:
        """Full representation."""
        return f"AtomImpl({self._symbol!r}, {self._name!r}, {self._atomic_weight})"

    def __str__(self) -> str:
        """Short display form."""
        return self._symbol

symbol property

Chemical symbol (1-2 letters): 'C', 'H', 'O', 'Na'.

name property

Human-readable name: 'Carbon', 'Hydrogen'.

atomic_weight property

Atomic mass in atomic mass units.

__init__(symbol, name, atomic_weight)

Initialize an atom.

Parameters:

Name Type Description Default
symbol str

Chemical symbol (1-2 letters)

required
name str

Human-readable English name

required
atomic_weight float

Atomic mass in atomic mass units

required
Source code in src/alienbio/bio/atom.py
def __init__(
    self,
    symbol: str,
    name: str,
    atomic_weight: float,
) -> None:
    """Initialize an atom.

    Args:
        symbol: Chemical symbol (1-2 letters)
        name: Human-readable English name
        atomic_weight: Atomic mass in atomic mass units
    """
    if not symbol or len(symbol) > 2:
        raise ValueError(f"Symbol must be 1-2 characters, got {symbol!r}")
    self._symbol = symbol
    self._name = name
    self._atomic_weight = atomic_weight

__eq__(other)

Atoms are equal if they have the same symbol.

Source code in src/alienbio/bio/atom.py
def __eq__(self, other: object) -> bool:
    """Atoms are equal if they have the same symbol."""
    if not isinstance(other, AtomImpl):
        return NotImplemented
    return self._symbol == other._symbol

__hash__()

Hash by symbol for use as dict key.

Source code in src/alienbio/bio/atom.py
def __hash__(self) -> int:
    """Hash by symbol for use as dict key."""
    return hash(self._symbol)

__repr__()

Full representation.

Source code in src/alienbio/bio/atom.py
def __repr__(self) -> str:
    """Full representation."""
    return f"AtomImpl({self._symbol!r}, {self._name!r}, {self._atomic_weight})"

__str__()

Short display form.

Source code in src/alienbio/bio/atom.py
def __str__(self) -> str:
    """Short display form."""
    return self._symbol

MoleculeImpl

Bases: Entity

Implementation: A molecule in the biological system.

Molecules are composed of atoms and participate in reactions.

Attributes:

Name Type Description
atoms Dict[AtomImpl, int]

Atom composition as {AtomImpl: count}

bdepth int

Biosynthetic depth (0 = primitive, higher = more complex)

name str

Human-readable name (e.g., 'glucose', 'water')

symbol str

Chemical formula derived from atoms (e.g., 'C6H12O6', 'H2O')

molecular_weight float

Computed from atom weights

Source code in src/alienbio/bio/molecule.py
@biotype("molecule")
class MoleculeImpl(Entity, head="Molecule"):
    """Implementation: A molecule in the biological system.

    Molecules are composed of atoms and participate in reactions.

    Attributes:
        atoms: Atom composition as {AtomImpl: count}
        bdepth: Biosynthetic depth (0 = primitive, higher = more complex)
        name: Human-readable name (e.g., 'glucose', 'water')
        symbol: Chemical formula derived from atoms (e.g., 'C6H12O6', 'H2O')
        molecular_weight: Computed from atom weights
    """

    __slots__ = ("_atoms", "_bdepth", "_name")

    def __init__(
        self,
        local_name: str,
        *,
        parent: Optional[Entity] = None,
        dat: Optional[Dat] = None,
        description: str = "",
        atoms: Optional[Dict[AtomImpl, int]] = None,
        bdepth: int = 0,
        name: Optional[str] = None,
    ) -> None:
        """Initialize a molecule.

        Args:
            local_name: Local name within parent (used as entity identifier)
            parent: Link to containing entity
            dat: DAT anchor for root molecules
            description: Human-readable description
            atoms: Atom composition as {AtomImpl: count}
            bdepth: Biosynthetic depth (0 = primitive)
            name: Human-readable name (defaults to local_name)
        """
        super().__init__(local_name, parent=parent, dat=dat, description=description)
        self._atoms: Dict[AtomImpl, int] = atoms.copy() if atoms else {}
        self._bdepth = bdepth
        self._name = name if name is not None else local_name

    @classmethod
    def hydrate(
        cls,
        data: dict[str, Any],
        *,
        dat: Optional[Dat] = None,
        parent: Optional[Entity] = None,
        local_name: Optional[str] = None,
    ) -> Self:
        """Create a Molecule from a dict.

        Args:
            data: Dict with optional keys: name, bdepth, atoms, description
            dat: DAT anchor (if root entity)
            parent: Parent entity (if child)
            local_name: Override name (defaults to data["name"])

        Returns:
            New MoleculeImpl instance
        """
        from ..infra.entity import MockDat

        name = local_name or data.get("name", "molecule")

        # Create mock dat if needed
        if dat is None and parent is None:
            dat = MockDat(f"mol/{name}")

        return cls(
            name,
            parent=parent,
            dat=dat,
            description=data.get("description", ""),
            bdepth=data.get("bdepth", 0),
            # atoms not hydrated here - would need atom registry
        )

    @property
    def atoms(self) -> Dict[AtomImpl, int]:
        """Atom composition: {atom: count}."""
        return self._atoms.copy()

    @property
    def bdepth(self) -> int:
        """Biosynthetic depth (0 = primitive, 4+ = complex)."""
        return self._bdepth

    @property
    def name(self) -> str:
        """Human-readable name: 'glucose', 'water'."""
        return self._name

    @property
    def symbol(self) -> str:
        """Chemical formula derived from atoms: 'C6H12O6', 'H2O'.

        Atoms are ordered by Hill system: C first, then H, then alphabetically.
        """
        if not self._atoms:
            return ""

        # Hill system: C first, then H, then alphabetically
        parts = []
        symbols_counts = [(atom.symbol, count) for atom, count in self._atoms.items()]

        # Sort: C first, H second, rest alphabetically
        def sort_key(item: tuple) -> tuple:
            sym = item[0]
            if sym == "C":
                return (0, sym)
            elif sym == "H":
                return (1, sym)
            else:
                return (2, sym)

        symbols_counts.sort(key=sort_key)

        for sym, count in symbols_counts:
            if count == 1:
                parts.append(sym)
            else:
                parts.append(f"{sym}{count}")

        return "".join(parts)

    @property
    def molecular_weight(self) -> float:
        """Molecular mass computed from atom weights."""
        return sum(
            atom.atomic_weight * count
            for atom, count in self._atoms.items()
        )

    def attributes(self) -> Dict[str, Any]:
        """Semantic content of this molecule."""
        result = super().attributes()
        if self._atoms:
            # Serialize atoms as {symbol: count} for readability
            result["atoms"] = {atom.symbol: count for atom, count in self._atoms.items()}
        if self._bdepth != 0:
            result["bdepth"] = self._bdepth
        if self._name != self._local_name:
            result["display_name"] = self._name
        return result

    def __repr__(self) -> str:
        """Full representation."""
        parts = [f"local_name={self._local_name!r}"]
        if self._name != self._local_name:
            parts.append(f"name={self._name!r}")
        if self._atoms:
            parts.append(f"symbol={self.symbol!r}")
        if self._bdepth != 0:
            parts.append(f"bdepth={self._bdepth}")
        if self.description:
            parts.append(f"description={self.description!r}")
        return f"MoleculeImpl({', '.join(parts)})"

atoms property

Atom composition: {atom: count}.

bdepth property

Biosynthetic depth (0 = primitive, 4+ = complex).

name property

Human-readable name: 'glucose', 'water'.

symbol property

Chemical formula derived from atoms: 'C6H12O6', 'H2O'.

Atoms are ordered by Hill system: C first, then H, then alphabetically.

molecular_weight property

Molecular mass computed from atom weights.

__init__(local_name, *, parent=None, dat=None, description='', atoms=None, bdepth=0, name=None)

Initialize a molecule.

Parameters:

Name Type Description Default
local_name str

Local name within parent (used as entity identifier)

required
parent Optional[Entity]

Link to containing entity

None
dat Optional[Dat]

DAT anchor for root molecules

None
description str

Human-readable description

''
atoms Optional[Dict[AtomImpl, int]]

Atom composition as {AtomImpl: count}

None
bdepth int

Biosynthetic depth (0 = primitive)

0
name Optional[str]

Human-readable name (defaults to local_name)

None
Source code in src/alienbio/bio/molecule.py
def __init__(
    self,
    local_name: str,
    *,
    parent: Optional[Entity] = None,
    dat: Optional[Dat] = None,
    description: str = "",
    atoms: Optional[Dict[AtomImpl, int]] = None,
    bdepth: int = 0,
    name: Optional[str] = None,
) -> None:
    """Initialize a molecule.

    Args:
        local_name: Local name within parent (used as entity identifier)
        parent: Link to containing entity
        dat: DAT anchor for root molecules
        description: Human-readable description
        atoms: Atom composition as {AtomImpl: count}
        bdepth: Biosynthetic depth (0 = primitive)
        name: Human-readable name (defaults to local_name)
    """
    super().__init__(local_name, parent=parent, dat=dat, description=description)
    self._atoms: Dict[AtomImpl, int] = atoms.copy() if atoms else {}
    self._bdepth = bdepth
    self._name = name if name is not None else local_name

hydrate(data, *, dat=None, parent=None, local_name=None) classmethod

Create a Molecule from a dict.

Parameters:

Name Type Description Default
data dict[str, Any]

Dict with optional keys: name, bdepth, atoms, description

required
dat Optional[Dat]

DAT anchor (if root entity)

None
parent Optional[Entity]

Parent entity (if child)

None
local_name Optional[str]

Override name (defaults to data["name"])

None

Returns:

Type Description
Self

New MoleculeImpl instance

Source code in src/alienbio/bio/molecule.py
@classmethod
def hydrate(
    cls,
    data: dict[str, Any],
    *,
    dat: Optional[Dat] = None,
    parent: Optional[Entity] = None,
    local_name: Optional[str] = None,
) -> Self:
    """Create a Molecule from a dict.

    Args:
        data: Dict with optional keys: name, bdepth, atoms, description
        dat: DAT anchor (if root entity)
        parent: Parent entity (if child)
        local_name: Override name (defaults to data["name"])

    Returns:
        New MoleculeImpl instance
    """
    from ..infra.entity import MockDat

    name = local_name or data.get("name", "molecule")

    # Create mock dat if needed
    if dat is None and parent is None:
        dat = MockDat(f"mol/{name}")

    return cls(
        name,
        parent=parent,
        dat=dat,
        description=data.get("description", ""),
        bdepth=data.get("bdepth", 0),
        # atoms not hydrated here - would need atom registry
    )

attributes()

Semantic content of this molecule.

Source code in src/alienbio/bio/molecule.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content of this molecule."""
    result = super().attributes()
    if self._atoms:
        # Serialize atoms as {symbol: count} for readability
        result["atoms"] = {atom.symbol: count for atom, count in self._atoms.items()}
    if self._bdepth != 0:
        result["bdepth"] = self._bdepth
    if self._name != self._local_name:
        result["display_name"] = self._name
    return result

__repr__()

Full representation.

Source code in src/alienbio/bio/molecule.py
def __repr__(self) -> str:
    """Full representation."""
    parts = [f"local_name={self._local_name!r}"]
    if self._name != self._local_name:
        parts.append(f"name={self._name!r}")
    if self._atoms:
        parts.append(f"symbol={self.symbol!r}")
    if self._bdepth != 0:
        parts.append(f"bdepth={self._bdepth}")
    if self.description:
        parts.append(f"description={self.description!r}")
    return f"MoleculeImpl({', '.join(parts)})"

ReactionImpl

Bases: Entity

Implementation: A reaction transforming reactants into products.

Reactions define transformations in the biological system. Each reaction has: - reactants: molecules consumed (with stoichiometric coefficients) - products: molecules produced (with stoichiometric coefficients) - rate: constant or function determining reaction speed

Example

A + 2B -> C with rate 0.1

reaction = ReactionImpl( "r1", reactants={mol_a: 1, mol_b: 2}, products={mol_c: 1}, rate=0.1, parent=chemistry, )

Source code in src/alienbio/bio/reaction.py
class ReactionImpl(Entity, head="Reaction"):
    """Implementation: A reaction transforming reactants into products.

    Reactions define transformations in the biological system.
    Each reaction has:
    - reactants: molecules consumed (with stoichiometric coefficients)
    - products: molecules produced (with stoichiometric coefficients)
    - rate: constant or function determining reaction speed

    Example:
        # A + 2B -> C with rate 0.1
        reaction = ReactionImpl(
            "r1",
            reactants={mol_a: 1, mol_b: 2},
            products={mol_c: 1},
            rate=0.1,
            parent=chemistry,
        )
    """

    __slots__ = ("_reactants", "_products", "_rate")

    def __init__(
        self,
        name: str,
        *,
        reactants: Optional[Dict[Molecule, float]] = None,
        products: Optional[Dict[Molecule, float]] = None,
        rate: RateValue = 1.0,
        parent: Optional[Entity] = None,
        dat: Optional[Dat] = None,
        description: str = "",
    ) -> None:
        """Initialize a reaction.

        Args:
            name: Local name within parent
            reactants: Dict mapping molecules to stoichiometric coefficients
            products: Dict mapping molecules to stoichiometric coefficients
            rate: Reaction rate (constant float or function of State)
            parent: Link to containing entity
            dat: DAT anchor for root reactions
            description: Human-readable description
        """
        super().__init__(name, parent=parent, dat=dat, description=description)
        self._reactants: Dict[Molecule, float] = reactants.copy() if reactants else {}
        self._products: Dict[Molecule, float] = products.copy() if products else {}
        self._rate: RateValue = rate

    @classmethod
    def hydrate(
        cls,
        data: dict[str, Any],
        *,
        molecules: dict[str, "MoleculeImpl"],
        dat: Optional[Dat] = None,
        parent: Optional[Entity] = None,
        local_name: Optional[str] = None,
    ) -> Self:
        """Create a Reaction from a dict.

        Args:
            data: Dict with keys: reactants, products, rate, name, description
            molecules: Dict mapping molecule names to MoleculeImpl instances
            dat: DAT anchor (if root entity)
            parent: Parent entity (if child)
            local_name: Override name (defaults to data key)

        Returns:
            New ReactionImpl instance
        """
        from ..infra.entity import MockDat

        name = local_name or data.get("name", "reaction")

        # Create mock dat if needed
        if dat is None and parent is None:
            dat = MockDat(f"rxn/{name}")

        # Build reactants dict: {MoleculeImpl: coefficient}
        reactants: Dict[Molecule, float] = {}
        for r in data.get("reactants", []):
            if isinstance(r, str):
                # Just a name, coefficient 1
                if r in molecules:
                    reactants[molecules[r]] = 1
            elif isinstance(r, dict):
                # {name: coef} format
                for mol_name, coef in r.items():
                    if mol_name in molecules:
                        reactants[molecules[mol_name]] = coef

        # Build products dict: {MoleculeImpl: coefficient}
        products: Dict[Molecule, float] = {}
        for p in data.get("products", []):
            if isinstance(p, str):
                # Just a name, coefficient 1
                if p in molecules:
                    products[molecules[p]] = 1
            elif isinstance(p, dict):
                # {name: coef} format
                for mol_name, coef in p.items():
                    if mol_name in molecules:
                        products[molecules[mol_name]] = coef

        # Get rate (function or constant)
        rate = data.get("rate", 1.0)

        return cls(
            name,
            reactants=reactants,
            products=products,
            rate=rate,
            parent=parent,
            dat=dat,
            description=data.get("description", ""),
        )

    @property
    def reactants(self) -> Dict[Molecule, float]:
        """Reactant molecules and their stoichiometric coefficients."""
        return self._reactants.copy()

    @property
    def products(self) -> Dict[Molecule, float]:
        """Product molecules and their stoichiometric coefficients."""
        return self._products.copy()

    @property
    def rate(self) -> RateValue:
        """Reaction rate (constant or function)."""
        return self._rate

    @property
    def name(self) -> str:
        """Human-readable name (same as local_name)."""
        return self._local_name

    @property
    def symbol(self) -> str:
        """Formula string: 'glucose + ATP -> G6P + ADP'."""
        reactant_str = " + ".join(
            f"{c}{m.name}" if c != 1 else m.name
            for m, c in self._reactants.items()
        )
        product_str = " + ".join(
            f"{c}{m.name}" if c != 1 else m.name
            for m, c in self._products.items()
        )
        return f"{reactant_str} -> {product_str}"

    def set_rate(self, rate: RateValue) -> None:
        """Set the reaction rate."""
        self._rate = rate

    def get_rate(self, state: State) -> float:
        """Get the effective rate for a given state.

        Args:
            state: Current system state

        Returns:
            Rate value (calls rate function if rate is callable)
        """
        if callable(self._rate):
            return self._rate(state)
        return self._rate

    def add_reactant(self, molecule: Molecule, coefficient: float = 1.0) -> None:
        """Add a reactant to this reaction."""
        self._reactants[molecule] = coefficient

    def add_product(self, molecule: Molecule, coefficient: float = 1.0) -> None:
        """Add a product to this reaction."""
        self._products[molecule] = coefficient

    def attributes(self) -> Dict[str, Any]:
        """Semantic content of this reaction."""
        result = super().attributes()

        # Serialize reactants as {molecule_name: coefficient}
        if self._reactants:
            result["reactants"] = {
                mol.local_name: coef for mol, coef in self._reactants.items()
            }
        if self._products:
            result["products"] = {
                mol.local_name: coef for mol, coef in self._products.items()
            }

        # Only serialize rate if it's a constant
        if not callable(self._rate):
            result["rate"] = self._rate

        return result

    def __repr__(self) -> str:
        """Full representation."""
        reactant_str = " + ".join(
            f"{c}{m.local_name}" if c != 1 else m.local_name
            for m, c in self._reactants.items()
        )
        product_str = " + ".join(
            f"{c}{m.local_name}" if c != 1 else m.local_name
            for m, c in self._products.items()
        )
        rate_str = "<fn>" if callable(self._rate) else str(self._rate)
        return f"ReactionImpl({self._local_name}: {reactant_str} -> {product_str}, rate={rate_str})"

reactants property

Reactant molecules and their stoichiometric coefficients.

products property

Product molecules and their stoichiometric coefficients.

rate property

Reaction rate (constant or function).

name property

Human-readable name (same as local_name).

symbol property

Formula string: 'glucose + ATP -> G6P + ADP'.

__init__(name, *, reactants=None, products=None, rate=1.0, parent=None, dat=None, description='')

Initialize a reaction.

Parameters:

Name Type Description Default
name str

Local name within parent

required
reactants Optional[Dict[Molecule, float]]

Dict mapping molecules to stoichiometric coefficients

None
products Optional[Dict[Molecule, float]]

Dict mapping molecules to stoichiometric coefficients

None
rate RateValue

Reaction rate (constant float or function of State)

1.0
parent Optional[Entity]

Link to containing entity

None
dat Optional[Dat]

DAT anchor for root reactions

None
description str

Human-readable description

''
Source code in src/alienbio/bio/reaction.py
def __init__(
    self,
    name: str,
    *,
    reactants: Optional[Dict[Molecule, float]] = None,
    products: Optional[Dict[Molecule, float]] = None,
    rate: RateValue = 1.0,
    parent: Optional[Entity] = None,
    dat: Optional[Dat] = None,
    description: str = "",
) -> None:
    """Initialize a reaction.

    Args:
        name: Local name within parent
        reactants: Dict mapping molecules to stoichiometric coefficients
        products: Dict mapping molecules to stoichiometric coefficients
        rate: Reaction rate (constant float or function of State)
        parent: Link to containing entity
        dat: DAT anchor for root reactions
        description: Human-readable description
    """
    super().__init__(name, parent=parent, dat=dat, description=description)
    self._reactants: Dict[Molecule, float] = reactants.copy() if reactants else {}
    self._products: Dict[Molecule, float] = products.copy() if products else {}
    self._rate: RateValue = rate

hydrate(data, *, molecules, dat=None, parent=None, local_name=None) classmethod

Create a Reaction from a dict.

Parameters:

Name Type Description Default
data dict[str, Any]

Dict with keys: reactants, products, rate, name, description

required
molecules dict[str, 'MoleculeImpl']

Dict mapping molecule names to MoleculeImpl instances

required
dat Optional[Dat]

DAT anchor (if root entity)

None
parent Optional[Entity]

Parent entity (if child)

None
local_name Optional[str]

Override name (defaults to data key)

None

Returns:

Type Description
Self

New ReactionImpl instance

Source code in src/alienbio/bio/reaction.py
@classmethod
def hydrate(
    cls,
    data: dict[str, Any],
    *,
    molecules: dict[str, "MoleculeImpl"],
    dat: Optional[Dat] = None,
    parent: Optional[Entity] = None,
    local_name: Optional[str] = None,
) -> Self:
    """Create a Reaction from a dict.

    Args:
        data: Dict with keys: reactants, products, rate, name, description
        molecules: Dict mapping molecule names to MoleculeImpl instances
        dat: DAT anchor (if root entity)
        parent: Parent entity (if child)
        local_name: Override name (defaults to data key)

    Returns:
        New ReactionImpl instance
    """
    from ..infra.entity import MockDat

    name = local_name or data.get("name", "reaction")

    # Create mock dat if needed
    if dat is None and parent is None:
        dat = MockDat(f"rxn/{name}")

    # Build reactants dict: {MoleculeImpl: coefficient}
    reactants: Dict[Molecule, float] = {}
    for r in data.get("reactants", []):
        if isinstance(r, str):
            # Just a name, coefficient 1
            if r in molecules:
                reactants[molecules[r]] = 1
        elif isinstance(r, dict):
            # {name: coef} format
            for mol_name, coef in r.items():
                if mol_name in molecules:
                    reactants[molecules[mol_name]] = coef

    # Build products dict: {MoleculeImpl: coefficient}
    products: Dict[Molecule, float] = {}
    for p in data.get("products", []):
        if isinstance(p, str):
            # Just a name, coefficient 1
            if p in molecules:
                products[molecules[p]] = 1
        elif isinstance(p, dict):
            # {name: coef} format
            for mol_name, coef in p.items():
                if mol_name in molecules:
                    products[molecules[mol_name]] = coef

    # Get rate (function or constant)
    rate = data.get("rate", 1.0)

    return cls(
        name,
        reactants=reactants,
        products=products,
        rate=rate,
        parent=parent,
        dat=dat,
        description=data.get("description", ""),
    )

set_rate(rate)

Set the reaction rate.

Source code in src/alienbio/bio/reaction.py
def set_rate(self, rate: RateValue) -> None:
    """Set the reaction rate."""
    self._rate = rate

get_rate(state)

Get the effective rate for a given state.

Parameters:

Name Type Description Default
state State

Current system state

required

Returns:

Type Description
float

Rate value (calls rate function if rate is callable)

Source code in src/alienbio/bio/reaction.py
def get_rate(self, state: State) -> float:
    """Get the effective rate for a given state.

    Args:
        state: Current system state

    Returns:
        Rate value (calls rate function if rate is callable)
    """
    if callable(self._rate):
        return self._rate(state)
    return self._rate

add_reactant(molecule, coefficient=1.0)

Add a reactant to this reaction.

Source code in src/alienbio/bio/reaction.py
def add_reactant(self, molecule: Molecule, coefficient: float = 1.0) -> None:
    """Add a reactant to this reaction."""
    self._reactants[molecule] = coefficient

add_product(molecule, coefficient=1.0)

Add a product to this reaction.

Source code in src/alienbio/bio/reaction.py
def add_product(self, molecule: Molecule, coefficient: float = 1.0) -> None:
    """Add a product to this reaction."""
    self._products[molecule] = coefficient

attributes()

Semantic content of this reaction.

Source code in src/alienbio/bio/reaction.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content of this reaction."""
    result = super().attributes()

    # Serialize reactants as {molecule_name: coefficient}
    if self._reactants:
        result["reactants"] = {
            mol.local_name: coef for mol, coef in self._reactants.items()
        }
    if self._products:
        result["products"] = {
            mol.local_name: coef for mol, coef in self._products.items()
        }

    # Only serialize rate if it's a constant
    if not callable(self._rate):
        result["rate"] = self._rate

    return result

__repr__()

Full representation.

Source code in src/alienbio/bio/reaction.py
def __repr__(self) -> str:
    """Full representation."""
    reactant_str = " + ".join(
        f"{c}{m.local_name}" if c != 1 else m.local_name
        for m, c in self._reactants.items()
    )
    product_str = " + ".join(
        f"{c}{m.local_name}" if c != 1 else m.local_name
        for m, c in self._products.items()
    )
    rate_str = "<fn>" if callable(self._rate) else str(self._rate)
    return f"ReactionImpl({self._local_name}: {reactant_str} -> {product_str}, rate={rate_str})"

Flow

Bases: ABC

Abstract base class for all flows.

Flows move molecules (or instances) between compartments. Each flow is anchored to an origin compartment.

Subclasses: - MembraneFlow: transport across parent-child membrane with stoichiometry - GeneralFlow: arbitrary state modifications (placeholder)

Common interface: - origin: the compartment where this flow is anchored - name: human-readable identifier - compute_flux(): calculate transfer rate - apply(): modify state based on flux

Source code in src/alienbio/bio/flow.py
class Flow(ABC):
    """Abstract base class for all flows.

    Flows move molecules (or instances) between compartments. Each flow is
    anchored to an origin compartment.

    Subclasses:
    - MembraneFlow: transport across parent-child membrane with stoichiometry
    - GeneralFlow: arbitrary state modifications (placeholder)

    Common interface:
    - origin: the compartment where this flow is anchored
    - name: human-readable identifier
    - compute_flux(): calculate transfer rate
    - apply(): modify state based on flux
    """

    __slots__ = ("_origin", "_name")

    def __init__(
        self,
        origin: CompartmentId,
        name: str = "",
    ) -> None:
        """Initialize base flow.

        Args:
            origin: The origin compartment (where this flow is anchored)
            name: Human-readable name for this flow
        """
        self._origin = origin
        self._name = name

    @property
    def origin(self) -> CompartmentId:
        """The origin compartment (where this flow is anchored)."""
        return self._origin

    @property
    def name(self) -> str:
        """Human-readable name."""
        return self._name

    @property
    @abstractmethod
    def is_membrane_flow(self) -> bool:
        """True if this is a membrane flow (origin ↔ parent)."""
        ...

    @property
    @abstractmethod
    def is_general_flow(self) -> bool:
        """True if this is a general flow (arbitrary edits)."""
        ...

    @abstractmethod
    def compute_flux(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
    ) -> float:
        """Compute flux for this flow.

        Args:
            state: Current world state with concentrations
            tree: Compartment topology

        Returns:
            Flux value (positive = into origin for membrane flows)
        """
        ...

    @abstractmethod
    def apply(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
        dt: float = 1.0,
    ) -> None:
        """Apply this flow to the state (mutates in place).

        Args:
            state: World state to modify
            tree: Compartment topology
            dt: Time step
        """
        ...

    @abstractmethod
    def attributes(self) -> Dict[str, Any]:
        """Semantic content for serialization."""
        ...

origin property

The origin compartment (where this flow is anchored).

name property

Human-readable name.

is_membrane_flow abstractmethod property

True if this is a membrane flow (origin ↔ parent).

is_general_flow abstractmethod property

True if this is a general flow (arbitrary edits).

__init__(origin, name='')

Initialize base flow.

Parameters:

Name Type Description Default
origin CompartmentId

The origin compartment (where this flow is anchored)

required
name str

Human-readable name for this flow

''
Source code in src/alienbio/bio/flow.py
def __init__(
    self,
    origin: CompartmentId,
    name: str = "",
) -> None:
    """Initialize base flow.

    Args:
        origin: The origin compartment (where this flow is anchored)
        name: Human-readable name for this flow
    """
    self._origin = origin
    self._name = name

compute_flux(state, tree) abstractmethod

Compute flux for this flow.

Parameters:

Name Type Description Default
state WorldStateImpl

Current world state with concentrations

required
tree CompartmentTreeImpl

Compartment topology

required

Returns:

Type Description
float

Flux value (positive = into origin for membrane flows)

Source code in src/alienbio/bio/flow.py
@abstractmethod
def compute_flux(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
) -> float:
    """Compute flux for this flow.

    Args:
        state: Current world state with concentrations
        tree: Compartment topology

    Returns:
        Flux value (positive = into origin for membrane flows)
    """
    ...

apply(state, tree, dt=1.0) abstractmethod

Apply this flow to the state (mutates in place).

Parameters:

Name Type Description Default
state WorldStateImpl

World state to modify

required
tree CompartmentTreeImpl

Compartment topology

required
dt float

Time step

1.0
Source code in src/alienbio/bio/flow.py
@abstractmethod
def apply(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
    dt: float = 1.0,
) -> None:
    """Apply this flow to the state (mutates in place).

    Args:
        state: World state to modify
        tree: Compartment topology
        dt: Time step
    """
    ...

attributes() abstractmethod

Semantic content for serialization.

Source code in src/alienbio/bio/flow.py
@abstractmethod
def attributes(self) -> Dict[str, Any]:
    """Semantic content for serialization."""
    ...

MembraneFlow

Bases: Flow

Transport across parent-child membrane with stoichiometry.

A MembraneFlow moves molecules across the membrane between a compartment and its parent. Like reactions, it can specify stoichiometry for multiple molecules moving together.

The rate equation determines how many "events" occur per unit time. Each event moves the specified stoichiometry of molecules.

Direction convention: - Positive stoichiometry = molecules move INTO the origin (from parent) - Negative stoichiometry = molecules move OUT OF origin (into parent)

Example

Sodium-glucose cotransporter (SGLT1)

Moves 2 Na+ and 1 glucose into the cell together

sglt1 = MembraneFlow( origin=cell_id, stoichiometry={"sodium": 2, "glucose": 1}, rate_constant=10.0, name="sglt1", )

Sodium-potassium pump (Na+/K+-ATPase)

Pumps 3 Na+ out, 2 K+ in per ATP hydrolyzed

na_k_pump = MembraneFlow( origin=cell_id, stoichiometry={"sodium": -3, "potassium": 2, "atp": -1, "adp": 1}, rate_constant=5.0, name="na_k_atpase", )

Source code in src/alienbio/bio/flow.py
class MembraneFlow(Flow):
    """Transport across parent-child membrane with stoichiometry.

    A MembraneFlow moves molecules across the membrane between a compartment
    and its parent. Like reactions, it can specify stoichiometry for multiple
    molecules moving together.

    The rate equation determines how many "events" occur per unit time.
    Each event moves the specified stoichiometry of molecules.

    Direction convention:
    - Positive stoichiometry = molecules move INTO the origin (from parent)
    - Negative stoichiometry = molecules move OUT OF origin (into parent)

    Example:
        # Sodium-glucose cotransporter (SGLT1)
        # Moves 2 Na+ and 1 glucose into the cell together
        sglt1 = MembraneFlow(
            origin=cell_id,
            stoichiometry={"sodium": 2, "glucose": 1},
            rate_constant=10.0,
            name="sglt1",
        )

        # Sodium-potassium pump (Na+/K+-ATPase)
        # Pumps 3 Na+ out, 2 K+ in per ATP hydrolyzed
        na_k_pump = MembraneFlow(
            origin=cell_id,
            stoichiometry={"sodium": -3, "potassium": 2, "atp": -1, "adp": 1},
            rate_constant=5.0,
            name="na_k_atpase",
        )
    """

    __slots__ = ("_stoichiometry", "_rate_constant", "_rate_fn")

    def __init__(
        self,
        origin: CompartmentId,
        stoichiometry: Dict[str, float],
        rate_constant: float = 1.0,
        rate_fn: Optional[Callable[..., float]] = None,
        name: str = "",
    ) -> None:
        """Initialize a membrane flow.

        Args:
            origin: The compartment whose membrane this flow crosses
            stoichiometry: Molecules and counts moved per event {molecule: count}
                          Positive = into origin, negative = out of origin
            rate_constant: Base rate of events per unit time
            rate_fn: Optional custom rate function
            name: Human-readable name for this flow
        """
        if not name:
            molecules = "_".join(stoichiometry.keys())
            name = f"membrane_{molecules}_at_{origin}"
        super().__init__(origin, name)

        self._stoichiometry = stoichiometry.copy()
        self._rate_constant = rate_constant
        self._rate_fn = rate_fn

    @property
    def stoichiometry(self) -> Dict[str, float]:
        """Molecules and counts moved per event {molecule: count}."""
        return self._stoichiometry.copy()

    @property
    def rate_constant(self) -> float:
        """Base rate of events per unit time."""
        return self._rate_constant

    @property
    def is_membrane_flow(self) -> bool:
        """True - this is a membrane flow."""
        return True

    @property
    def is_general_flow(self) -> bool:
        """False - this is not a general flow."""
        return False

    def compute_flux(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
    ) -> float:
        """Compute the rate of events (not molecules).

        Returns the number of "transport events" per unit time.
        Multiply by stoichiometry to get actual molecule transfer.

        Args:
            state: Current world state with concentrations
            tree: Compartment topology

        Returns:
            Event rate (events per unit time)
        """
        parent = tree.parent(self._origin)
        if parent is None:
            return 0.0

        if self._rate_fn is not None:
            # Custom rate function - pass state and relevant info
            return self._rate_fn(state, self._origin, parent)
        else:
            # Simple constant rate
            return self._rate_constant

    def apply(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
        dt: float = 1.0,
    ) -> None:
        """Apply this flow to the state (mutates in place).

        Computes event rate, then applies stoichiometry to both
        origin and parent compartments.

        Args:
            state: World state to modify
            tree: Compartment topology
            dt: Time step
        """
        parent = tree.parent(self._origin)
        if parent is None:
            return

        event_rate = self.compute_flux(state, tree) * dt

        # Apply stoichiometry
        # Positive stoich = into origin (from parent)
        # Negative stoich = out of origin (into parent)
        for molecule_name, count in self._stoichiometry.items():
            # TODO: Need molecule name -> ID mapping from chemistry
            # For now, this is a placeholder showing the pattern
            # molecules_transferred = event_rate * count
            # origin gains: +molecules_transferred
            # parent loses: -molecules_transferred
            pass

    def attributes(self) -> Dict[str, Any]:
        """Semantic content for serialization."""
        result: Dict[str, Any] = {
            "type": "membrane",
            "name": self._name,
            "origin": self._origin,
            "stoichiometry": self._stoichiometry.copy(),
            "rate_constant": self._rate_constant,
        }
        # Note: rate_fn cannot be serialized
        return result

    def __repr__(self) -> str:
        """Full representation."""
        stoich_str = ", ".join(f"{m}:{c}" for m, c in self._stoichiometry.items())
        return f"MembraneFlow(origin={self._origin}, stoich={{{stoich_str}}}, rate={self._rate_constant})"

    def __str__(self) -> str:
        """Short representation."""
        return f"MembraneFlow({self._name})"

stoichiometry property

Molecules and counts moved per event {molecule: count}.

rate_constant property

Base rate of events per unit time.

is_membrane_flow property

True - this is a membrane flow.

is_general_flow property

False - this is not a general flow.

__init__(origin, stoichiometry, rate_constant=1.0, rate_fn=None, name='')

Initialize a membrane flow.

Parameters:

Name Type Description Default
origin CompartmentId

The compartment whose membrane this flow crosses

required
stoichiometry Dict[str, float]

Molecules and counts moved per event {molecule: count} Positive = into origin, negative = out of origin

required
rate_constant float

Base rate of events per unit time

1.0
rate_fn Optional[Callable[..., float]]

Optional custom rate function

None
name str

Human-readable name for this flow

''
Source code in src/alienbio/bio/flow.py
def __init__(
    self,
    origin: CompartmentId,
    stoichiometry: Dict[str, float],
    rate_constant: float = 1.0,
    rate_fn: Optional[Callable[..., float]] = None,
    name: str = "",
) -> None:
    """Initialize a membrane flow.

    Args:
        origin: The compartment whose membrane this flow crosses
        stoichiometry: Molecules and counts moved per event {molecule: count}
                      Positive = into origin, negative = out of origin
        rate_constant: Base rate of events per unit time
        rate_fn: Optional custom rate function
        name: Human-readable name for this flow
    """
    if not name:
        molecules = "_".join(stoichiometry.keys())
        name = f"membrane_{molecules}_at_{origin}"
    super().__init__(origin, name)

    self._stoichiometry = stoichiometry.copy()
    self._rate_constant = rate_constant
    self._rate_fn = rate_fn

compute_flux(state, tree)

Compute the rate of events (not molecules).

Returns the number of "transport events" per unit time. Multiply by stoichiometry to get actual molecule transfer.

Parameters:

Name Type Description Default
state WorldStateImpl

Current world state with concentrations

required
tree CompartmentTreeImpl

Compartment topology

required

Returns:

Type Description
float

Event rate (events per unit time)

Source code in src/alienbio/bio/flow.py
def compute_flux(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
) -> float:
    """Compute the rate of events (not molecules).

    Returns the number of "transport events" per unit time.
    Multiply by stoichiometry to get actual molecule transfer.

    Args:
        state: Current world state with concentrations
        tree: Compartment topology

    Returns:
        Event rate (events per unit time)
    """
    parent = tree.parent(self._origin)
    if parent is None:
        return 0.0

    if self._rate_fn is not None:
        # Custom rate function - pass state and relevant info
        return self._rate_fn(state, self._origin, parent)
    else:
        # Simple constant rate
        return self._rate_constant

apply(state, tree, dt=1.0)

Apply this flow to the state (mutates in place).

Computes event rate, then applies stoichiometry to both origin and parent compartments.

Parameters:

Name Type Description Default
state WorldStateImpl

World state to modify

required
tree CompartmentTreeImpl

Compartment topology

required
dt float

Time step

1.0
Source code in src/alienbio/bio/flow.py
def apply(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
    dt: float = 1.0,
) -> None:
    """Apply this flow to the state (mutates in place).

    Computes event rate, then applies stoichiometry to both
    origin and parent compartments.

    Args:
        state: World state to modify
        tree: Compartment topology
        dt: Time step
    """
    parent = tree.parent(self._origin)
    if parent is None:
        return

    event_rate = self.compute_flux(state, tree) * dt

    # Apply stoichiometry
    # Positive stoich = into origin (from parent)
    # Negative stoich = out of origin (into parent)
    for molecule_name, count in self._stoichiometry.items():
        # TODO: Need molecule name -> ID mapping from chemistry
        # For now, this is a placeholder showing the pattern
        # molecules_transferred = event_rate * count
        # origin gains: +molecules_transferred
        # parent loses: -molecules_transferred
        pass

attributes()

Semantic content for serialization.

Source code in src/alienbio/bio/flow.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content for serialization."""
    result: Dict[str, Any] = {
        "type": "membrane",
        "name": self._name,
        "origin": self._origin,
        "stoichiometry": self._stoichiometry.copy(),
        "rate_constant": self._rate_constant,
    }
    # Note: rate_fn cannot be serialized
    return result

__repr__()

Full representation.

Source code in src/alienbio/bio/flow.py
def __repr__(self) -> str:
    """Full representation."""
    stoich_str = ", ".join(f"{m}:{c}" for m, c in self._stoichiometry.items())
    return f"MembraneFlow(origin={self._origin}, stoich={{{stoich_str}}}, rate={self._rate_constant})"

__str__()

Short representation.

Source code in src/alienbio/bio/flow.py
def __str__(self) -> str:
    """Short representation."""
    return f"MembraneFlow({self._name})"

GeneralFlow

Bases: Flow

Arbitrary state modifications (placeholder).

GeneralFlow is a catch-all for flows that don't fit the MembraneFlow pattern. This includes: - Lateral flows between siblings - Instance transfers (RBCs moving between compartments) - Any other arbitrary edits to the system

NOTE: This is currently a placeholder. Full implementation will require a more general interpreter to handle arbitrary state modifications specified via Expr or similar.

For now, GeneralFlow stores an apply_fn that takes state and tree and performs arbitrary modifications.

Source code in src/alienbio/bio/flow.py
class GeneralFlow(Flow):
    """Arbitrary state modifications (placeholder).

    GeneralFlow is a catch-all for flows that don't fit the MembraneFlow pattern.
    This includes:
    - Lateral flows between siblings
    - Instance transfers (RBCs moving between compartments)
    - Any other arbitrary edits to the system

    NOTE: This is currently a placeholder. Full implementation will require
    a more general interpreter to handle arbitrary state modifications
    specified via Expr or similar.

    For now, GeneralFlow stores an apply_fn that takes state and tree
    and performs arbitrary modifications.
    """

    __slots__ = ("_apply_fn", "_description")

    def __init__(
        self,
        origin: CompartmentId,
        apply_fn: Optional[Callable[[WorldStateImpl, CompartmentTreeImpl, float], None]] = None,
        name: str = "",
        description: str = "",
    ) -> None:
        """Initialize a general flow.

        Args:
            origin: The compartment where this flow is conceptually anchored
            apply_fn: Function (state, tree, dt) -> None that modifies state
            name: Human-readable name for this flow
            description: Description of what this flow does

        NOTE: This is a placeholder. Full implementation will need a more
        general interpreter to support Expr-based specifications.
        """
        if not name:
            name = f"general_flow_at_{origin}"
        super().__init__(origin, name)

        self._apply_fn = apply_fn
        self._description = description

    @property
    def description(self) -> str:
        """Description of what this flow does."""
        return self._description

    @property
    def is_membrane_flow(self) -> bool:
        """False - this is not a membrane flow."""
        return False

    @property
    def is_general_flow(self) -> bool:
        """True - this is a general flow."""
        return True

    def compute_flux(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
    ) -> float:
        """General flows don't have a simple flux concept.

        Returns 0.0 as placeholder. The actual work happens in apply().
        """
        return 0.0

    def apply(
        self,
        state: WorldStateImpl,
        tree: CompartmentTreeImpl,
        dt: float = 1.0,
    ) -> None:
        """Apply this flow to the state (mutates in place).

        Args:
            state: World state to modify
            tree: Compartment topology
            dt: Time step
        """
        if self._apply_fn is not None:
            self._apply_fn(state, tree, dt)

    def attributes(self) -> Dict[str, Any]:
        """Semantic content for serialization.

        NOTE: apply_fn cannot be serialized. Full implementation will
        need Expr-based specification that can be serialized.
        """
        return {
            "type": "general",
            "name": self._name,
            "origin": self._origin,
            "description": self._description,
        }

    def __repr__(self) -> str:
        """Full representation."""
        return f"GeneralFlow(origin={self._origin}, name={self._name!r})"

    def __str__(self) -> str:
        """Short representation."""
        return f"GeneralFlow({self._name})"

description property

Description of what this flow does.

is_membrane_flow property

False - this is not a membrane flow.

is_general_flow property

True - this is a general flow.

__init__(origin, apply_fn=None, name='', description='')

Initialize a general flow.

Parameters:

Name Type Description Default
origin CompartmentId

The compartment where this flow is conceptually anchored

required
apply_fn Optional[Callable[[WorldStateImpl, CompartmentTreeImpl, float], None]]

Function (state, tree, dt) -> None that modifies state

None
name str

Human-readable name for this flow

''
description str

Description of what this flow does

''

NOTE: This is a placeholder. Full implementation will need a more general interpreter to support Expr-based specifications.

Source code in src/alienbio/bio/flow.py
def __init__(
    self,
    origin: CompartmentId,
    apply_fn: Optional[Callable[[WorldStateImpl, CompartmentTreeImpl, float], None]] = None,
    name: str = "",
    description: str = "",
) -> None:
    """Initialize a general flow.

    Args:
        origin: The compartment where this flow is conceptually anchored
        apply_fn: Function (state, tree, dt) -> None that modifies state
        name: Human-readable name for this flow
        description: Description of what this flow does

    NOTE: This is a placeholder. Full implementation will need a more
    general interpreter to support Expr-based specifications.
    """
    if not name:
        name = f"general_flow_at_{origin}"
    super().__init__(origin, name)

    self._apply_fn = apply_fn
    self._description = description

compute_flux(state, tree)

General flows don't have a simple flux concept.

Returns 0.0 as placeholder. The actual work happens in apply().

Source code in src/alienbio/bio/flow.py
def compute_flux(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
) -> float:
    """General flows don't have a simple flux concept.

    Returns 0.0 as placeholder. The actual work happens in apply().
    """
    return 0.0

apply(state, tree, dt=1.0)

Apply this flow to the state (mutates in place).

Parameters:

Name Type Description Default
state WorldStateImpl

World state to modify

required
tree CompartmentTreeImpl

Compartment topology

required
dt float

Time step

1.0
Source code in src/alienbio/bio/flow.py
def apply(
    self,
    state: WorldStateImpl,
    tree: CompartmentTreeImpl,
    dt: float = 1.0,
) -> None:
    """Apply this flow to the state (mutates in place).

    Args:
        state: World state to modify
        tree: Compartment topology
        dt: Time step
    """
    if self._apply_fn is not None:
        self._apply_fn(state, tree, dt)

attributes()

Semantic content for serialization.

NOTE: apply_fn cannot be serialized. Full implementation will need Expr-based specification that can be serialized.

Source code in src/alienbio/bio/flow.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content for serialization.

    NOTE: apply_fn cannot be serialized. Full implementation will
    need Expr-based specification that can be serialized.
    """
    return {
        "type": "general",
        "name": self._name,
        "origin": self._origin,
        "description": self._description,
    }

__repr__()

Full representation.

Source code in src/alienbio/bio/flow.py
def __repr__(self) -> str:
    """Full representation."""
    return f"GeneralFlow(origin={self._origin}, name={self._name!r})"

__str__()

Short representation.

Source code in src/alienbio/bio/flow.py
def __str__(self) -> str:
    """Short representation."""
    return f"GeneralFlow({self._name})"

ChemistryImpl

Bases: Entity

Implementation: Container for a chemical system.

Chemistry holds atoms, molecules, and reactions as public dict attributes. These are indexed by: - atoms: by symbol ("C", "H", "O") - molecules: by name ("glucose", "atp") - reactions: by name ("glycolysis_step1", "atp_synthesis")

Chemistry is conceptually immutable - built complete via constructor, though the dicts are technically mutable for flexibility.

Example

chem = ChemistryImpl( "glycolysis", atoms={"C": carbon, "H": hydrogen, "O": oxygen}, molecules={"glucose": glucose_mol, "atp": atp_mol}, reactions={"step1": reaction1, "step2": reaction2}, dat=dat, )

Direct access to contents

chem.atoms["C"] # -> carbon atom chem.molecules["glucose"] # -> glucose molecule chem.reactions["step1"] # -> reaction1

Source code in src/alienbio/bio/chemistry.py
class ChemistryImpl(Entity, head="Chemistry"):
    """Implementation: Container for a chemical system.

    Chemistry holds atoms, molecules, and reactions as public dict attributes.
    These are indexed by:
    - atoms: by symbol ("C", "H", "O")
    - molecules: by name ("glucose", "atp")
    - reactions: by name ("glycolysis_step1", "atp_synthesis")

    Chemistry is conceptually immutable - built complete via constructor,
    though the dicts are technically mutable for flexibility.

    Example:
        chem = ChemistryImpl(
            "glycolysis",
            atoms={"C": carbon, "H": hydrogen, "O": oxygen},
            molecules={"glucose": glucose_mol, "atp": atp_mol},
            reactions={"step1": reaction1, "step2": reaction2},
            dat=dat,
        )

        # Direct access to contents
        chem.atoms["C"]  # -> carbon atom
        chem.molecules["glucose"]  # -> glucose molecule
        chem.reactions["step1"]  # -> reaction1
    """

    __slots__ = ("atoms", "molecules", "reactions")

    # Public attributes - direct access, no property wrappers
    atoms: Dict[str, AtomImpl]
    molecules: Dict[str, MoleculeImpl]
    reactions: Dict[str, ReactionImpl]

    def __init__(
        self,
        name: str,
        *,
        atoms: Optional[Dict[str, AtomImpl]] = None,
        molecules: Optional[Dict[str, MoleculeImpl]] = None,
        reactions: Optional[Dict[str, ReactionImpl]] = None,
        parent: Optional[Entity] = None,
        dat: Optional[Dat] = None,
        description: str = "",
    ) -> None:
        """Initialize a chemistry container.

        Args:
            name: Local name within parent
            atoms: Dict of atoms by symbol
            molecules: Dict of molecules by name
            reactions: Dict of reactions by name
            parent: Link to containing entity
            dat: DAT anchor for root chemistry entities
            description: Human-readable description
        """
        super().__init__(name, parent=parent, dat=dat, description=description)
        self.atoms = atoms.copy() if atoms else {}
        self.molecules = molecules.copy() if molecules else {}
        self.reactions = reactions.copy() if reactions else {}

    @classmethod
    def hydrate(
        cls,
        data: dict[str, Any],
        *,
        dat: Optional[Dat] = None,
        parent: Optional[Entity] = None,
        local_name: Optional[str] = None,
    ) -> Self:
        """Create a Chemistry from a dict.

        Recursively hydrates molecules and reactions from nested dicts.

        Args:
            data: Dict with keys: molecules, reactions, atoms, description
                  Each molecule/reaction can be a dict that gets hydrated.
            dat: DAT anchor (if root entity)
            parent: Parent entity (if child)
            local_name: Override name

        Returns:
            New ChemistryImpl with hydrated molecules and reactions
        """
        from ..infra.entity import MockDat

        name = local_name or data.get("name", "chemistry")

        # Create mock dat if needed
        if dat is None and parent is None:
            dat = MockDat(f"chem/{name}")

        # Extract molecules and reactions data
        molecules_data = data.get("molecules", {})
        reactions_data = data.get("reactions", {})

        # First pass: hydrate molecules
        molecules: Dict[str, MoleculeImpl] = {}
        for mol_key, mol_data in molecules_data.items():
            if isinstance(mol_data, dict):
                molecules[mol_key] = MoleculeImpl.hydrate(
                    mol_data,
                    local_name=mol_key,
                )
            else:
                # Simple name, create basic molecule
                molecules[mol_key] = MoleculeImpl.hydrate(
                    {"name": mol_key},
                    local_name=mol_key,
                )

        # Second pass: hydrate reactions (needs molecules)
        reactions: Dict[str, ReactionImpl] = {}
        for rxn_key, rxn_data in reactions_data.items():
            if isinstance(rxn_data, dict):
                reactions[rxn_key] = ReactionImpl.hydrate(
                    rxn_data,
                    molecules=molecules,
                    local_name=rxn_key,
                )

        return cls(
            name,
            molecules=molecules,
            reactions=reactions,
            parent=parent,
            dat=dat,
            description=data.get("description", ""),
        )

    def validate(self) -> list[str]:
        """Validate the chemistry for consistency.

        Checks:
        - All molecule atoms are atoms in this chemistry
        - All reaction reactants/products are molecules in this chemistry

        Returns:
            List of error messages (empty if valid)
        """
        errors: list[str] = []
        atom_set = set(self.atoms.values())
        mol_set = set(self.molecules.values())

        # Check that all molecule atoms exist in chemistry
        for mol_name, molecule in self.molecules.items():
            for atom in molecule.atoms:
                if atom not in atom_set:
                    errors.append(
                        f"Molecule {mol_name}: atom {atom.symbol} not in chemistry"
                    )

        # Check that all reaction molecules exist in chemistry
        for rxn_name, reaction in self.reactions.items():
            for mol in reaction.reactants:
                if mol not in mol_set:
                    errors.append(
                        f"Reaction {rxn_name}: reactant {mol.name} not in chemistry"
                    )
            for mol in reaction.products:
                if mol not in mol_set:
                    errors.append(
                        f"Reaction {rxn_name}: product {mol.name} not in chemistry"
                    )

        return errors

    def attributes(self) -> Dict[str, Any]:
        """Semantic content of this chemistry."""
        result = super().attributes()

        # Serialize atoms as {symbol: {name, atomic_weight}}
        if self.atoms:
            result["atoms"] = {
                sym: {"name": atom.name, "atomic_weight": atom.atomic_weight}
                for sym, atom in self.atoms.items()
            }

        # Serialize molecules by name
        if self.molecules:
            result["molecules"] = {
                name: mol.attributes()
                for name, mol in self.molecules.items()
            }

        # Serialize reactions by name
        if self.reactions:
            result["reactions"] = {
                name: rxn.attributes()
                for name, rxn in self.reactions.items()
            }

        return result

    def __repr__(self) -> str:
        """Full representation."""
        return (
            f"ChemistryImpl({self._local_name!r}, "
            f"atoms={len(self.atoms)}, "
            f"molecules={len(self.molecules)}, "
            f"reactions={len(self.reactions)})"
        )

__init__(name, *, atoms=None, molecules=None, reactions=None, parent=None, dat=None, description='')

Initialize a chemistry container.

Parameters:

Name Type Description Default
name str

Local name within parent

required
atoms Optional[Dict[str, AtomImpl]]

Dict of atoms by symbol

None
molecules Optional[Dict[str, MoleculeImpl]]

Dict of molecules by name

None
reactions Optional[Dict[str, ReactionImpl]]

Dict of reactions by name

None
parent Optional[Entity]

Link to containing entity

None
dat Optional[Dat]

DAT anchor for root chemistry entities

None
description str

Human-readable description

''
Source code in src/alienbio/bio/chemistry.py
def __init__(
    self,
    name: str,
    *,
    atoms: Optional[Dict[str, AtomImpl]] = None,
    molecules: Optional[Dict[str, MoleculeImpl]] = None,
    reactions: Optional[Dict[str, ReactionImpl]] = None,
    parent: Optional[Entity] = None,
    dat: Optional[Dat] = None,
    description: str = "",
) -> None:
    """Initialize a chemistry container.

    Args:
        name: Local name within parent
        atoms: Dict of atoms by symbol
        molecules: Dict of molecules by name
        reactions: Dict of reactions by name
        parent: Link to containing entity
        dat: DAT anchor for root chemistry entities
        description: Human-readable description
    """
    super().__init__(name, parent=parent, dat=dat, description=description)
    self.atoms = atoms.copy() if atoms else {}
    self.molecules = molecules.copy() if molecules else {}
    self.reactions = reactions.copy() if reactions else {}

hydrate(data, *, dat=None, parent=None, local_name=None) classmethod

Create a Chemistry from a dict.

Recursively hydrates molecules and reactions from nested dicts.

Parameters:

Name Type Description Default
data dict[str, Any]

Dict with keys: molecules, reactions, atoms, description Each molecule/reaction can be a dict that gets hydrated.

required
dat Optional[Dat]

DAT anchor (if root entity)

None
parent Optional[Entity]

Parent entity (if child)

None
local_name Optional[str]

Override name

None

Returns:

Type Description
Self

New ChemistryImpl with hydrated molecules and reactions

Source code in src/alienbio/bio/chemistry.py
@classmethod
def hydrate(
    cls,
    data: dict[str, Any],
    *,
    dat: Optional[Dat] = None,
    parent: Optional[Entity] = None,
    local_name: Optional[str] = None,
) -> Self:
    """Create a Chemistry from a dict.

    Recursively hydrates molecules and reactions from nested dicts.

    Args:
        data: Dict with keys: molecules, reactions, atoms, description
              Each molecule/reaction can be a dict that gets hydrated.
        dat: DAT anchor (if root entity)
        parent: Parent entity (if child)
        local_name: Override name

    Returns:
        New ChemistryImpl with hydrated molecules and reactions
    """
    from ..infra.entity import MockDat

    name = local_name or data.get("name", "chemistry")

    # Create mock dat if needed
    if dat is None and parent is None:
        dat = MockDat(f"chem/{name}")

    # Extract molecules and reactions data
    molecules_data = data.get("molecules", {})
    reactions_data = data.get("reactions", {})

    # First pass: hydrate molecules
    molecules: Dict[str, MoleculeImpl] = {}
    for mol_key, mol_data in molecules_data.items():
        if isinstance(mol_data, dict):
            molecules[mol_key] = MoleculeImpl.hydrate(
                mol_data,
                local_name=mol_key,
            )
        else:
            # Simple name, create basic molecule
            molecules[mol_key] = MoleculeImpl.hydrate(
                {"name": mol_key},
                local_name=mol_key,
            )

    # Second pass: hydrate reactions (needs molecules)
    reactions: Dict[str, ReactionImpl] = {}
    for rxn_key, rxn_data in reactions_data.items():
        if isinstance(rxn_data, dict):
            reactions[rxn_key] = ReactionImpl.hydrate(
                rxn_data,
                molecules=molecules,
                local_name=rxn_key,
            )

    return cls(
        name,
        molecules=molecules,
        reactions=reactions,
        parent=parent,
        dat=dat,
        description=data.get("description", ""),
    )

validate()

Validate the chemistry for consistency.

Checks: - All molecule atoms are atoms in this chemistry - All reaction reactants/products are molecules in this chemistry

Returns:

Type Description
list[str]

List of error messages (empty if valid)

Source code in src/alienbio/bio/chemistry.py
def validate(self) -> list[str]:
    """Validate the chemistry for consistency.

    Checks:
    - All molecule atoms are atoms in this chemistry
    - All reaction reactants/products are molecules in this chemistry

    Returns:
        List of error messages (empty if valid)
    """
    errors: list[str] = []
    atom_set = set(self.atoms.values())
    mol_set = set(self.molecules.values())

    # Check that all molecule atoms exist in chemistry
    for mol_name, molecule in self.molecules.items():
        for atom in molecule.atoms:
            if atom not in atom_set:
                errors.append(
                    f"Molecule {mol_name}: atom {atom.symbol} not in chemistry"
                )

    # Check that all reaction molecules exist in chemistry
    for rxn_name, reaction in self.reactions.items():
        for mol in reaction.reactants:
            if mol not in mol_set:
                errors.append(
                    f"Reaction {rxn_name}: reactant {mol.name} not in chemistry"
                )
        for mol in reaction.products:
            if mol not in mol_set:
                errors.append(
                    f"Reaction {rxn_name}: product {mol.name} not in chemistry"
                )

    return errors

attributes()

Semantic content of this chemistry.

Source code in src/alienbio/bio/chemistry.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content of this chemistry."""
    result = super().attributes()

    # Serialize atoms as {symbol: {name, atomic_weight}}
    if self.atoms:
        result["atoms"] = {
            sym: {"name": atom.name, "atomic_weight": atom.atomic_weight}
            for sym, atom in self.atoms.items()
        }

    # Serialize molecules by name
    if self.molecules:
        result["molecules"] = {
            name: mol.attributes()
            for name, mol in self.molecules.items()
        }

    # Serialize reactions by name
    if self.reactions:
        result["reactions"] = {
            name: rxn.attributes()
            for name, rxn in self.reactions.items()
        }

    return result

__repr__()

Full representation.

Source code in src/alienbio/bio/chemistry.py
def __repr__(self) -> str:
    """Full representation."""
    return (
        f"ChemistryImpl({self._local_name!r}, "
        f"atoms={len(self.atoms)}, "
        f"molecules={len(self.molecules)}, "
        f"reactions={len(self.reactions)})"
    )

CompartmentImpl

Bases: Entity

Implementation: A compartment in the biological hierarchy.

Compartments represent biological regions: organisms, organs, cells, organelles. Each compartment can contain child compartments, forming a tree structure.

The compartment entity specifies: - Structure: kind and child compartments - Initial state: multiplicity and concentrations - Behavior: membrane flows and active reactions

This entity tree serves as both the initial WorldState specification and the complete simulation configuration.

Attributes:

Name Type Description
kind str

Type of compartment ("organism", "organ", "cell", "organelle", etc.)

multiplicity float

Number of instances (default 1.0)

volume float

Volume of each instance in arbitrary units (default 1.0)

concentrations Dict[str, float]

Initial molecule concentrations {molecule_name: value}

membrane_flows List[GeneralFlow]

Flows across this compartment's membrane

active_reactions Optional[List[str]]

Reactions active here (None = all from chemistry)

children List[CompartmentImpl]

Child compartments

Example

Define an organism with cells

organism = CompartmentImpl( "body", volume=70000, # 70 liters in mL kind="organism", concentrations={"glucose": 5.0, "oxygen": 2.0}, )

liver = CompartmentImpl( "liver", volume=1500, # 1.5 liters in mL parent=organism, kind="organ", )

hepatocyte = CompartmentImpl( "hepatocyte", volume=3e-9, # ~3000 cubic microns in mL parent=liver, kind="cell", multiplicity=1e9, # 1 billion liver cells concentrations={"glucose": 1.0}, membrane_flows=[glucose_uptake_flow], active_reactions=["glycolysis", "gluconeogenesis"], )

Source code in src/alienbio/bio/compartment.py
class CompartmentImpl(Entity, head="Compartment"):
    """Implementation: A compartment in the biological hierarchy.

    Compartments represent biological regions: organisms, organs, cells, organelles.
    Each compartment can contain child compartments, forming a tree structure.

    The compartment entity specifies:
    - Structure: kind and child compartments
    - Initial state: multiplicity and concentrations
    - Behavior: membrane flows and active reactions

    This entity tree serves as both the initial WorldState specification and
    the complete simulation configuration.

    Attributes:
        kind: Type of compartment ("organism", "organ", "cell", "organelle", etc.)
        multiplicity: Number of instances (default 1.0)
        volume: Volume of each instance in arbitrary units (default 1.0)
        concentrations: Initial molecule concentrations {molecule_name: value}
        membrane_flows: Flows across this compartment's membrane
        active_reactions: Reactions active here (None = all from chemistry)
        children: Child compartments

    Example:
        # Define an organism with cells
        organism = CompartmentImpl(
            "body",
            volume=70000,  # 70 liters in mL
            kind="organism",
            concentrations={"glucose": 5.0, "oxygen": 2.0},
        )

        liver = CompartmentImpl(
            "liver",
            volume=1500,  # 1.5 liters in mL
            parent=organism,
            kind="organ",
        )

        hepatocyte = CompartmentImpl(
            "hepatocyte",
            volume=3e-9,  # ~3000 cubic microns in mL
            parent=liver,
            kind="cell",
            multiplicity=1e9,  # 1 billion liver cells
            concentrations={"glucose": 1.0},
            membrane_flows=[glucose_uptake_flow],
            active_reactions=["glycolysis", "gluconeogenesis"],
        )
    """

    __slots__ = (
        "_kind",
        "_multiplicity",
        "_volume",
        "_concentrations",
        "_membrane_flows",
        "_active_reactions",
        "_children",
    )

    def __init__(
        self,
        local_name: str,
        *,
        volume: float,
        parent: Optional[Entity] = None,
        dat: Optional[Dat] = None,
        description: str = "",
        kind: str = "compartment",
        multiplicity: float = 1.0,
        concentrations: Optional[Dict[str, float]] = None,
        membrane_flows: Optional[List[GeneralFlow]] = None,
        active_reactions: Optional[List[str]] = None,
    ) -> None:
        """Initialize a compartment.

        Args:
            local_name: Local name within parent (used as entity identifier)
            volume: Volume of each instance (required - no default, scale depends on use case)
            parent: Parent compartment (or None for root)
            dat: DAT anchor for root compartments
            description: Human-readable description
            kind: Type of compartment ("organism", "organ", "cell", "organelle")
            multiplicity: Number of instances of this compartment (default 1.0)
            concentrations: Initial molecule concentrations {name: value}
            membrane_flows: Flows across this compartment's membrane
            active_reactions: Reaction names active here (None = all from chemistry)
        """
        super().__init__(local_name, parent=parent, dat=dat, description=description)
        self._kind = kind
        self._multiplicity = multiplicity
        self._volume = volume
        self._concentrations: Dict[str, float] = (
            concentrations.copy() if concentrations else {}
        )
        self._membrane_flows: List[GeneralFlow] = (
            list(membrane_flows) if membrane_flows else []
        )
        self._active_reactions: Optional[List[str]] = (
            list(active_reactions) if active_reactions else None
        )
        self._children: List[CompartmentImpl] = []

        # Register with parent
        if parent is not None and isinstance(parent, CompartmentImpl):
            parent._children.append(self)

    @property
    def kind(self) -> str:
        """Type of compartment: 'organism', 'organ', 'cell', 'organelle'."""
        return self._kind

    @property
    def multiplicity(self) -> float:
        """Number of instances of this compartment."""
        return self._multiplicity

    @property
    def volume(self) -> float:
        """Volume of each instance in arbitrary units."""
        return self._volume

    @property
    def concentrations(self) -> Dict[str, float]:
        """Initial molecule concentrations {name: value}."""
        return self._concentrations.copy()

    @property
    def membrane_flows(self) -> List[GeneralFlow]:
        """Flows across this compartment's membrane."""
        return list(self._membrane_flows)

    @property
    def active_reactions(self) -> Optional[List[str]]:
        """Reaction names active in this compartment (None = all)."""
        return list(self._active_reactions) if self._active_reactions else None

    @property
    def children(self) -> List[CompartmentImpl]:
        """Child compartments."""
        return list(self._children)

    def add_child(self, child: CompartmentImpl) -> None:
        """Add a child compartment."""
        if child not in self._children:
            self._children.append(child)

    def add_flow(self, flow: GeneralFlow) -> None:
        """Add a membrane flow."""
        self._membrane_flows.append(flow)

    def set_concentration(self, molecule: str, value: float) -> None:
        """Set initial concentration for a molecule."""
        self._concentrations[molecule] = value

    def set_multiplicity(self, value: float) -> None:
        """Set the multiplicity (instance count)."""
        self._multiplicity = value

    def set_volume(self, value: float) -> None:
        """Set the volume of each instance."""
        self._volume = value

    def set_active_reactions(self, reactions: Optional[List[str]]) -> None:
        """Set active reactions (None = all from chemistry)."""
        self._active_reactions = list(reactions) if reactions else None

    # ── Tree traversal ────────────────────────────────────────────────────────

    def all_descendants(self) -> List[CompartmentImpl]:
        """Get all descendant compartments (depth-first)."""
        result = []
        stack = list(self._children)
        while stack:
            child = stack.pop()
            result.append(child)
            stack.extend(child._children)
        return result

    def all_compartments(self) -> List[CompartmentImpl]:
        """Get self and all descendants."""
        return [self] + self.all_descendants()

    def depth(self) -> int:
        """Get depth in tree (root = 0)."""
        d = 0
        current = self._parent
        while current is not None and isinstance(current, CompartmentImpl):
            d += 1
            current = current._parent
        return d

    # ── Serialization ─────────────────────────────────────────────────────────

    def attributes(self) -> Dict[str, Any]:
        """Semantic content for serialization."""
        result: Dict[str, Any] = {
            "kind": self._kind,
            "volume": self._volume,  # Always include - required field
        }
        if self._multiplicity != 1.0:
            result["multiplicity"] = self._multiplicity
        if self._concentrations:
            result["concentrations"] = self._concentrations.copy()
        if self._active_reactions is not None:
            result["active_reactions"] = self._active_reactions.copy()
        # Note: membrane_flows and children serialized separately
        return result

    def __repr__(self) -> str:
        """Full representation."""
        return (
            f"CompartmentImpl({self._local_name!r}, kind={self._kind!r}, "
            f"multiplicity={self._multiplicity}, children={len(self._children)})"
        )

    def __str__(self) -> str:
        """Short representation."""
        mult_str = f" x{self._multiplicity:g}" if self._multiplicity != 1.0 else ""
        return f"{self._kind}:{self._local_name}{mult_str}"

kind property

Type of compartment: 'organism', 'organ', 'cell', 'organelle'.

multiplicity property

Number of instances of this compartment.

volume property

Volume of each instance in arbitrary units.

concentrations property

Initial molecule concentrations {name: value}.

membrane_flows property

Flows across this compartment's membrane.

active_reactions property

Reaction names active in this compartment (None = all).

children property

Child compartments.

__init__(local_name, *, volume, parent=None, dat=None, description='', kind='compartment', multiplicity=1.0, concentrations=None, membrane_flows=None, active_reactions=None)

Initialize a compartment.

Parameters:

Name Type Description Default
local_name str

Local name within parent (used as entity identifier)

required
volume float

Volume of each instance (required - no default, scale depends on use case)

required
parent Optional[Entity]

Parent compartment (or None for root)

None
dat Optional[Dat]

DAT anchor for root compartments

None
description str

Human-readable description

''
kind str

Type of compartment ("organism", "organ", "cell", "organelle")

'compartment'
multiplicity float

Number of instances of this compartment (default 1.0)

1.0
concentrations Optional[Dict[str, float]]

Initial molecule concentrations {name: value}

None
membrane_flows Optional[List[GeneralFlow]]

Flows across this compartment's membrane

None
active_reactions Optional[List[str]]

Reaction names active here (None = all from chemistry)

None
Source code in src/alienbio/bio/compartment.py
def __init__(
    self,
    local_name: str,
    *,
    volume: float,
    parent: Optional[Entity] = None,
    dat: Optional[Dat] = None,
    description: str = "",
    kind: str = "compartment",
    multiplicity: float = 1.0,
    concentrations: Optional[Dict[str, float]] = None,
    membrane_flows: Optional[List[GeneralFlow]] = None,
    active_reactions: Optional[List[str]] = None,
) -> None:
    """Initialize a compartment.

    Args:
        local_name: Local name within parent (used as entity identifier)
        volume: Volume of each instance (required - no default, scale depends on use case)
        parent: Parent compartment (or None for root)
        dat: DAT anchor for root compartments
        description: Human-readable description
        kind: Type of compartment ("organism", "organ", "cell", "organelle")
        multiplicity: Number of instances of this compartment (default 1.0)
        concentrations: Initial molecule concentrations {name: value}
        membrane_flows: Flows across this compartment's membrane
        active_reactions: Reaction names active here (None = all from chemistry)
    """
    super().__init__(local_name, parent=parent, dat=dat, description=description)
    self._kind = kind
    self._multiplicity = multiplicity
    self._volume = volume
    self._concentrations: Dict[str, float] = (
        concentrations.copy() if concentrations else {}
    )
    self._membrane_flows: List[GeneralFlow] = (
        list(membrane_flows) if membrane_flows else []
    )
    self._active_reactions: Optional[List[str]] = (
        list(active_reactions) if active_reactions else None
    )
    self._children: List[CompartmentImpl] = []

    # Register with parent
    if parent is not None and isinstance(parent, CompartmentImpl):
        parent._children.append(self)

add_child(child)

Add a child compartment.

Source code in src/alienbio/bio/compartment.py
def add_child(self, child: CompartmentImpl) -> None:
    """Add a child compartment."""
    if child not in self._children:
        self._children.append(child)

add_flow(flow)

Add a membrane flow.

Source code in src/alienbio/bio/compartment.py
def add_flow(self, flow: GeneralFlow) -> None:
    """Add a membrane flow."""
    self._membrane_flows.append(flow)

set_concentration(molecule, value)

Set initial concentration for a molecule.

Source code in src/alienbio/bio/compartment.py
def set_concentration(self, molecule: str, value: float) -> None:
    """Set initial concentration for a molecule."""
    self._concentrations[molecule] = value

set_multiplicity(value)

Set the multiplicity (instance count).

Source code in src/alienbio/bio/compartment.py
def set_multiplicity(self, value: float) -> None:
    """Set the multiplicity (instance count)."""
    self._multiplicity = value

set_volume(value)

Set the volume of each instance.

Source code in src/alienbio/bio/compartment.py
def set_volume(self, value: float) -> None:
    """Set the volume of each instance."""
    self._volume = value

set_active_reactions(reactions)

Set active reactions (None = all from chemistry).

Source code in src/alienbio/bio/compartment.py
def set_active_reactions(self, reactions: Optional[List[str]]) -> None:
    """Set active reactions (None = all from chemistry)."""
    self._active_reactions = list(reactions) if reactions else None

all_descendants()

Get all descendant compartments (depth-first).

Source code in src/alienbio/bio/compartment.py
def all_descendants(self) -> List[CompartmentImpl]:
    """Get all descendant compartments (depth-first)."""
    result = []
    stack = list(self._children)
    while stack:
        child = stack.pop()
        result.append(child)
        stack.extend(child._children)
    return result

all_compartments()

Get self and all descendants.

Source code in src/alienbio/bio/compartment.py
def all_compartments(self) -> List[CompartmentImpl]:
    """Get self and all descendants."""
    return [self] + self.all_descendants()

depth()

Get depth in tree (root = 0).

Source code in src/alienbio/bio/compartment.py
def depth(self) -> int:
    """Get depth in tree (root = 0)."""
    d = 0
    current = self._parent
    while current is not None and isinstance(current, CompartmentImpl):
        d += 1
        current = current._parent
    return d

attributes()

Semantic content for serialization.

Source code in src/alienbio/bio/compartment.py
def attributes(self) -> Dict[str, Any]:
    """Semantic content for serialization."""
    result: Dict[str, Any] = {
        "kind": self._kind,
        "volume": self._volume,  # Always include - required field
    }
    if self._multiplicity != 1.0:
        result["multiplicity"] = self._multiplicity
    if self._concentrations:
        result["concentrations"] = self._concentrations.copy()
    if self._active_reactions is not None:
        result["active_reactions"] = self._active_reactions.copy()
    # Note: membrane_flows and children serialized separately
    return result

__repr__()

Full representation.

Source code in src/alienbio/bio/compartment.py
def __repr__(self) -> str:
    """Full representation."""
    return (
        f"CompartmentImpl({self._local_name!r}, kind={self._kind!r}, "
        f"multiplicity={self._multiplicity}, children={len(self._children)})"
    )

__str__()

Short representation.

Source code in src/alienbio/bio/compartment.py
def __str__(self) -> str:
    """Short representation."""
    mult_str = f" x{self._multiplicity:g}" if self._multiplicity != 1.0 else ""
    return f"{self._kind}:{self._local_name}{mult_str}"

CompartmentTreeImpl

Implementation: Hierarchical structure of compartments.

Represents the tree topology of compartments (organism > organ > cell > organelle). Stored separately from concentrations to allow efficient structure updates.

The tree is represented with: - parents: List[Optional[CompartmentId]] - parent[child] = parent_id or None for root - children: Dict[CompartmentId, List[CompartmentId]] - children by parent

Compartments are identified by integer IDs (0, 1, 2, ...).

Example

Create tree: organism with two organs

tree = CompartmentTreeImpl() organism = tree.add_root("organism") # 0 organ_a = tree.add_child(organism, "organ_a") # 1 organ_b = tree.add_child(organism, "organ_b") # 2 cell_1 = tree.add_child(organ_a, "cell_1") # 3

print(tree.parent(cell_1)) # 1 (organ_a) print(tree.children(organism)) # [1, 2]

Source code in src/alienbio/bio/compartment_tree.py
class CompartmentTreeImpl:
    """Implementation: Hierarchical structure of compartments.

    Represents the tree topology of compartments (organism > organ > cell > organelle).
    Stored separately from concentrations to allow efficient structure updates.

    The tree is represented with:
    - parents: List[Optional[CompartmentId]] - parent[child] = parent_id or None for root
    - children: Dict[CompartmentId, List[CompartmentId]] - children by parent

    Compartments are identified by integer IDs (0, 1, 2, ...).

    Example:
        # Create tree: organism with two organs
        tree = CompartmentTreeImpl()
        organism = tree.add_root("organism")      # 0
        organ_a = tree.add_child(organism, "organ_a")  # 1
        organ_b = tree.add_child(organism, "organ_b")  # 2
        cell_1 = tree.add_child(organ_a, "cell_1")     # 3

        print(tree.parent(cell_1))    # 1 (organ_a)
        print(tree.children(organism))  # [1, 2]
    """

    __slots__ = ("_parents", "_children", "_names", "_root")

    def __init__(self) -> None:
        """Initialize empty compartment tree."""
        self._parents: List[Optional[CompartmentId]] = []
        self._children: Dict[CompartmentId, List[CompartmentId]] = {}
        self._names: List[str] = []
        self._root: Optional[CompartmentId] = None

    @property
    def num_compartments(self) -> int:
        """Total number of compartments."""
        return len(self._parents)

    def parent(self, child: CompartmentId) -> Optional[CompartmentId]:
        """Get parent of a compartment (None for root)."""
        return self._parents[child]

    def children(self, parent: CompartmentId) -> List[CompartmentId]:
        """Get children of a compartment."""
        return self._children.get(parent, [])

    def root(self) -> CompartmentId:
        """Get the root compartment."""
        if self._root is None:
            raise ValueError("Tree has no root")
        return self._root

    def is_root(self, compartment: CompartmentId) -> bool:
        """Check if compartment is the root."""
        return self._parents[compartment] is None

    def name(self, compartment: CompartmentId) -> str:
        """Get the name of a compartment."""
        return self._names[compartment]

    def add_root(self, name: str = "root") -> CompartmentId:
        """Add the root compartment.

        Args:
            name: Human-readable name for the root

        Returns:
            The root compartment ID (always 0)

        Raises:
            ValueError: If root already exists
        """
        if self._root is not None:
            raise ValueError("Root already exists")

        compartment_id = len(self._parents)
        self._parents.append(None)
        self._children[compartment_id] = []
        self._names.append(name)
        self._root = compartment_id
        return compartment_id

    def add_child(
        self, parent: CompartmentId, name: str = ""
    ) -> CompartmentId:
        """Add a child compartment.

        Args:
            parent: Parent compartment ID
            name: Human-readable name for the child

        Returns:
            The new compartment ID
        """
        if parent >= len(self._parents):
            raise ValueError(f"Parent {parent} does not exist")

        compartment_id = len(self._parents)
        if not name:
            name = f"compartment_{compartment_id}"

        self._parents.append(parent)
        self._children[compartment_id] = []
        self._children[parent].append(compartment_id)
        self._names.append(name)
        return compartment_id

    def ancestors(self, compartment: CompartmentId) -> List[CompartmentId]:
        """Get all ancestors from compartment to root (inclusive)."""
        result = []
        current: Optional[CompartmentId] = compartment
        while current is not None:
            result.append(current)
            current = self._parents[current]
        return result

    def descendants(self, compartment: CompartmentId) -> List[CompartmentId]:
        """Get all descendants of a compartment (not including self)."""
        result = []
        stack = list(self._children.get(compartment, []))
        while stack:
            child = stack.pop()
            result.append(child)
            stack.extend(self._children.get(child, []))
        return result

    def depth(self, compartment: CompartmentId) -> int:
        """Get depth of compartment (root = 0)."""
        d = 0
        current = self._parents[compartment]
        while current is not None:
            d += 1
            current = self._parents[current]
        return d

    def to_dict(self) -> Dict:
        """Serialize tree structure."""
        return {
            "parents": self._parents.copy(),
            "names": self._names.copy(),
        }

    @classmethod
    def from_dict(cls, data: Dict) -> CompartmentTreeImpl:
        """Deserialize tree structure."""
        tree = cls()
        tree._parents = data["parents"]
        tree._names = data["names"]

        # Rebuild children dict and find root
        for child, parent in enumerate(tree._parents):
            tree._children[child] = []
            if parent is None:
                tree._root = child

        for child, parent in enumerate(tree._parents):
            if parent is not None:
                tree._children[parent].append(child)

        return tree

    def __repr__(self) -> str:
        """Full representation."""
        return f"CompartmentTreeImpl(compartments={self.num_compartments})"

    def __str__(self) -> str:
        """Tree visualization."""
        if self._root is None:
            return "CompartmentTree(empty)"

        lines = []

        def _format(comp: CompartmentId, indent: str = "") -> None:
            lines.append(f"{indent}{self._names[comp]} ({comp})")
            children = self._children.get(comp, [])
            for i, child in enumerate(children):
                is_last = i == len(children) - 1
                prefix = "└── " if is_last else "├── "
                next_indent = indent + ("    " if is_last else "│   ")
                lines.append(f"{indent}{prefix}{self._names[child]} ({child})")
                for grandchild in self._children.get(child, []):
                    _format(grandchild, next_indent)

        _format(self._root)
        return "\n".join(lines)

num_compartments property

Total number of compartments.

__init__()

Initialize empty compartment tree.

Source code in src/alienbio/bio/compartment_tree.py
def __init__(self) -> None:
    """Initialize empty compartment tree."""
    self._parents: List[Optional[CompartmentId]] = []
    self._children: Dict[CompartmentId, List[CompartmentId]] = {}
    self._names: List[str] = []
    self._root: Optional[CompartmentId] = None

parent(child)

Get parent of a compartment (None for root).

Source code in src/alienbio/bio/compartment_tree.py
def parent(self, child: CompartmentId) -> Optional[CompartmentId]:
    """Get parent of a compartment (None for root)."""
    return self._parents[child]

children(parent)

Get children of a compartment.

Source code in src/alienbio/bio/compartment_tree.py
def children(self, parent: CompartmentId) -> List[CompartmentId]:
    """Get children of a compartment."""
    return self._children.get(parent, [])

root()

Get the root compartment.

Source code in src/alienbio/bio/compartment_tree.py
def root(self) -> CompartmentId:
    """Get the root compartment."""
    if self._root is None:
        raise ValueError("Tree has no root")
    return self._root

is_root(compartment)

Check if compartment is the root.

Source code in src/alienbio/bio/compartment_tree.py
def is_root(self, compartment: CompartmentId) -> bool:
    """Check if compartment is the root."""
    return self._parents[compartment] is None

name(compartment)

Get the name of a compartment.

Source code in src/alienbio/bio/compartment_tree.py
def name(self, compartment: CompartmentId) -> str:
    """Get the name of a compartment."""
    return self._names[compartment]

add_root(name='root')

Add the root compartment.

Parameters:

Name Type Description Default
name str

Human-readable name for the root

'root'

Returns:

Type Description
CompartmentId

The root compartment ID (always 0)

Raises:

Type Description
ValueError

If root already exists

Source code in src/alienbio/bio/compartment_tree.py
def add_root(self, name: str = "root") -> CompartmentId:
    """Add the root compartment.

    Args:
        name: Human-readable name for the root

    Returns:
        The root compartment ID (always 0)

    Raises:
        ValueError: If root already exists
    """
    if self._root is not None:
        raise ValueError("Root already exists")

    compartment_id = len(self._parents)
    self._parents.append(None)
    self._children[compartment_id] = []
    self._names.append(name)
    self._root = compartment_id
    return compartment_id

add_child(parent, name='')

Add a child compartment.

Parameters:

Name Type Description Default
parent CompartmentId

Parent compartment ID

required
name str

Human-readable name for the child

''

Returns:

Type Description
CompartmentId

The new compartment ID

Source code in src/alienbio/bio/compartment_tree.py
def add_child(
    self, parent: CompartmentId, name: str = ""
) -> CompartmentId:
    """Add a child compartment.

    Args:
        parent: Parent compartment ID
        name: Human-readable name for the child

    Returns:
        The new compartment ID
    """
    if parent >= len(self._parents):
        raise ValueError(f"Parent {parent} does not exist")

    compartment_id = len(self._parents)
    if not name:
        name = f"compartment_{compartment_id}"

    self._parents.append(parent)
    self._children[compartment_id] = []
    self._children[parent].append(compartment_id)
    self._names.append(name)
    return compartment_id

ancestors(compartment)

Get all ancestors from compartment to root (inclusive).

Source code in src/alienbio/bio/compartment_tree.py
def ancestors(self, compartment: CompartmentId) -> List[CompartmentId]:
    """Get all ancestors from compartment to root (inclusive)."""
    result = []
    current: Optional[CompartmentId] = compartment
    while current is not None:
        result.append(current)
        current = self._parents[current]
    return result

descendants(compartment)

Get all descendants of a compartment (not including self).

Source code in src/alienbio/bio/compartment_tree.py
def descendants(self, compartment: CompartmentId) -> List[CompartmentId]:
    """Get all descendants of a compartment (not including self)."""
    result = []
    stack = list(self._children.get(compartment, []))
    while stack:
        child = stack.pop()
        result.append(child)
        stack.extend(self._children.get(child, []))
    return result

depth(compartment)

Get depth of compartment (root = 0).

Source code in src/alienbio/bio/compartment_tree.py
def depth(self, compartment: CompartmentId) -> int:
    """Get depth of compartment (root = 0)."""
    d = 0
    current = self._parents[compartment]
    while current is not None:
        d += 1
        current = self._parents[current]
    return d

to_dict()

Serialize tree structure.

Source code in src/alienbio/bio/compartment_tree.py
def to_dict(self) -> Dict:
    """Serialize tree structure."""
    return {
        "parents": self._parents.copy(),
        "names": self._names.copy(),
    }

from_dict(data) classmethod

Deserialize tree structure.

Source code in src/alienbio/bio/compartment_tree.py
@classmethod
def from_dict(cls, data: Dict) -> CompartmentTreeImpl:
    """Deserialize tree structure."""
    tree = cls()
    tree._parents = data["parents"]
    tree._names = data["names"]

    # Rebuild children dict and find root
    for child, parent in enumerate(tree._parents):
        tree._children[child] = []
        if parent is None:
            tree._root = child

    for child, parent in enumerate(tree._parents):
        if parent is not None:
            tree._children[parent].append(child)

    return tree

__repr__()

Full representation.

Source code in src/alienbio/bio/compartment_tree.py
def __repr__(self) -> str:
    """Full representation."""
    return f"CompartmentTreeImpl(compartments={self.num_compartments})"

__str__()

Tree visualization.

Source code in src/alienbio/bio/compartment_tree.py
def __str__(self) -> str:
    """Tree visualization."""
    if self._root is None:
        return "CompartmentTree(empty)"

    lines = []

    def _format(comp: CompartmentId, indent: str = "") -> None:
        lines.append(f"{indent}{self._names[comp]} ({comp})")
        children = self._children.get(comp, [])
        for i, child in enumerate(children):
            is_last = i == len(children) - 1
            prefix = "└── " if is_last else "├── "
            next_indent = indent + ("    " if is_last else "│   ")
            lines.append(f"{indent}{prefix}{self._names[child]} ({child})")
            for grandchild in self._children.get(child, []):
                _format(grandchild, next_indent)

    _format(self._root)
    return "\n".join(lines)

WorldStateImpl

Implementation: Dense concentration storage for all compartments.

Stores concentrations as a flat array indexed by [compartment, molecule]. Also stores multiplicity (instance count) per compartment. Dense storage is efficient for small to medium molecule counts.

Each WorldState holds a reference to its CompartmentTree. Multiple states share the same tree reference (immutable sharing) until topology changes. When topology changes (e.g., cell division), a new tree is created.

Attributes:

Name Type Description
tree CompartmentTreeImpl

The CompartmentTree this state belongs to (shared reference)

num_compartments int

Number of compartments (derived from tree)

num_molecules int

Number of molecules in vocabulary

concentrations int

Flat array [num_compartments * num_molecules]

multiplicities int

Array [num_compartments] - instance count per compartment

The concentration array is row-major: concentrations[comp * num_molecules + mol]

Multiplicity represents how many instances of this compartment exist. For example, "arterial red blood cells" might have multiplicity 1e6. Concentrations are per-instance; total molecules = multiplicity * concentration.

Example

tree = CompartmentTreeImpl() root = tree.add_root("organism") cell = tree.add_child(root, "cell") state = WorldStateImpl(tree=tree, num_molecules=50)

Set concentrations

state.set(compartment=cell, molecule=5, value=1.0) print(state.get(cell, 5)) # 1.0

Set multiplicity (number of cells)

state.set_multiplicity(cell, 1000.0) print(state.get_multiplicity(cell)) # 1000.0

Source code in src/alienbio/bio/world_state.py
class WorldStateImpl:
    """Implementation: Dense concentration storage for all compartments.

    Stores concentrations as a flat array indexed by [compartment, molecule].
    Also stores multiplicity (instance count) per compartment.
    Dense storage is efficient for small to medium molecule counts.

    Each WorldState holds a reference to its CompartmentTree. Multiple states
    share the same tree reference (immutable sharing) until topology changes.
    When topology changes (e.g., cell division), a new tree is created.

    Attributes:
        tree: The CompartmentTree this state belongs to (shared reference)
        num_compartments: Number of compartments (derived from tree)
        num_molecules: Number of molecules in vocabulary
        concentrations: Flat array [num_compartments * num_molecules]
        multiplicities: Array [num_compartments] - instance count per compartment

    The concentration array is row-major: concentrations[comp * num_molecules + mol]

    Multiplicity represents how many instances of this compartment exist.
    For example, "arterial red blood cells" might have multiplicity 1e6.
    Concentrations are per-instance; total molecules = multiplicity * concentration.

    Example:
        tree = CompartmentTreeImpl()
        root = tree.add_root("organism")
        cell = tree.add_child(root, "cell")
        state = WorldStateImpl(tree=tree, num_molecules=50)

        # Set concentrations
        state.set(compartment=cell, molecule=5, value=1.0)
        print(state.get(cell, 5))  # 1.0

        # Set multiplicity (number of cells)
        state.set_multiplicity(cell, 1000.0)
        print(state.get_multiplicity(cell))  # 1000.0
    """

    __slots__ = ("_tree", "_num_molecules", "_concentrations", "_multiplicities")

    def __init__(
        self,
        tree: CompartmentTreeImpl,
        num_molecules: int,
        initial_concentrations: Optional[List[float]] = None,
        initial_multiplicities: Optional[List[float]] = None,
    ) -> None:
        """Initialize world state.

        Args:
            tree: CompartmentTree defining the topology (shared reference)
            num_molecules: Number of molecules in vocabulary
            initial_concentrations: Optional flat array of initial concentrations
            initial_multiplicities: Optional array of initial multiplicities per compartment
        """
        self._tree = tree
        self._num_molecules = num_molecules

        num_compartments = tree.num_compartments
        size = num_compartments * num_molecules

        # Initialize concentrations
        if initial_concentrations is not None:
            if len(initial_concentrations) != size:
                raise ValueError(
                    f"Initial concentrations size {len(initial_concentrations)} != "
                    f"{num_compartments} * {num_molecules} = {size}"
                )
            self._concentrations = list(initial_concentrations)
        else:
            self._concentrations = [0.0] * size

        # Initialize multiplicities (default 1.0 for each compartment)
        if initial_multiplicities is not None:
            if len(initial_multiplicities) != num_compartments:
                raise ValueError(
                    f"Initial multiplicities size {len(initial_multiplicities)} != "
                    f"num_compartments {num_compartments}"
                )
            self._multiplicities = list(initial_multiplicities)
        else:
            self._multiplicities = [1.0] * num_compartments

    @property
    def tree(self) -> CompartmentTreeImpl:
        """The compartment tree this state belongs to (shared reference)."""
        return self._tree

    @property
    def num_compartments(self) -> int:
        """Number of compartments (from tree)."""
        return self._tree.num_compartments

    @property
    def num_molecules(self) -> int:
        """Number of molecules in vocabulary."""
        return self._num_molecules

    def _index(self, compartment: CompartmentId, molecule: MoleculeId) -> int:
        """Compute flat array index."""
        return compartment * self._num_molecules + molecule

    def get(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
        """Get concentration of molecule in compartment."""
        return self._concentrations[self._index(compartment, molecule)]

    def set(
        self, compartment: CompartmentId, molecule: MoleculeId, value: float
    ) -> None:
        """Set concentration of molecule in compartment."""
        self._concentrations[self._index(compartment, molecule)] = value

    def get_compartment(self, compartment: CompartmentId) -> List[float]:
        """Get all concentrations for a compartment."""
        start = compartment * self._num_molecules
        end = start + self._num_molecules
        return self._concentrations[start:end]

    def set_compartment(
        self, compartment: CompartmentId, values: List[float]
    ) -> None:
        """Set all concentrations for a compartment."""
        if len(values) != self._num_molecules:
            raise ValueError(
                f"Values length {len(values)} != num_molecules {self._num_molecules}"
            )
        start = compartment * self._num_molecules
        for i, v in enumerate(values):
            self._concentrations[start + i] = v

    # ── Multiplicity methods ──────────────────────────────────────────────────

    def get_multiplicity(self, compartment: CompartmentId) -> float:
        """Get multiplicity (instance count) for a compartment."""
        return self._multiplicities[compartment]

    def set_multiplicity(self, compartment: CompartmentId, value: float) -> None:
        """Set multiplicity (instance count) for a compartment."""
        self._multiplicities[compartment] = value

    def get_all_multiplicities(self) -> List[float]:
        """Get multiplicities for all compartments."""
        return self._multiplicities.copy()

    def total_molecules(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
        """Get total molecules = multiplicity * concentration."""
        return self._multiplicities[compartment] * self.get(compartment, molecule)

    # ── Copy and array methods ────────────────────────────────────────────────

    def copy(self) -> WorldStateImpl:
        """Create a copy of this state (shares tree reference)."""
        return WorldStateImpl(
            self._tree,  # Shared reference - tree is immutable
            self._num_molecules,
            initial_concentrations=self._concentrations.copy(),
            initial_multiplicities=self._multiplicities.copy(),
        )

    def as_array(self) -> Any:
        """Get concentrations as 2D numpy array [compartments x molecules].

        Returns a view if numpy is available, otherwise a list of lists.
        """
        try:
            import numpy as np

            arr = np.array(self._concentrations, dtype=np.float64)
            return arr.reshape(self.num_compartments, self._num_molecules)
        except ImportError:
            # Fallback: return list of lists
            return [
                self.get_compartment(c) for c in range(self.num_compartments)
            ]

    def from_array(self, arr: Any) -> None:
        """Set concentrations from 2D array [compartments x molecules]."""
        try:
            import numpy as np

            flat = np.asarray(arr, dtype=np.float64).flatten()
            if len(flat) != len(self._concentrations):
                raise ValueError(
                    f"Array size {len(flat)} != expected {len(self._concentrations)}"
                )
            self._concentrations = flat.tolist()
        except ImportError:
            # Fallback: assume list of lists
            idx = 0
            for row in arr:
                for val in row:
                    self._concentrations[idx] = float(val)
                    idx += 1

    def __repr__(self) -> str:
        """Full representation."""
        return (
            f"WorldStateImpl(compartments={self.num_compartments}, "
            f"molecules={self._num_molecules})"
        )

    def __str__(self) -> str:
        """Short representation with summary stats."""
        total = sum(self._concentrations)
        nonzero = sum(1 for c in self._concentrations if c > 0)
        return (
            f"WorldState({self.num_compartments}x{self._num_molecules}, "
            f"total={total:.3g}, nonzero={nonzero})"
        )

tree property

The compartment tree this state belongs to (shared reference).

num_compartments property

Number of compartments (from tree).

num_molecules property

Number of molecules in vocabulary.

__init__(tree, num_molecules, initial_concentrations=None, initial_multiplicities=None)

Initialize world state.

Parameters:

Name Type Description Default
tree CompartmentTreeImpl

CompartmentTree defining the topology (shared reference)

required
num_molecules int

Number of molecules in vocabulary

required
initial_concentrations Optional[List[float]]

Optional flat array of initial concentrations

None
initial_multiplicities Optional[List[float]]

Optional array of initial multiplicities per compartment

None
Source code in src/alienbio/bio/world_state.py
def __init__(
    self,
    tree: CompartmentTreeImpl,
    num_molecules: int,
    initial_concentrations: Optional[List[float]] = None,
    initial_multiplicities: Optional[List[float]] = None,
) -> None:
    """Initialize world state.

    Args:
        tree: CompartmentTree defining the topology (shared reference)
        num_molecules: Number of molecules in vocabulary
        initial_concentrations: Optional flat array of initial concentrations
        initial_multiplicities: Optional array of initial multiplicities per compartment
    """
    self._tree = tree
    self._num_molecules = num_molecules

    num_compartments = tree.num_compartments
    size = num_compartments * num_molecules

    # Initialize concentrations
    if initial_concentrations is not None:
        if len(initial_concentrations) != size:
            raise ValueError(
                f"Initial concentrations size {len(initial_concentrations)} != "
                f"{num_compartments} * {num_molecules} = {size}"
            )
        self._concentrations = list(initial_concentrations)
    else:
        self._concentrations = [0.0] * size

    # Initialize multiplicities (default 1.0 for each compartment)
    if initial_multiplicities is not None:
        if len(initial_multiplicities) != num_compartments:
            raise ValueError(
                f"Initial multiplicities size {len(initial_multiplicities)} != "
                f"num_compartments {num_compartments}"
            )
        self._multiplicities = list(initial_multiplicities)
    else:
        self._multiplicities = [1.0] * num_compartments

get(compartment, molecule)

Get concentration of molecule in compartment.

Source code in src/alienbio/bio/world_state.py
def get(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
    """Get concentration of molecule in compartment."""
    return self._concentrations[self._index(compartment, molecule)]

set(compartment, molecule, value)

Set concentration of molecule in compartment.

Source code in src/alienbio/bio/world_state.py
def set(
    self, compartment: CompartmentId, molecule: MoleculeId, value: float
) -> None:
    """Set concentration of molecule in compartment."""
    self._concentrations[self._index(compartment, molecule)] = value

get_compartment(compartment)

Get all concentrations for a compartment.

Source code in src/alienbio/bio/world_state.py
def get_compartment(self, compartment: CompartmentId) -> List[float]:
    """Get all concentrations for a compartment."""
    start = compartment * self._num_molecules
    end = start + self._num_molecules
    return self._concentrations[start:end]

set_compartment(compartment, values)

Set all concentrations for a compartment.

Source code in src/alienbio/bio/world_state.py
def set_compartment(
    self, compartment: CompartmentId, values: List[float]
) -> None:
    """Set all concentrations for a compartment."""
    if len(values) != self._num_molecules:
        raise ValueError(
            f"Values length {len(values)} != num_molecules {self._num_molecules}"
        )
    start = compartment * self._num_molecules
    for i, v in enumerate(values):
        self._concentrations[start + i] = v

get_multiplicity(compartment)

Get multiplicity (instance count) for a compartment.

Source code in src/alienbio/bio/world_state.py
def get_multiplicity(self, compartment: CompartmentId) -> float:
    """Get multiplicity (instance count) for a compartment."""
    return self._multiplicities[compartment]

set_multiplicity(compartment, value)

Set multiplicity (instance count) for a compartment.

Source code in src/alienbio/bio/world_state.py
def set_multiplicity(self, compartment: CompartmentId, value: float) -> None:
    """Set multiplicity (instance count) for a compartment."""
    self._multiplicities[compartment] = value

get_all_multiplicities()

Get multiplicities for all compartments.

Source code in src/alienbio/bio/world_state.py
def get_all_multiplicities(self) -> List[float]:
    """Get multiplicities for all compartments."""
    return self._multiplicities.copy()

total_molecules(compartment, molecule)

Get total molecules = multiplicity * concentration.

Source code in src/alienbio/bio/world_state.py
def total_molecules(self, compartment: CompartmentId, molecule: MoleculeId) -> float:
    """Get total molecules = multiplicity * concentration."""
    return self._multiplicities[compartment] * self.get(compartment, molecule)

copy()

Create a copy of this state (shares tree reference).

Source code in src/alienbio/bio/world_state.py
def copy(self) -> WorldStateImpl:
    """Create a copy of this state (shares tree reference)."""
    return WorldStateImpl(
        self._tree,  # Shared reference - tree is immutable
        self._num_molecules,
        initial_concentrations=self._concentrations.copy(),
        initial_multiplicities=self._multiplicities.copy(),
    )

as_array()

Get concentrations as 2D numpy array [compartments x molecules].

Returns a view if numpy is available, otherwise a list of lists.

Source code in src/alienbio/bio/world_state.py
def as_array(self) -> Any:
    """Get concentrations as 2D numpy array [compartments x molecules].

    Returns a view if numpy is available, otherwise a list of lists.
    """
    try:
        import numpy as np

        arr = np.array(self._concentrations, dtype=np.float64)
        return arr.reshape(self.num_compartments, self._num_molecules)
    except ImportError:
        # Fallback: return list of lists
        return [
            self.get_compartment(c) for c in range(self.num_compartments)
        ]

from_array(arr)

Set concentrations from 2D array [compartments x molecules].

Source code in src/alienbio/bio/world_state.py
def from_array(self, arr: Any) -> None:
    """Set concentrations from 2D array [compartments x molecules]."""
    try:
        import numpy as np

        flat = np.asarray(arr, dtype=np.float64).flatten()
        if len(flat) != len(self._concentrations):
            raise ValueError(
                f"Array size {len(flat)} != expected {len(self._concentrations)}"
            )
        self._concentrations = flat.tolist()
    except ImportError:
        # Fallback: assume list of lists
        idx = 0
        for row in arr:
            for val in row:
                self._concentrations[idx] = float(val)
                idx += 1

__repr__()

Full representation.

Source code in src/alienbio/bio/world_state.py
def __repr__(self) -> str:
    """Full representation."""
    return (
        f"WorldStateImpl(compartments={self.num_compartments}, "
        f"molecules={self._num_molecules})"
    )

__str__()

Short representation with summary stats.

Source code in src/alienbio/bio/world_state.py
def __str__(self) -> str:
    """Short representation with summary stats."""
    total = sum(self._concentrations)
    nonzero = sum(1 for c in self._concentrations if c > 0)
    return (
        f"WorldState({self.num_compartments}x{self._num_molecules}, "
        f"total={total:.3g}, nonzero={nonzero})"
    )

StateImpl

Implementation: Concentrations of molecules at a point in time.

State is essentially a dict mapping molecules to concentration values. It's tied to a Chemistry which defines the valid molecules.

Attributes:

Name Type Description
chemistry ChemistryImpl

The Chemistry this state belongs to

concentrations ChemistryImpl

Dict mapping molecule names to concentration values

Example

state = StateImpl(chemistry) state["glucose"] = 1.0 state["atp"] = 0.5 print(state["glucose"]) # 1.0

Source code in src/alienbio/bio/state.py
class StateImpl:
    """Implementation: Concentrations of molecules at a point in time.

    State is essentially a dict mapping molecules to concentration values.
    It's tied to a Chemistry which defines the valid molecules.

    Attributes:
        chemistry: The Chemistry this state belongs to
        concentrations: Dict mapping molecule names to concentration values

    Example:
        state = StateImpl(chemistry)
        state["glucose"] = 1.0
        state["atp"] = 0.5
        print(state["glucose"])  # 1.0
    """

    __slots__ = ("_chemistry", "_concentrations")

    def __init__(
        self,
        chemistry: ChemistryImpl,
        initial: Optional[Dict[str, float]] = None,
    ) -> None:
        """Initialize state for a chemistry.

        Args:
            chemistry: The Chemistry defining valid molecules
            initial: Optional dict of initial concentrations by molecule name
        """
        self._chemistry = chemistry
        self._concentrations: Dict[str, float] = {}

        # Initialize all molecules to 0.0
        for name in chemistry.molecules:
            self._concentrations[name] = 0.0

        # Apply initial values
        if initial:
            for name, value in initial.items():
                if name in self._concentrations:
                    self._concentrations[name] = value
                else:
                    raise KeyError(f"Unknown molecule: {name!r}")

    @property
    def chemistry(self) -> ChemistryImpl:
        """The Chemistry this state belongs to."""
        return self._chemistry

    def __getitem__(self, key: str) -> float:
        """Get concentration by molecule name."""
        return self._concentrations[key]

    def __setitem__(self, key: str, value: float) -> None:
        """Set concentration by molecule name."""
        if key not in self._concentrations:
            raise KeyError(f"Unknown molecule: {key!r}")
        self._concentrations[key] = value

    def __contains__(self, key: str) -> bool:
        """Check if molecule exists in state."""
        return key in self._concentrations

    def __iter__(self) -> Iterator[str]:
        """Iterate over molecule names."""
        return iter(self._concentrations)

    def __len__(self) -> int:
        """Number of molecules in state."""
        return len(self._concentrations)

    def get(self, key: str, default: float = 0.0) -> float:
        """Get concentration with default."""
        return self._concentrations.get(key, default)

    def get_molecule(self, molecule: MoleculeImpl) -> float:
        """Get concentration by molecule object."""
        return self._concentrations[molecule.name]

    def set_molecule(self, molecule: MoleculeImpl, value: float) -> None:
        """Set concentration by molecule object."""
        self[molecule.name] = value

    def items(self) -> Iterator[tuple[str, float]]:
        """Iterate over (name, concentration) pairs."""
        return iter(self._concentrations.items())

    def copy(self) -> StateImpl:
        """Create a copy of this state."""
        new_state = StateImpl(self._chemistry)
        new_state._concentrations = self._concentrations.copy()
        return new_state

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dict for serialization."""
        return {
            "chemistry": self._chemistry.local_name,
            "concentrations": self._concentrations.copy(),
        }

    @classmethod
    def from_dict(cls, chemistry: ChemistryImpl, data: Dict[str, Any]) -> StateImpl:
        """Create state from serialized dict."""
        return cls(chemistry, initial=data.get("concentrations", {}))

    def __repr__(self) -> str:
        """Full representation."""
        conc_str = ", ".join(
            f"{k}={v:.3g}" for k, v in self._concentrations.items()
        )
        return f"StateImpl({conc_str})"

    def __str__(self) -> str:
        """Short representation."""
        non_zero = [(k, v) for k, v in self._concentrations.items() if v > 0]
        if not non_zero:
            return "StateImpl(empty)"
        conc_str = ", ".join(f"{k}={v:.3g}" for k, v in non_zero[:5])
        if len(non_zero) > 5:
            conc_str += f", ... ({len(non_zero)} molecules)"
        return f"StateImpl({conc_str})"

chemistry property

The Chemistry this state belongs to.

__init__(chemistry, initial=None)

Initialize state for a chemistry.

Parameters:

Name Type Description Default
chemistry ChemistryImpl

The Chemistry defining valid molecules

required
initial Optional[Dict[str, float]]

Optional dict of initial concentrations by molecule name

None
Source code in src/alienbio/bio/state.py
def __init__(
    self,
    chemistry: ChemistryImpl,
    initial: Optional[Dict[str, float]] = None,
) -> None:
    """Initialize state for a chemistry.

    Args:
        chemistry: The Chemistry defining valid molecules
        initial: Optional dict of initial concentrations by molecule name
    """
    self._chemistry = chemistry
    self._concentrations: Dict[str, float] = {}

    # Initialize all molecules to 0.0
    for name in chemistry.molecules:
        self._concentrations[name] = 0.0

    # Apply initial values
    if initial:
        for name, value in initial.items():
            if name in self._concentrations:
                self._concentrations[name] = value
            else:
                raise KeyError(f"Unknown molecule: {name!r}")

__getitem__(key)

Get concentration by molecule name.

Source code in src/alienbio/bio/state.py
def __getitem__(self, key: str) -> float:
    """Get concentration by molecule name."""
    return self._concentrations[key]

__setitem__(key, value)

Set concentration by molecule name.

Source code in src/alienbio/bio/state.py
def __setitem__(self, key: str, value: float) -> None:
    """Set concentration by molecule name."""
    if key not in self._concentrations:
        raise KeyError(f"Unknown molecule: {key!r}")
    self._concentrations[key] = value

__contains__(key)

Check if molecule exists in state.

Source code in src/alienbio/bio/state.py
def __contains__(self, key: str) -> bool:
    """Check if molecule exists in state."""
    return key in self._concentrations

__iter__()

Iterate over molecule names.

Source code in src/alienbio/bio/state.py
def __iter__(self) -> Iterator[str]:
    """Iterate over molecule names."""
    return iter(self._concentrations)

__len__()

Number of molecules in state.

Source code in src/alienbio/bio/state.py
def __len__(self) -> int:
    """Number of molecules in state."""
    return len(self._concentrations)

get(key, default=0.0)

Get concentration with default.

Source code in src/alienbio/bio/state.py
def get(self, key: str, default: float = 0.0) -> float:
    """Get concentration with default."""
    return self._concentrations.get(key, default)

get_molecule(molecule)

Get concentration by molecule object.

Source code in src/alienbio/bio/state.py
def get_molecule(self, molecule: MoleculeImpl) -> float:
    """Get concentration by molecule object."""
    return self._concentrations[molecule.name]

set_molecule(molecule, value)

Set concentration by molecule object.

Source code in src/alienbio/bio/state.py
def set_molecule(self, molecule: MoleculeImpl, value: float) -> None:
    """Set concentration by molecule object."""
    self[molecule.name] = value

items()

Iterate over (name, concentration) pairs.

Source code in src/alienbio/bio/state.py
def items(self) -> Iterator[tuple[str, float]]:
    """Iterate over (name, concentration) pairs."""
    return iter(self._concentrations.items())

copy()

Create a copy of this state.

Source code in src/alienbio/bio/state.py
def copy(self) -> StateImpl:
    """Create a copy of this state."""
    new_state = StateImpl(self._chemistry)
    new_state._concentrations = self._concentrations.copy()
    return new_state

to_dict()

Convert to dict for serialization.

Source code in src/alienbio/bio/state.py
def to_dict(self) -> Dict[str, Any]:
    """Convert to dict for serialization."""
    return {
        "chemistry": self._chemistry.local_name,
        "concentrations": self._concentrations.copy(),
    }

from_dict(chemistry, data) classmethod

Create state from serialized dict.

Source code in src/alienbio/bio/state.py
@classmethod
def from_dict(cls, chemistry: ChemistryImpl, data: Dict[str, Any]) -> StateImpl:
    """Create state from serialized dict."""
    return cls(chemistry, initial=data.get("concentrations", {}))

__repr__()

Full representation.

Source code in src/alienbio/bio/state.py
def __repr__(self) -> str:
    """Full representation."""
    conc_str = ", ".join(
        f"{k}={v:.3g}" for k, v in self._concentrations.items()
    )
    return f"StateImpl({conc_str})"

__str__()

Short representation.

Source code in src/alienbio/bio/state.py
def __str__(self) -> str:
    """Short representation."""
    non_zero = [(k, v) for k, v in self._concentrations.items() if v > 0]
    if not non_zero:
        return "StateImpl(empty)"
    conc_str = ", ".join(f"{k}={v:.3g}" for k, v in non_zero[:5])
    if len(non_zero) > 5:
        conc_str += f", ... ({len(non_zero)} molecules)"
    return f"StateImpl({conc_str})"

ReferenceSimulatorImpl

Bases: SimulatorBase

Reference implementation: Basic simulator applying reactions once per step.

This is the reference implementation for testing and validation. For each reaction: - Compute rate (constant or from rate function) - Subtract rate * coefficient from each reactant - Add rate * coefficient to each product

Note: This is a simple Euler-style implementation. For more accurate kinetics, use specialized simulators (JAX, etc.).

Source code in src/alienbio/bio/simulator.py
@factory(name="reference", protocol=Simulator, default=True)
class ReferenceSimulatorImpl(SimulatorBase):
    """Reference implementation: Basic simulator applying reactions once per step.

    This is the reference implementation for testing and validation.
    For each reaction:
    - Compute rate (constant or from rate function)
    - Subtract rate * coefficient from each reactant
    - Add rate * coefficient to each product

    Note: This is a simple Euler-style implementation. For more
    accurate kinetics, use specialized simulators (JAX, etc.).
    """

    __slots__ = ()

    def step(self, state: StateImpl) -> StateImpl:
        """Apply all reactions once."""
        new_state = state.copy()

        for reaction in self._chemistry.reactions.values():
            # Get effective rate for this state
            rate = reaction.get_rate(state) * self._dt

            # Apply reaction: consume reactants, produce products
            for molecule, coef in reaction.reactants.items():
                current = new_state.get_molecule(molecule)
                new_state.set_molecule(molecule, max(0.0, current - rate * coef))

            for molecule, coef in reaction.products.items():
                current = new_state.get_molecule(molecule)
                new_state.set_molecule(molecule, current + rate * coef)

        return new_state

step(state)

Apply all reactions once.

Source code in src/alienbio/bio/simulator.py
def step(self, state: StateImpl) -> StateImpl:
    """Apply all reactions once."""
    new_state = state.copy()

    for reaction in self._chemistry.reactions.values():
        # Get effective rate for this state
        rate = reaction.get_rate(state) * self._dt

        # Apply reaction: consume reactants, produce products
        for molecule, coef in reaction.reactants.items():
            current = new_state.get_molecule(molecule)
            new_state.set_molecule(molecule, max(0.0, current - rate * coef))

        for molecule, coef in reaction.products.items():
            current = new_state.get_molecule(molecule)
            new_state.set_molecule(molecule, current + rate * coef)

    return new_state

SimulatorBase

Bases: ABC

Abstract base class for simulators.

A Simulator advances the state of a chemical system over time. Subclasses implement the actual simulation algorithm.

The basic interface: - step(state) -> state: advance one time step - run(state, n) -> [states]: run n steps, return timeline

Example

sim = MySimulator(chemistry, dt=0.01) timeline = sim.run(initial_state, steps=100)

Source code in src/alienbio/bio/simulator.py
class SimulatorBase(ABC):
    """Abstract base class for simulators.

    A Simulator advances the state of a chemical system over time.
    Subclasses implement the actual simulation algorithm.

    The basic interface:
    - step(state) -> state: advance one time step
    - run(state, n) -> [states]: run n steps, return timeline

    Example:
        sim = MySimulator(chemistry, dt=0.01)
        timeline = sim.run(initial_state, steps=100)
    """

    __slots__ = ("_chemistry", "_dt")

    def __init__(self, chemistry: ChemistryImpl, dt: float = 1.0) -> None:
        """Initialize simulator.

        Args:
            chemistry: The Chemistry to simulate
            dt: Time step size (default 1.0)
        """
        self._chemistry = chemistry
        self._dt = dt

    @property
    def chemistry(self) -> ChemistryImpl:
        """The Chemistry being simulated."""
        return self._chemistry

    @property
    def dt(self) -> float:
        """Time step size."""
        return self._dt

    @abstractmethod
    def step(self, state: StateImpl) -> StateImpl:
        """Advance the simulation by one time step.

        Args:
            state: Current system state

        Returns:
            New state after applying all reactions once
        """
        ...

    def run(self, state: StateImpl, steps: int) -> List[StateImpl]:
        """Run simulation for multiple steps.

        Args:
            state: Initial state
            steps: Number of steps to run

        Returns:
            Timeline of states (length = steps + 1, including initial)
        """
        timeline = [state.copy()]
        current = state.copy()

        for _ in range(steps):
            current = self.step(current)
            timeline.append(current.copy())

        return timeline

chemistry property

The Chemistry being simulated.

dt property

Time step size.

__init__(chemistry, dt=1.0)

Initialize simulator.

Parameters:

Name Type Description Default
chemistry ChemistryImpl

The Chemistry to simulate

required
dt float

Time step size (default 1.0)

1.0
Source code in src/alienbio/bio/simulator.py
def __init__(self, chemistry: ChemistryImpl, dt: float = 1.0) -> None:
    """Initialize simulator.

    Args:
        chemistry: The Chemistry to simulate
        dt: Time step size (default 1.0)
    """
    self._chemistry = chemistry
    self._dt = dt

step(state) abstractmethod

Advance the simulation by one time step.

Parameters:

Name Type Description Default
state StateImpl

Current system state

required

Returns:

Type Description
StateImpl

New state after applying all reactions once

Source code in src/alienbio/bio/simulator.py
@abstractmethod
def step(self, state: StateImpl) -> StateImpl:
    """Advance the simulation by one time step.

    Args:
        state: Current system state

    Returns:
        New state after applying all reactions once
    """
    ...

run(state, steps)

Run simulation for multiple steps.

Parameters:

Name Type Description Default
state StateImpl

Initial state

required
steps int

Number of steps to run

required

Returns:

Type Description
List[StateImpl]

Timeline of states (length = steps + 1, including initial)

Source code in src/alienbio/bio/simulator.py
def run(self, state: StateImpl, steps: int) -> List[StateImpl]:
    """Run simulation for multiple steps.

    Args:
        state: Initial state
        steps: Number of steps to run

    Returns:
        Timeline of states (length = steps + 1, including initial)
    """
    timeline = [state.copy()]
    current = state.copy()

    for _ in range(steps):
        current = self.step(current)
        timeline.append(current.copy())

    return timeline

WorldSimulatorImpl

Implementation: Multi-compartment simulator with reactions and flows.

Simulates a world with: - Multiple compartments organized in a tree (organism > organ > cell) - Reactions that occur within compartments - Flows that transport molecules across compartment membranes

Each step: 1. Compute all reaction rates (per compartment) 2. Compute all flow fluxes (between parent-child pairs) 3. Apply reactions (modify concentrations within compartments) 4. Apply flows (transfer molecules across membranes)

Example

Build world

tree = CompartmentTreeImpl() organism = tree.add_root("organism") cell = tree.add_child(organism, "cell")

Define reactions and flows

reactions = [ReactionSpec("r1", {0: 1}, {1: 1}, rate_constant=0.1)] flows = [GeneralFlow(child=cell, molecule=0, rate_constant=0.05)]

Create simulator

sim = WorldSimulatorImpl( tree=tree, reactions=reactions, flows=flows, num_molecules=10, dt=0.1, )

Run simulation

state = WorldStateImpl(tree=tree, num_molecules=10) state.set(organism, 0, 100.0) # initial concentration history = sim.run(state, steps=1000, sample_every=100)

All states in history share the same tree reference

assert history[0].tree is history[-1].tree

Source code in src/alienbio/bio/world_simulator.py
class WorldSimulatorImpl:
    """Implementation: Multi-compartment simulator with reactions and flows.

    Simulates a world with:
    - Multiple compartments organized in a tree (organism > organ > cell)
    - Reactions that occur within compartments
    - Flows that transport molecules across compartment membranes

    Each step:
    1. Compute all reaction rates (per compartment)
    2. Compute all flow fluxes (between parent-child pairs)
    3. Apply reactions (modify concentrations within compartments)
    4. Apply flows (transfer molecules across membranes)

    Example:
        # Build world
        tree = CompartmentTreeImpl()
        organism = tree.add_root("organism")
        cell = tree.add_child(organism, "cell")

        # Define reactions and flows
        reactions = [ReactionSpec("r1", {0: 1}, {1: 1}, rate_constant=0.1)]
        flows = [GeneralFlow(child=cell, molecule=0, rate_constant=0.05)]

        # Create simulator
        sim = WorldSimulatorImpl(
            tree=tree,
            reactions=reactions,
            flows=flows,
            num_molecules=10,
            dt=0.1,
        )

        # Run simulation
        state = WorldStateImpl(tree=tree, num_molecules=10)
        state.set(organism, 0, 100.0)  # initial concentration
        history = sim.run(state, steps=1000, sample_every=100)

        # All states in history share the same tree reference
        assert history[0].tree is history[-1].tree
    """

    __slots__ = ("_tree", "_reactions", "_flows", "_num_molecules", "_dt")

    def __init__(
        self,
        tree: CompartmentTreeImpl,
        reactions: List[ReactionSpec],
        flows: List[GeneralFlow],
        num_molecules: int,
        dt: float = 1.0,
    ) -> None:
        """Initialize world simulator.

        Args:
            tree: Compartment topology
            reactions: List of reaction specifications
            flows: List of flow specifications
            num_molecules: Number of molecules in vocabulary
            dt: Time step size
        """
        self._tree = tree
        self._reactions = reactions
        self._flows = flows
        self._num_molecules = num_molecules
        self._dt = dt

    @property
    def tree(self) -> CompartmentTreeImpl:
        """Compartment topology."""
        return self._tree

    @property
    def reactions(self) -> List[ReactionSpec]:
        """Reaction specifications."""
        return self._reactions

    @property
    def flows(self) -> List[GeneralFlow]:
        """Flow specifications."""
        return self._flows

    @property
    def num_molecules(self) -> int:
        """Number of molecules in vocabulary."""
        return self._num_molecules

    @property
    def dt(self) -> float:
        """Time step size."""
        return self._dt

    def step(self, state: WorldStateImpl) -> WorldStateImpl:
        """Advance simulation by one time step.

        Args:
            state: Current world state

        Returns:
            New state after applying reactions and flows
        """
        new_state = state.copy()

        # Apply reactions in each compartment
        for reaction in self._reactions:
            compartments = reaction.compartments
            if compartments is None:
                compartments = range(self._tree.num_compartments)

            for comp in compartments:
                self._apply_reaction(new_state, reaction, comp)

        # Apply flows between compartments
        for flow in self._flows:
            flow.apply(new_state, self._tree, self._dt)

        return new_state

    def _apply_reaction(
        self,
        state: WorldStateImpl,
        reaction: ReactionSpec,
        compartment: CompartmentId,
    ) -> None:
        """Apply a single reaction in a compartment."""
        # Compute rate using mass-action kinetics
        rate = reaction.rate_constant
        for mol_id, stoich in reaction.reactants.items():
            conc = state.get(compartment, mol_id)
            rate *= conc ** stoich

        rate *= self._dt

        # Consume reactants
        for mol_id, stoich in reaction.reactants.items():
            current = state.get(compartment, mol_id)
            new_val = max(0.0, current - rate * stoich)
            state.set(compartment, mol_id, new_val)

        # Produce products
        for mol_id, stoich in reaction.products.items():
            current = state.get(compartment, mol_id)
            state.set(compartment, mol_id, current + rate * stoich)

    def run(
        self,
        state: WorldStateImpl,
        steps: int,
        sample_every: Optional[int] = None,
    ) -> List[WorldStateImpl]:
        """Run simulation for multiple steps.

        Args:
            state: Initial state (not modified)
            steps: Number of steps to run
            sample_every: If set, only keep every Nth state (plus final)

        Returns:
            List of states (timeline)
        """
        if sample_every is None:
            sample_every = 1

        history: List[WorldStateImpl] = []
        current = state.copy()

        for i in range(steps):
            if i % sample_every == 0:
                history.append(current.copy())
            current = self.step(current)

        # Always include final state
        history.append(current.copy())
        return history

    @classmethod
    def from_chemistry(
        cls,
        chemistry: ChemistryImpl,
        tree: CompartmentTreeImpl,
        flows: Optional[List[GeneralFlow]] = None,
        dt: float = 1.0,
    ) -> WorldSimulatorImpl:
        """Create simulator from a Chemistry and compartment tree.

        Args:
            chemistry: Chemistry containing molecules and reactions
            tree: Compartment topology
            flows: Optional list of flows (empty if not provided)
            dt: Time step

        Returns:
            Configured WorldSimulatorImpl
        """
        # Build molecule ID mapping
        mol_names = list(chemistry.molecules.keys())
        mol_to_id = {name: i for i, name in enumerate(mol_names)}

        # Convert reactions to specs
        reaction_specs = []
        for rxn_name, reaction in chemistry.reactions.items():
            reactants = {}
            products = {}

            for mol, stoich in reaction.reactants.items():
                mol_id = mol_to_id.get(mol.name)
                if mol_id is not None:
                    reactants[mol_id] = stoich

            for mol, stoich in reaction.products.items():
                mol_id = mol_to_id.get(mol.name)
                if mol_id is not None:
                    products[mol_id] = stoich

            # Get rate constant (only works for constant rates)
            rate = reaction.rate if isinstance(reaction.rate, (int, float)) else 1.0

            reaction_specs.append(ReactionSpec(
                name=rxn_name,
                reactants=reactants,
                products=products,
                rate_constant=rate,
                compartments=None,  # Apply to all compartments
            ))

        return cls(
            tree=tree,
            reactions=reaction_specs,
            flows=flows or [],
            num_molecules=len(mol_names),
            dt=dt,
        )

    def __repr__(self) -> str:
        """Full representation."""
        return (
            f"WorldSimulatorImpl(compartments={self._tree.num_compartments}, "
            f"molecules={self._num_molecules}, "
            f"reactions={len(self._reactions)}, "
            f"flows={len(self._flows)}, dt={self._dt})"
        )

tree property

Compartment topology.

reactions property

Reaction specifications.

flows property

Flow specifications.

num_molecules property

Number of molecules in vocabulary.

dt property

Time step size.

__init__(tree, reactions, flows, num_molecules, dt=1.0)

Initialize world simulator.

Parameters:

Name Type Description Default
tree CompartmentTreeImpl

Compartment topology

required
reactions List[ReactionSpec]

List of reaction specifications

required
flows List[GeneralFlow]

List of flow specifications

required
num_molecules int

Number of molecules in vocabulary

required
dt float

Time step size

1.0
Source code in src/alienbio/bio/world_simulator.py
def __init__(
    self,
    tree: CompartmentTreeImpl,
    reactions: List[ReactionSpec],
    flows: List[GeneralFlow],
    num_molecules: int,
    dt: float = 1.0,
) -> None:
    """Initialize world simulator.

    Args:
        tree: Compartment topology
        reactions: List of reaction specifications
        flows: List of flow specifications
        num_molecules: Number of molecules in vocabulary
        dt: Time step size
    """
    self._tree = tree
    self._reactions = reactions
    self._flows = flows
    self._num_molecules = num_molecules
    self._dt = dt

step(state)

Advance simulation by one time step.

Parameters:

Name Type Description Default
state WorldStateImpl

Current world state

required

Returns:

Type Description
WorldStateImpl

New state after applying reactions and flows

Source code in src/alienbio/bio/world_simulator.py
def step(self, state: WorldStateImpl) -> WorldStateImpl:
    """Advance simulation by one time step.

    Args:
        state: Current world state

    Returns:
        New state after applying reactions and flows
    """
    new_state = state.copy()

    # Apply reactions in each compartment
    for reaction in self._reactions:
        compartments = reaction.compartments
        if compartments is None:
            compartments = range(self._tree.num_compartments)

        for comp in compartments:
            self._apply_reaction(new_state, reaction, comp)

    # Apply flows between compartments
    for flow in self._flows:
        flow.apply(new_state, self._tree, self._dt)

    return new_state

run(state, steps, sample_every=None)

Run simulation for multiple steps.

Parameters:

Name Type Description Default
state WorldStateImpl

Initial state (not modified)

required
steps int

Number of steps to run

required
sample_every Optional[int]

If set, only keep every Nth state (plus final)

None

Returns:

Type Description
List[WorldStateImpl]

List of states (timeline)

Source code in src/alienbio/bio/world_simulator.py
def run(
    self,
    state: WorldStateImpl,
    steps: int,
    sample_every: Optional[int] = None,
) -> List[WorldStateImpl]:
    """Run simulation for multiple steps.

    Args:
        state: Initial state (not modified)
        steps: Number of steps to run
        sample_every: If set, only keep every Nth state (plus final)

    Returns:
        List of states (timeline)
    """
    if sample_every is None:
        sample_every = 1

    history: List[WorldStateImpl] = []
    current = state.copy()

    for i in range(steps):
        if i % sample_every == 0:
            history.append(current.copy())
        current = self.step(current)

    # Always include final state
    history.append(current.copy())
    return history

from_chemistry(chemistry, tree, flows=None, dt=1.0) classmethod

Create simulator from a Chemistry and compartment tree.

Parameters:

Name Type Description Default
chemistry ChemistryImpl

Chemistry containing molecules and reactions

required
tree CompartmentTreeImpl

Compartment topology

required
flows Optional[List[GeneralFlow]]

Optional list of flows (empty if not provided)

None
dt float

Time step

1.0

Returns:

Type Description
WorldSimulatorImpl

Configured WorldSimulatorImpl

Source code in src/alienbio/bio/world_simulator.py
@classmethod
def from_chemistry(
    cls,
    chemistry: ChemistryImpl,
    tree: CompartmentTreeImpl,
    flows: Optional[List[GeneralFlow]] = None,
    dt: float = 1.0,
) -> WorldSimulatorImpl:
    """Create simulator from a Chemistry and compartment tree.

    Args:
        chemistry: Chemistry containing molecules and reactions
        tree: Compartment topology
        flows: Optional list of flows (empty if not provided)
        dt: Time step

    Returns:
        Configured WorldSimulatorImpl
    """
    # Build molecule ID mapping
    mol_names = list(chemistry.molecules.keys())
    mol_to_id = {name: i for i, name in enumerate(mol_names)}

    # Convert reactions to specs
    reaction_specs = []
    for rxn_name, reaction in chemistry.reactions.items():
        reactants = {}
        products = {}

        for mol, stoich in reaction.reactants.items():
            mol_id = mol_to_id.get(mol.name)
            if mol_id is not None:
                reactants[mol_id] = stoich

        for mol, stoich in reaction.products.items():
            mol_id = mol_to_id.get(mol.name)
            if mol_id is not None:
                products[mol_id] = stoich

        # Get rate constant (only works for constant rates)
        rate = reaction.rate if isinstance(reaction.rate, (int, float)) else 1.0

        reaction_specs.append(ReactionSpec(
            name=rxn_name,
            reactants=reactants,
            products=products,
            rate_constant=rate,
            compartments=None,  # Apply to all compartments
        ))

    return cls(
        tree=tree,
        reactions=reaction_specs,
        flows=flows or [],
        num_molecules=len(mol_names),
        dt=dt,
    )

__repr__()

Full representation.

Source code in src/alienbio/bio/world_simulator.py
def __repr__(self) -> str:
    """Full representation."""
    return (
        f"WorldSimulatorImpl(compartments={self._tree.num_compartments}, "
        f"molecules={self._num_molecules}, "
        f"reactions={len(self._reactions)}, "
        f"flows={len(self._flows)}, dt={self._dt})"
    )

ReactionSpec

Specification for a reaction in the world simulator.

Reactions occur within a single compartment and transform molecules. This is a lightweight spec using molecule IDs for efficient simulation.

Attributes:

Name Type Description
name

Human-readable name

reactants

Dict[MoleculeId, stoichiometry]

products

Dict[MoleculeId, stoichiometry]

rate_constant

Base reaction rate

compartments

Which compartments this reaction occurs in (None = all)

Source code in src/alienbio/bio/world_simulator.py
class ReactionSpec:
    """Specification for a reaction in the world simulator.

    Reactions occur within a single compartment and transform molecules.
    This is a lightweight spec using molecule IDs for efficient simulation.

    Attributes:
        name: Human-readable name
        reactants: Dict[MoleculeId, stoichiometry]
        products: Dict[MoleculeId, stoichiometry]
        rate_constant: Base reaction rate
        compartments: Which compartments this reaction occurs in (None = all)
    """

    __slots__ = ("name", "reactants", "products", "rate_constant", "compartments")

    def __init__(
        self,
        name: str,
        reactants: Dict[MoleculeId, float],
        products: Dict[MoleculeId, float],
        rate_constant: float = 1.0,
        compartments: Optional[List[CompartmentId]] = None,
    ) -> None:
        self.name = name
        self.reactants = reactants
        self.products = products
        self.rate_constant = rate_constant
        self.compartments = compartments  # None means all compartments

BioSystem

A complete biological system: chemistry + state + simulator.

BioSystem wraps a Chemistry (molecules and reactions), a State (concentrations), and a Simulator into a single convenient object.

Example

system = BioSystem(chemistry, state) timeline = system.run(steps=100)

With random initial concentrations

system = BioSystem.random(chemistry, seed=42)

Source code in src/alienbio/bio/biosystem.py
class BioSystem:
    """A complete biological system: chemistry + state + simulator.

    BioSystem wraps a Chemistry (molecules and reactions), a State
    (concentrations), and a Simulator into a single convenient object.

    Example:
        system = BioSystem(chemistry, state)
        timeline = system.run(steps=100)

        # With random initial concentrations
        system = BioSystem.random(chemistry, seed=42)
    """

    __slots__ = ("_chemistry", "_state", "_simulator")

    def __init__(
        self,
        chemistry: ChemistryImpl,
        state: Optional[StateImpl] = None,
        *,
        simulator: Optional[SimulatorBase] = None,
        dt: float = 1.0,
    ) -> None:
        self._chemistry = chemistry
        self._state = state if state is not None else StateImpl(chemistry)
        self._simulator = simulator or ReferenceSimulatorImpl(chemistry, dt=dt)

    @classmethod
    def random(
        cls,
        chemistry: ChemistryImpl,
        *,
        seed: Optional[int] = None,
        min_conc: float = 0.0,
        max_conc: float = 10.0,
        dt: float = 1.0,
    ) -> BioSystem:
        """Create a BioSystem with random initial concentrations."""
        rng = random.Random(seed)
        initial = {
            name: rng.uniform(min_conc, max_conc)
            for name in chemistry.molecules
        }
        state = StateImpl(chemistry, initial=initial)
        return cls(chemistry, state, dt=dt)

    @property
    def chemistry(self) -> ChemistryImpl:
        return self._chemistry

    @property
    def state(self) -> StateImpl:
        return self._state

    @state.setter
    def state(self, value: StateImpl) -> None:
        self._state = value

    @property
    def simulator(self) -> SimulatorBase:
        return self._simulator

    @property
    def num_molecules(self) -> int:
        return len(self._chemistry.molecules)

    @property
    def num_reactions(self) -> int:
        return len(self._chemistry.reactions)

    def step(self) -> StateImpl:
        """Advance one time step, updating internal state."""
        self._state = self._simulator.step(self._state)
        return self._state

    def run(self, steps: int) -> List[StateImpl]:
        """Run simulation for multiple steps.

        Updates internal state to the final state.
        Returns the full timeline including initial state.
        """
        timeline = self._simulator.run(self._state, steps)
        self._state = timeline[-1].copy()
        return timeline

    def __repr__(self) -> str:
        return (
            f"BioSystem(molecules={self.num_molecules}, "
            f"reactions={self.num_reactions}, dt={self._simulator.dt})"
        )

random(chemistry, *, seed=None, min_conc=0.0, max_conc=10.0, dt=1.0) classmethod

Create a BioSystem with random initial concentrations.

Source code in src/alienbio/bio/biosystem.py
@classmethod
def random(
    cls,
    chemistry: ChemistryImpl,
    *,
    seed: Optional[int] = None,
    min_conc: float = 0.0,
    max_conc: float = 10.0,
    dt: float = 1.0,
) -> BioSystem:
    """Create a BioSystem with random initial concentrations."""
    rng = random.Random(seed)
    initial = {
        name: rng.uniform(min_conc, max_conc)
        for name in chemistry.molecules
    }
    state = StateImpl(chemistry, initial=initial)
    return cls(chemistry, state, dt=dt)

step()

Advance one time step, updating internal state.

Source code in src/alienbio/bio/biosystem.py
def step(self) -> StateImpl:
    """Advance one time step, updating internal state."""
    self._state = self._simulator.step(self._state)
    return self._state

run(steps)

Run simulation for multiple steps.

Updates internal state to the final state. Returns the full timeline including initial state.

Source code in src/alienbio/bio/biosystem.py
def run(self, steps: int) -> List[StateImpl]:
    """Run simulation for multiple steps.

    Updates internal state to the final state.
    Returns the full timeline including initial state.
    """
    timeline = self._simulator.run(self._state, steps)
    self._state = timeline[-1].copy()
    return timeline

StabilityResult dataclass

Result of a stability check on a timeline.

Source code in src/alienbio/bio/equilibrium.py
@dataclass
class StabilityResult:
    """Result of a stability check on a timeline."""

    stable: bool
    variance: Dict[str, float]
    max_variance: float
    unstable_molecules: List[str]
    steps_run: int
    window: int

HomeostasisTarget dataclass

A target concentration range for a molecule.

Source code in src/alienbio/bio/equilibrium.py
@dataclass
class HomeostasisTarget:
    """A target concentration range for a molecule."""

    molecule: str
    target: float
    tolerance: float = 0.1

    @property
    def low(self) -> float:
        return self.target * (1.0 - self.tolerance)

    @property
    def high(self) -> float:
        return self.target * (1.0 + self.tolerance)

    def check(self, concentration: float) -> bool:
        return self.low <= concentration <= self.high

AgentInterface

Agent-facing API for interacting with a BioSystem.

Bundles all available measurements and actions with text descriptions. Provides a uniform interface for agents to query and modify the system.

Example

interface = AgentInterface(system) conc = interface.measure("concentration", molecule="A") interface.act("add_molecule", molecule="A", amount=5.0)

Source code in src/alienbio/bio/agent_interface.py
class AgentInterface:
    """Agent-facing API for interacting with a BioSystem.

    Bundles all available measurements and actions with text descriptions.
    Provides a uniform interface for agents to query and modify the system.

    Example:
        interface = AgentInterface(system)
        conc = interface.measure("concentration", molecule="A")
        interface.act("add_molecule", molecule="A", amount=5.0)
    """

    def __init__(self, system: "BioSystem") -> None:
        self._system = system

        # Register built-in measurements
        self._measurements: Dict[str, Any] = {
            "concentration": ConcentrationMeasurement(),
            "all_concentrations": AllConcentrationsMeasurement(),
            "rate": RateMeasurement(),
            "molecule_count": MoleculeCountMeasurement(),
            "reaction_count": ReactionCountMeasurement(),
        }

        # Register built-in actions
        self._actions: Dict[str, Any] = {
            "add_molecule": AddMoleculeAction(),
            "remove_molecule": RemoveMoleculeAction(),
            "set_concentration": SetConcentrationAction(),
            "adjust_rate": AdjustRateAction(),
        }

    @property
    def system(self) -> "BioSystem":
        return self._system

    def available_measurements(self) -> List[Dict[str, Any]]:
        """List all available measurements with descriptions."""
        return [
            {
                "name": m.name,
                "description": m.description,
                "params": getattr(m, "params", {}),
            }
            for m in self._measurements.values()
        ]

    def available_actions(self) -> List[Dict[str, Any]]:
        """List all available actions with descriptions."""
        return [
            {
                "name": a.name,
                "description": a.description,
                "params": getattr(a, "params", {}),
            }
            for a in self._actions.values()
        ]

    def measure(self, name: str, **params: Any) -> Any:
        """Take a measurement.

        Args:
            name: Measurement name (e.g., "concentration")
            **params: Measurement parameters (e.g., molecule="A")

        Returns:
            Measurement result (type depends on measurement)

        Raises:
            KeyError: If measurement name is unknown
        """
        if name not in self._measurements:
            raise KeyError(f"Unknown measurement: {name!r}")
        m = self._measurements[name]
        return m.measure(self._system.state, **params)

    def act(self, name: str, **params: Any) -> Any:
        """Execute an action.

        For state-modifying actions (add_molecule, remove_molecule,
        set_concentration), the system's state is updated in place.

        Args:
            name: Action name (e.g., "add_molecule")
            **params: Action parameters (e.g., molecule="A", amount=5.0)

        Returns:
            The new state after the action

        Raises:
            KeyError: If action name is unknown
        """
        if name not in self._actions:
            raise KeyError(f"Unknown action: {name!r}")
        a = self._actions[name]
        new_state = a.apply(self._system.state, **params)
        self._system.state = new_state
        return new_state

    def describe(self) -> str:
        """Generate a text description of the interface for an agent."""
        lines = ["Available measurements:"]
        for m in self.available_measurements():
            params_str = ", ".join(f"{k}: {v}" for k, v in m["params"].items())
            if params_str:
                lines.append(f"  - {m['name']}({params_str}): {m['description']}")
            else:
                lines.append(f"  - {m['name']}(): {m['description']}")

        lines.append("")
        lines.append("Available actions:")
        for a in self.available_actions():
            params_str = ", ".join(f"{k}: {v}" for k, v in a["params"].items())
            if params_str:
                lines.append(f"  - {a['name']}({params_str}): {a['description']}")
            else:
                lines.append(f"  - {a['name']}(): {a['description']}")

        return "\n".join(lines)

available_measurements()

List all available measurements with descriptions.

Source code in src/alienbio/bio/agent_interface.py
def available_measurements(self) -> List[Dict[str, Any]]:
    """List all available measurements with descriptions."""
    return [
        {
            "name": m.name,
            "description": m.description,
            "params": getattr(m, "params", {}),
        }
        for m in self._measurements.values()
    ]

available_actions()

List all available actions with descriptions.

Source code in src/alienbio/bio/agent_interface.py
def available_actions(self) -> List[Dict[str, Any]]:
    """List all available actions with descriptions."""
    return [
        {
            "name": a.name,
            "description": a.description,
            "params": getattr(a, "params", {}),
        }
        for a in self._actions.values()
    ]

measure(name, **params)

Take a measurement.

Parameters:

Name Type Description Default
name str

Measurement name (e.g., "concentration")

required
**params Any

Measurement parameters (e.g., molecule="A")

{}

Returns:

Type Description
Any

Measurement result (type depends on measurement)

Raises:

Type Description
KeyError

If measurement name is unknown

Source code in src/alienbio/bio/agent_interface.py
def measure(self, name: str, **params: Any) -> Any:
    """Take a measurement.

    Args:
        name: Measurement name (e.g., "concentration")
        **params: Measurement parameters (e.g., molecule="A")

    Returns:
        Measurement result (type depends on measurement)

    Raises:
        KeyError: If measurement name is unknown
    """
    if name not in self._measurements:
        raise KeyError(f"Unknown measurement: {name!r}")
    m = self._measurements[name]
    return m.measure(self._system.state, **params)

act(name, **params)

Execute an action.

For state-modifying actions (add_molecule, remove_molecule, set_concentration), the system's state is updated in place.

Parameters:

Name Type Description Default
name str

Action name (e.g., "add_molecule")

required
**params Any

Action parameters (e.g., molecule="A", amount=5.0)

{}

Returns:

Type Description
Any

The new state after the action

Raises:

Type Description
KeyError

If action name is unknown

Source code in src/alienbio/bio/agent_interface.py
def act(self, name: str, **params: Any) -> Any:
    """Execute an action.

    For state-modifying actions (add_molecule, remove_molecule,
    set_concentration), the system's state is updated in place.

    Args:
        name: Action name (e.g., "add_molecule")
        **params: Action parameters (e.g., molecule="A", amount=5.0)

    Returns:
        The new state after the action

    Raises:
        KeyError: If action name is unknown
    """
    if name not in self._actions:
        raise KeyError(f"Unknown action: {name!r}")
    a = self._actions[name]
    new_state = a.apply(self._system.state, **params)
    self._system.state = new_state
    return new_state

describe()

Generate a text description of the interface for an agent.

Source code in src/alienbio/bio/agent_interface.py
def describe(self) -> str:
    """Generate a text description of the interface for an agent."""
    lines = ["Available measurements:"]
    for m in self.available_measurements():
        params_str = ", ".join(f"{k}: {v}" for k, v in m["params"].items())
        if params_str:
            lines.append(f"  - {m['name']}({params_str}): {m['description']}")
        else:
            lines.append(f"  - {m['name']}(): {m['description']}")

    lines.append("")
    lines.append("Available actions:")
    for a in self.available_actions():
        params_str = ", ".join(f"{k}: {v}" for k, v in a["params"].items())
        if params_str:
            lines.append(f"  - {a['name']}({params_str}): {a['description']}")
        else:
            lines.append(f"  - {a['name']}(): {a['description']}")

    return "\n".join(lines)

MeasurementSpec dataclass

Specification for a measurement type.

Source code in src/alienbio/bio/measurements.py
@dataclass
class MeasurementSpec:
    """Specification for a measurement type."""

    name: str
    description: str
    params: Dict[str, str]
    cost: float = 0.0

ConcentrationMeasurement

Measure the concentration of a molecule in the current state.

Source code in src/alienbio/bio/measurements.py
class ConcentrationMeasurement:
    """Measure the concentration of a molecule in the current state."""

    name = "concentration"
    description = "Measure the concentration of a specific molecule"
    params = {"molecule": "str"}

    def measure(self, state: StateImpl, molecule: str) -> float:
        """Return the concentration of the named molecule."""
        return state[molecule]

measure(state, molecule)

Return the concentration of the named molecule.

Source code in src/alienbio/bio/measurements.py
def measure(self, state: StateImpl, molecule: str) -> float:
    """Return the concentration of the named molecule."""
    return state[molecule]

AllConcentrationsMeasurement

Measure all concentrations in the current state.

Source code in src/alienbio/bio/measurements.py
class AllConcentrationsMeasurement:
    """Measure all concentrations in the current state."""

    name = "all_concentrations"
    description = "Measure all molecule concentrations"
    params: Dict[str, str] = {}

    def measure(self, state: StateImpl) -> Dict[str, float]:
        """Return all molecule concentrations as a dict."""
        return {name: state[name] for name in state}

measure(state)

Return all molecule concentrations as a dict.

Source code in src/alienbio/bio/measurements.py
def measure(self, state: StateImpl) -> Dict[str, float]:
    """Return all molecule concentrations as a dict."""
    return {name: state[name] for name in state}

RateMeasurement

Measure the effective rate of a reaction at the current state.

Source code in src/alienbio/bio/measurements.py
class RateMeasurement:
    """Measure the effective rate of a reaction at the current state."""

    name = "rate"
    description = "Measure the effective rate of a specific reaction"
    params = {"reaction": "str"}

    def measure(
        self,
        state: StateImpl,
        reaction_name: str,
    ) -> float:
        """Return the effective rate of the named reaction."""
        reaction = state.chemistry.reactions[reaction_name]
        return reaction.get_rate(state)

measure(state, reaction_name)

Return the effective rate of the named reaction.

Source code in src/alienbio/bio/measurements.py
def measure(
    self,
    state: StateImpl,
    reaction_name: str,
) -> float:
    """Return the effective rate of the named reaction."""
    reaction = state.chemistry.reactions[reaction_name]
    return reaction.get_rate(state)

MoleculeCountMeasurement

Measure the number of molecules in the chemistry.

Source code in src/alienbio/bio/measurements.py
class MoleculeCountMeasurement:
    """Measure the number of molecules in the chemistry."""

    name = "molecule_count"
    description = "Count the number of molecule species"
    params: Dict[str, str] = {}

    def measure(self, state: StateImpl) -> int:
        """Return the number of molecules."""
        return len(state.chemistry.molecules)

measure(state)

Return the number of molecules.

Source code in src/alienbio/bio/measurements.py
def measure(self, state: StateImpl) -> int:
    """Return the number of molecules."""
    return len(state.chemistry.molecules)

ReactionCountMeasurement

Measure the number of reactions in the chemistry.

Source code in src/alienbio/bio/measurements.py
class ReactionCountMeasurement:
    """Measure the number of reactions in the chemistry."""

    name = "reaction_count"
    description = "Count the number of reactions"
    params: Dict[str, str] = {}

    def measure(self, state: StateImpl) -> int:
        """Return the number of reactions."""
        return len(state.chemistry.reactions)

measure(state)

Return the number of reactions.

Source code in src/alienbio/bio/measurements.py
def measure(self, state: StateImpl) -> int:
    """Return the number of reactions."""
    return len(state.chemistry.reactions)

ActionSpec dataclass

Specification for an action type.

Source code in src/alienbio/bio/actions.py
@dataclass
class ActionSpec:
    """Specification for an action type."""

    name: str
    description: str
    params: Dict[str, str]
    cost: float = 1.0

AddMoleculeAction

Add a specified amount of a molecule to the state.

Source code in src/alienbio/bio/actions.py
class AddMoleculeAction:
    """Add a specified amount of a molecule to the state."""

    name = "add_molecule"
    description = "Add molecules to the system"
    params = {"molecule": "str", "amount": "float"}

    def apply(self, state: StateImpl, molecule: str, amount: float) -> StateImpl:
        """Add amount to the named molecule's concentration.

        Returns a new state (does not modify the original).
        """
        new_state = state.copy()
        current = new_state[molecule]
        new_state[molecule] = current + amount
        return new_state

apply(state, molecule, amount)

Add amount to the named molecule's concentration.

Returns a new state (does not modify the original).

Source code in src/alienbio/bio/actions.py
def apply(self, state: StateImpl, molecule: str, amount: float) -> StateImpl:
    """Add amount to the named molecule's concentration.

    Returns a new state (does not modify the original).
    """
    new_state = state.copy()
    current = new_state[molecule]
    new_state[molecule] = current + amount
    return new_state

RemoveMoleculeAction

Remove a specified amount of a molecule from the state.

Source code in src/alienbio/bio/actions.py
class RemoveMoleculeAction:
    """Remove a specified amount of a molecule from the state."""

    name = "remove_molecule"
    description = "Remove molecules from the system"
    params = {"molecule": "str", "amount": "float"}

    def apply(self, state: StateImpl, molecule: str, amount: float) -> StateImpl:
        """Remove amount from the named molecule's concentration.

        Clamps at zero. Returns a new state.
        """
        new_state = state.copy()
        current = new_state[molecule]
        new_state[molecule] = max(0.0, current - amount)
        return new_state

apply(state, molecule, amount)

Remove amount from the named molecule's concentration.

Clamps at zero. Returns a new state.

Source code in src/alienbio/bio/actions.py
def apply(self, state: StateImpl, molecule: str, amount: float) -> StateImpl:
    """Remove amount from the named molecule's concentration.

    Clamps at zero. Returns a new state.
    """
    new_state = state.copy()
    current = new_state[molecule]
    new_state[molecule] = max(0.0, current - amount)
    return new_state

SetConcentrationAction

Set a molecule's concentration to a specific value.

Source code in src/alienbio/bio/actions.py
class SetConcentrationAction:
    """Set a molecule's concentration to a specific value."""

    name = "set_concentration"
    description = "Set a molecule's concentration"
    params = {"molecule": "str", "value": "float"}

    def apply(self, state: StateImpl, molecule: str, value: float) -> StateImpl:
        """Set the named molecule to the specified concentration.

        Returns a new state.
        """
        new_state = state.copy()
        new_state[molecule] = value
        return new_state

apply(state, molecule, value)

Set the named molecule to the specified concentration.

Returns a new state.

Source code in src/alienbio/bio/actions.py
def apply(self, state: StateImpl, molecule: str, value: float) -> StateImpl:
    """Set the named molecule to the specified concentration.

    Returns a new state.
    """
    new_state = state.copy()
    new_state[molecule] = value
    return new_state

AdjustRateAction

Adjust a reaction's rate constant.

Source code in src/alienbio/bio/actions.py
class AdjustRateAction:
    """Adjust a reaction's rate constant."""

    name = "adjust_rate"
    description = "Adjust a reaction's rate"
    params = {"reaction": "str", "factor": "float"}

    def apply(
        self,
        state: StateImpl,
        reaction_name: str,
        factor: float,
    ) -> StateImpl:
        """Scale the named reaction's rate by factor.

        Modifies the reaction in-place (rates are shared across states).
        Returns the same state unchanged (rate is on the reaction, not the state).
        """
        reaction = state.chemistry.reactions[reaction_name]
        current_rate = reaction.rate
        if callable(current_rate):
            # For function rates, wrap with scaling factor
            original_fn = current_rate
            reaction.set_rate(lambda s, _fn=original_fn, _f=factor: _fn(s) * _f)
        else:
            reaction.set_rate(current_rate * factor)
        return state

apply(state, reaction_name, factor)

Scale the named reaction's rate by factor.

Modifies the reaction in-place (rates are shared across states). Returns the same state unchanged (rate is on the reaction, not the state).

Source code in src/alienbio/bio/actions.py
def apply(
    self,
    state: StateImpl,
    reaction_name: str,
    factor: float,
) -> StateImpl:
    """Scale the named reaction's rate by factor.

    Modifies the reaction in-place (rates are shared across states).
    Returns the same state unchanged (rate is on the reaction, not the state).
    """
    reaction = state.chemistry.reactions[reaction_name]
    current_rate = reaction.rate
    if callable(current_rate):
        # For function rates, wrap with scaling factor
        original_fn = current_rate
        reaction.set_rate(lambda s, _fn=original_fn, _f=factor: _fn(s) * _f)
    else:
        reaction.set_rate(current_rate * factor)
    return state

Task

Bases: ABC

Abstract base class for tasks.

A task defines: - A setup (preparing the system) - A goal description (what the agent should do) - Scoring criteria (how to evaluate the result)

Source code in src/alienbio/bio/task.py
class Task(ABC):
    """Abstract base class for tasks.

    A task defines:
    - A setup (preparing the system)
    - A goal description (what the agent should do)
    - Scoring criteria (how to evaluate the result)
    """

    @property
    @abstractmethod
    def name(self) -> str:
        """Short identifier for the task."""

    @property
    @abstractmethod
    def description(self) -> str:
        """Human-readable description of the goal."""

    @abstractmethod
    def score(self, interface: "AgentInterface", prediction: Any) -> TaskResult:
        """Score the agent's prediction or action.

        Args:
            interface: The agent interface (gives access to system)
            prediction: The agent's output

        Returns:
            TaskResult with score in [0, 1] and details dict
        """

name abstractmethod property

Short identifier for the task.

description abstractmethod property

Human-readable description of the goal.

score(interface, prediction) abstractmethod

Score the agent's prediction or action.

Parameters:

Name Type Description Default
interface 'AgentInterface'

The agent interface (gives access to system)

required
prediction Any

The agent's output

required

Returns:

Type Description
TaskResult

TaskResult with score in [0, 1] and details dict

Source code in src/alienbio/bio/task.py
@abstractmethod
def score(self, interface: "AgentInterface", prediction: Any) -> TaskResult:
    """Score the agent's prediction or action.

    Args:
        interface: The agent interface (gives access to system)
        prediction: The agent's output

    Returns:
        TaskResult with score in [0, 1] and details dict
    """

PredictTask

Bases: Task

Predict the concentration of a molecule after N simulation steps.

The agent must forecast what the concentration of a target molecule will be after the system runs for a specified number of steps.

Source code in src/alienbio/bio/task.py
class PredictTask(Task):
    """Predict the concentration of a molecule after N simulation steps.

    The agent must forecast what the concentration of a target molecule
    will be after the system runs for a specified number of steps.
    """

    def __init__(
        self,
        target_molecule: str,
        steps: int,
        *,
        tolerance: float = 0.1,
    ) -> None:
        self._target = target_molecule
        self._steps = steps
        self._tolerance = tolerance

    @property
    def name(self) -> str:
        return "predict"

    @property
    def description(self) -> str:
        return (
            f"Predict the concentration of {self._target!r} "
            f"after {self._steps} simulation steps"
        )

    @property
    def target_molecule(self) -> str:
        return self._target

    @property
    def steps(self) -> int:
        return self._steps

    def score(self, interface: "AgentInterface", prediction: float) -> TaskResult:
        """Score a concentration prediction.

        Runs the system forward, compares prediction to actual.
        Score = max(0, 1 - |predicted - actual| / max(actual, tolerance)).

        Args:
            interface: Agent interface wrapping the system
            prediction: Predicted concentration value

        Returns:
            TaskResult with score in [0, 1]
        """
        # Run forward from current state
        interface.system.run(self._steps)
        actual = interface.measure("concentration", molecule=self._target)

        # Score: relative error, clamped to [0, 1]
        denom = max(abs(actual), self._tolerance)
        error = abs(prediction - actual) / denom
        task_score = max(0.0, 1.0 - error)

        return TaskResult(
            score=task_score,
            details={
                "predicted": prediction,
                "actual": actual,
                "error": abs(prediction - actual),
                "steps": self._steps,
                "target": self._target,
            },
        )

score(interface, prediction)

Score a concentration prediction.

Runs the system forward, compares prediction to actual. Score = max(0, 1 - |predicted - actual| / max(actual, tolerance)).

Parameters:

Name Type Description Default
interface 'AgentInterface'

Agent interface wrapping the system

required
prediction float

Predicted concentration value

required

Returns:

Type Description
TaskResult

TaskResult with score in [0, 1]

Source code in src/alienbio/bio/task.py
def score(self, interface: "AgentInterface", prediction: float) -> TaskResult:
    """Score a concentration prediction.

    Runs the system forward, compares prediction to actual.
    Score = max(0, 1 - |predicted - actual| / max(actual, tolerance)).

    Args:
        interface: Agent interface wrapping the system
        prediction: Predicted concentration value

    Returns:
        TaskResult with score in [0, 1]
    """
    # Run forward from current state
    interface.system.run(self._steps)
    actual = interface.measure("concentration", molecule=self._target)

    # Score: relative error, clamped to [0, 1]
    denom = max(abs(actual), self._tolerance)
    error = abs(prediction - actual) / denom
    task_score = max(0.0, 1.0 - error)

    return TaskResult(
        score=task_score,
        details={
            "predicted": prediction,
            "actual": actual,
            "error": abs(prediction - actual),
            "steps": self._steps,
            "target": self._target,
        },
    )

TaskResult dataclass

Result of evaluating an agent on a task.

Source code in src/alienbio/bio/task.py
@dataclass
class TaskResult:
    """Result of evaluating an agent on a task."""

    score: float
    details: Dict[str, Any]

ExperimentResult dataclass

Result of running an experiment.

Source code in src/alienbio/bio/experiment.py
@dataclass
class ExperimentResult:
    """Result of running an experiment."""

    task_name: str
    score: float
    prediction: Any
    details: Dict[str, Any]

DiagnoseTask

Bases: Task

Identify which perturbation was applied to a diseased system.

The agent receives a diseased system and a list of candidate perturbations. It must identify which perturbation was actually applied.

Source code in src/alienbio/bio/diagnosis.py
class DiagnoseTask(Task):
    """Identify which perturbation was applied to a diseased system.

    The agent receives a diseased system and a list of candidate perturbations.
    It must identify which perturbation was actually applied.
    """

    def __init__(
        self,
        candidates: List["Perturbation"],
        applied_index: int,
    ) -> None:
        self._candidates = candidates
        self._applied_index = applied_index

    @property
    def name(self) -> str:
        return "diagnose"

    @property
    def description(self) -> str:
        n = len(self._candidates)
        return f"Identify which of {n} perturbations was applied to the system"

    @property
    def candidates(self) -> List["Perturbation"]:
        return list(self._candidates)

    @property
    def num_candidates(self) -> int:
        return len(self._candidates)

    @property
    def correct_index(self) -> int:
        return self._applied_index

    def score(self, interface: "AgentInterface", prediction: int) -> TaskResult:
        """Score a diagnosis prediction.

        Args:
            interface: Agent interface (not used for scoring, but part of protocol)
            prediction: Index into candidates list

        Returns:
            TaskResult with score 1.0 if correct, 0.0 if wrong
        """
        correct = prediction == self._applied_index
        return TaskResult(
            score=1.0 if correct else 0.0,
            details={
                "predicted_index": prediction,
                "correct_index": self._applied_index,
                "correct": correct,
                "num_candidates": len(self._candidates),
            },
        )

score(interface, prediction)

Score a diagnosis prediction.

Parameters:

Name Type Description Default
interface 'AgentInterface'

Agent interface (not used for scoring, but part of protocol)

required
prediction int

Index into candidates list

required

Returns:

Type Description
TaskResult

TaskResult with score 1.0 if correct, 0.0 if wrong

Source code in src/alienbio/bio/diagnosis.py
def score(self, interface: "AgentInterface", prediction: int) -> TaskResult:
    """Score a diagnosis prediction.

    Args:
        interface: Agent interface (not used for scoring, but part of protocol)
        prediction: Index into candidates list

    Returns:
        TaskResult with score 1.0 if correct, 0.0 if wrong
    """
    correct = prediction == self._applied_index
    return TaskResult(
        score=1.0 if correct else 0.0,
        details={
            "predicted_index": prediction,
            "correct_index": self._applied_index,
            "correct": correct,
            "num_candidates": len(self._candidates),
        },
    )

CureTask

Bases: Task

Restore a diseased system to healthy range.

The agent receives a diseased system and must use actions to bring concentrations back within the healthy baseline ranges.

Source code in src/alienbio/bio/diagnosis.py
class CureTask(Task):
    """Restore a diseased system to healthy range.

    The agent receives a diseased system and must use actions to
    bring concentrations back within the healthy baseline ranges.
    """

    def __init__(
        self,
        baseline: "Baseline",
        *,
        recovery_steps: int = 200,
    ) -> None:
        self._baseline = baseline
        self._recovery_steps = recovery_steps

    @property
    def name(self) -> str:
        return "cure"

    @property
    def description(self) -> str:
        return "Restore the diseased system to healthy concentration ranges"

    @property
    def baseline(self) -> "Baseline":
        return self._baseline

    @property
    def recovery_steps(self) -> int:
        return self._recovery_steps

    def score(self, interface: "AgentInterface", prediction: Any = None) -> TaskResult:
        """Score cure attempt by checking if system is healthy after recovery.

        The agent should have already applied actions before score() is called.
        This method runs the system forward to allow actions to take effect,
        then checks whether concentrations are within healthy ranges.

        Args:
            interface: Agent interface wrapping the system
            prediction: Ignored (actions are applied directly)

        Returns:
            TaskResult with score based on fraction of molecules in range
        """
        # Run forward to let cure take effect
        interface.system.run(self._recovery_steps)

        concentrations = {
            name: interface.system.state[name]
            for name in interface.system.chemistry.molecules
        }

        # Score: fraction of molecules within healthy range
        in_range = 0
        total = len(self._baseline.ranges)
        details_per_mol: Dict[str, Any] = {}

        for r in self._baseline.ranges:
            val = concentrations.get(r.molecule, 0.0)
            healthy = r.contains(val)
            if healthy:
                in_range += 1
            details_per_mol[r.molecule] = {
                "value": val,
                "low": r.low,
                "high": r.high,
                "in_range": healthy,
            }

        cure_score = in_range / total if total > 0 else 1.0

        return TaskResult(
            score=cure_score,
            details={
                "molecules": details_per_mol,
                "in_range": in_range,
                "total": total,
            },
        )

score(interface, prediction=None)

Score cure attempt by checking if system is healthy after recovery.

The agent should have already applied actions before score() is called. This method runs the system forward to allow actions to take effect, then checks whether concentrations are within healthy ranges.

Parameters:

Name Type Description Default
interface 'AgentInterface'

Agent interface wrapping the system

required
prediction Any

Ignored (actions are applied directly)

None

Returns:

Type Description
TaskResult

TaskResult with score based on fraction of molecules in range

Source code in src/alienbio/bio/diagnosis.py
def score(self, interface: "AgentInterface", prediction: Any = None) -> TaskResult:
    """Score cure attempt by checking if system is healthy after recovery.

    The agent should have already applied actions before score() is called.
    This method runs the system forward to allow actions to take effect,
    then checks whether concentrations are within healthy ranges.

    Args:
        interface: Agent interface wrapping the system
        prediction: Ignored (actions are applied directly)

    Returns:
        TaskResult with score based on fraction of molecules in range
    """
    # Run forward to let cure take effect
    interface.system.run(self._recovery_steps)

    concentrations = {
        name: interface.system.state[name]
        for name in interface.system.chemistry.molecules
    }

    # Score: fraction of molecules within healthy range
    in_range = 0
    total = len(self._baseline.ranges)
    details_per_mol: Dict[str, Any] = {}

    for r in self._baseline.ranges:
        val = concentrations.get(r.molecule, 0.0)
        healthy = r.contains(val)
        if healthy:
            in_range += 1
        details_per_mol[r.molecule] = {
            "value": val,
            "low": r.low,
            "high": r.high,
            "in_range": healthy,
        }

    cure_score = in_range / total if total > 0 else 1.0

    return TaskResult(
        score=cure_score,
        details={
            "molecules": details_per_mol,
            "in_range": in_range,
            "total": total,
        },
    )

TestSuite dataclass

A batch of experiments to run.

Pairs tasks with agent interfaces for batch execution.

Source code in src/alienbio/bio/harness.py
@dataclass
class TestSuite:
    """A batch of experiments to run.

    Pairs tasks with agent interfaces for batch execution.
    """

    name: str
    experiments: List[_ExperimentSpec] = field(default_factory=list)

    def add(self, interface: "AgentInterface", task: "Task") -> None:
        """Add an experiment to the suite."""
        self.experiments.append(_ExperimentSpec(interface, task))

    @property
    def count(self) -> int:
        return len(self.experiments)

add(interface, task)

Add an experiment to the suite.

Source code in src/alienbio/bio/harness.py
def add(self, interface: "AgentInterface", task: "Task") -> None:
    """Add an experiment to the suite."""
    self.experiments.append(_ExperimentSpec(interface, task))

TestResults dataclass

Aggregated results from running a test suite.

Source code in src/alienbio/bio/harness.py
@dataclass
class TestResults:
    """Aggregated results from running a test suite."""

    suite_name: str
    results: List[ExperimentResult]

    @property
    def count(self) -> int:
        return len(self.results)

    @property
    def scores(self) -> List[float]:
        return [r.score for r in self.results]

    @property
    def mean_score(self) -> float:
        if not self.results:
            return 0.0
        return sum(self.scores) / len(self.scores)

    @property
    def pass_rate(self, threshold: float = 0.5) -> float:
        """Fraction of experiments scoring above threshold."""
        if not self.results:
            return 0.0
        passed = sum(1 for s in self.scores if s >= threshold)
        return passed / len(self.scores)

    def scores_by_task(self) -> Dict[str, List[float]]:
        """Group scores by task name."""
        by_task: Dict[str, List[float]] = {}
        for r in self.results:
            by_task.setdefault(r.task_name, []).append(r.score)
        return by_task

    def to_dict(self) -> Dict[str, Any]:
        """Export results as a serializable dict."""
        return {
            "suite_name": self.suite_name,
            "count": self.count,
            "mean_score": self.mean_score,
            "results": [
                {
                    "task_name": r.task_name,
                    "score": r.score,
                    "prediction": _safe_serialize(r.prediction),
                    "details": r.details,
                }
                for r in self.results
            ],
        }

    def to_json(self) -> str:
        """Export results as JSON string."""
        return json.dumps(self.to_dict(), indent=2, default=str)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "TestResults":
        """Import results from a dict."""
        results = [
            ExperimentResult(
                task_name=r["task_name"],
                score=r["score"],
                prediction=r["prediction"],
                details=r["details"],
            )
            for r in data["results"]
        ]
        return cls(suite_name=data["suite_name"], results=results)

    @classmethod
    def from_json(cls, json_str: str) -> "TestResults":
        """Import results from JSON string."""
        return cls.from_dict(json.loads(json_str))

pass_rate property

Fraction of experiments scoring above threshold.

scores_by_task()

Group scores by task name.

Source code in src/alienbio/bio/harness.py
def scores_by_task(self) -> Dict[str, List[float]]:
    """Group scores by task name."""
    by_task: Dict[str, List[float]] = {}
    for r in self.results:
        by_task.setdefault(r.task_name, []).append(r.score)
    return by_task

to_dict()

Export results as a serializable dict.

Source code in src/alienbio/bio/harness.py
def to_dict(self) -> Dict[str, Any]:
    """Export results as a serializable dict."""
    return {
        "suite_name": self.suite_name,
        "count": self.count,
        "mean_score": self.mean_score,
        "results": [
            {
                "task_name": r.task_name,
                "score": r.score,
                "prediction": _safe_serialize(r.prediction),
                "details": r.details,
            }
            for r in self.results
        ],
    }

to_json()

Export results as JSON string.

Source code in src/alienbio/bio/harness.py
def to_json(self) -> str:
    """Export results as JSON string."""
    return json.dumps(self.to_dict(), indent=2, default=str)

from_dict(data) classmethod

Import results from a dict.

Source code in src/alienbio/bio/harness.py
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TestResults":
    """Import results from a dict."""
    results = [
        ExperimentResult(
            task_name=r["task_name"],
            score=r["score"],
            prediction=r["prediction"],
            details=r["details"],
        )
        for r in data["results"]
    ]
    return cls(suite_name=data["suite_name"], results=results)

from_json(json_str) classmethod

Import results from JSON string.

Source code in src/alienbio/bio/harness.py
@classmethod
def from_json(cls, json_str: str) -> "TestResults":
    """Import results from JSON string."""
    return cls.from_dict(json.loads(json_str))

AgentStats dataclass

Statistical summary for one agent.

Source code in src/alienbio/bio/comparison.py
@dataclass
class AgentStats:
    """Statistical summary for one agent."""

    agent_name: str
    mean: float
    std: float
    min: float
    max: float
    count: int
    pass_rate: float

ComparisonTable dataclass

Comparison of multiple agents.

Source code in src/alienbio/bio/comparison.py
@dataclass
class ComparisonTable:
    """Comparison of multiple agents."""

    agents: List[AgentStats]

    @property
    def ranking(self) -> List[AgentStats]:
        """Agents sorted by mean score, descending."""
        return sorted(self.agents, key=lambda a: a.mean, reverse=True)

    def leader(self) -> AgentStats:
        """Top-ranked agent."""
        return self.ranking[0]

    def to_dict(self) -> Dict[str, Any]:
        """Export as serializable dict."""
        return {
            "agents": [
                {
                    "agent_name": a.agent_name,
                    "mean": a.mean,
                    "std": a.std,
                    "min": a.min,
                    "max": a.max,
                    "count": a.count,
                    "pass_rate": a.pass_rate,
                }
                for a in self.ranking
            ],
        }

ranking property

Agents sorted by mean score, descending.

leader()

Top-ranked agent.

Source code in src/alienbio/bio/comparison.py
def leader(self) -> AgentStats:
    """Top-ranked agent."""
    return self.ranking[0]

to_dict()

Export as serializable dict.

Source code in src/alienbio/bio/comparison.py
def to_dict(self) -> Dict[str, Any]:
    """Export as serializable dict."""
    return {
        "agents": [
            {
                "agent_name": a.agent_name,
                "mean": a.mean,
                "std": a.std,
                "min": a.min,
                "max": a.max,
                "count": a.count,
                "pass_rate": a.pass_rate,
            }
            for a in self.ranking
        ],
    }

QuiescenceTimeout

Bases: Exception

Raised when quiescence is not reached within the timeout.

Source code in src/alienbio/bio/quiescence.py
class QuiescenceTimeout(Exception):
    """Raised when quiescence is not reached within the timeout."""

HealthRange dataclass

Acceptable range for a molecule concentration.

Source code in src/alienbio/bio/disease.py
@dataclass
class HealthRange:
    """Acceptable range for a molecule concentration."""

    molecule: str
    low: float
    high: float

    def contains(self, value: float) -> bool:
        return self.low <= value <= self.high

Baseline dataclass

Healthy baseline for a biological system.

Defines steady-state concentrations and acceptable ranges.

Source code in src/alienbio/bio/disease.py
@dataclass
class Baseline:
    """Healthy baseline for a biological system.

    Defines steady-state concentrations and acceptable ranges.
    """

    steady_state: Dict[str, float]
    ranges: List[HealthRange]

    def is_healthy(self, concentrations: Dict[str, float]) -> bool:
        """Check if all concentrations are within healthy ranges."""
        for r in self.ranges:
            val = concentrations.get(r.molecule, 0.0)
            if not r.contains(val):
                return False
        return True

is_healthy(concentrations)

Check if all concentrations are within healthy ranges.

Source code in src/alienbio/bio/disease.py
def is_healthy(self, concentrations: Dict[str, float]) -> bool:
    """Check if all concentrations are within healthy ranges."""
    for r in self.ranges:
        val = concentrations.get(r.molecule, 0.0)
        if not r.contains(val):
            return False
    return True

Perturbation dataclass

A perturbation that creates disease from a healthy system.

Can alter reaction rates or remove reactions entirely.

Source code in src/alienbio/bio/disease.py
@dataclass
class Perturbation:
    """A perturbation that creates disease from a healthy system.

    Can alter reaction rates or remove reactions entirely.
    """

    name: str
    kind: str  # "rate_change" or "reaction_removal"
    target_reaction: str
    factor: Optional[float] = None  # for rate_change: multiply rate by this

    def apply(self, system: "BioSystem") -> None:
        """Apply perturbation to a system (modifies in-place)."""
        if self.kind == "reaction_removal":
            # Set rate to zero effectively
            rxn = system.chemistry.reactions[self.target_reaction]
            rxn.set_rate(0.0)
        elif self.kind == "rate_change":
            rxn = system.chemistry.reactions[self.target_reaction]
            current = rxn.rate
            factor = self.factor if self.factor is not None else 0.1
            if callable(current):
                orig = current
                rxn.set_rate(lambda s, _fn=orig, _f=factor: _fn(s) * _f)
            else:
                rxn.set_rate(current * factor)
        else:
            raise ValueError(f"Unknown perturbation kind: {self.kind!r}")

apply(system)

Apply perturbation to a system (modifies in-place).

Source code in src/alienbio/bio/disease.py
def apply(self, system: "BioSystem") -> None:
    """Apply perturbation to a system (modifies in-place)."""
    if self.kind == "reaction_removal":
        # Set rate to zero effectively
        rxn = system.chemistry.reactions[self.target_reaction]
        rxn.set_rate(0.0)
    elif self.kind == "rate_change":
        rxn = system.chemistry.reactions[self.target_reaction]
        current = rxn.rate
        factor = self.factor if self.factor is not None else 0.1
        if callable(current):
            orig = current
            rxn.set_rate(lambda s, _fn=orig, _f=factor: _fn(s) * _f)
        else:
            rxn.set_rate(current * factor)
    else:
        raise ValueError(f"Unknown perturbation kind: {self.kind!r}")

Symptom dataclass

An observable symptom — a measurement outside healthy range.

Source code in src/alienbio/bio/disease.py
@dataclass
class Symptom:
    """An observable symptom — a measurement outside healthy range."""

    molecule: str
    value: float
    healthy_range: HealthRange
    deviation: float  # how far outside range

OrganSpec dataclass

Specification for an organ (compartment with reactions).

Source code in src/alienbio/bio/organ_generator.py
@dataclass
class OrganSpec:
    """Specification for an organ (compartment with reactions)."""

    name: str
    reactions: List[str]  # reaction names active in this organ
    initial_concentrations: Dict[int, float]  # mol_id -> concentration

Transport link between two compartments.

Source code in src/alienbio/bio/organ_generator.py
@dataclass
class TransportLink:
    """Transport link between two compartments."""

    source: CompartmentId
    target: CompartmentId
    molecule_id: int
    rate: float

Organism dataclass

A generated multi-compartment organism.

Source code in src/alienbio/bio/organ_generator.py
@dataclass
class Organism:
    """A generated multi-compartment organism."""

    tree: CompartmentTreeImpl
    state: WorldStateImpl
    simulator: WorldSimulatorImpl
    transport_links: List[TransportLink]

    @property
    def num_compartments(self) -> int:
        return self.tree.num_compartments

    @property
    def num_transport_links(self) -> int:
        return len(self.transport_links)

PerturbationResult dataclass

Result of a perturbation experiment.

Source code in src/alienbio/bio/perturbation.py
@dataclass
class PerturbationResult:
    """Result of a perturbation experiment."""

    recovered: bool
    baseline_final: Dict[str, float]
    perturbed_final: Dict[str, float]
    max_deviation: float
    recovery_step: Optional[int]
    steps_run: int

DriftResult dataclass

Result of a reaction removal drift experiment.

Source code in src/alienbio/bio/perturbation.py
@dataclass
class DriftResult:
    """Result of a reaction removal drift experiment."""

    drifted: bool
    baseline_final: Dict[str, float]
    modified_final: Dict[str, float]
    drift_per_molecule: Dict[str, float]
    max_drift: float
    steps_run: int

get_atom(symbol)

Get an atom by its symbol.

Parameters:

Name Type Description Default
symbol str

Chemical symbol (e.g., 'C', 'H', 'Na')

required

Returns:

Type Description
AtomImpl

The AtomImpl for that element

Raises:

Type Description
KeyError

If the symbol is not in COMMON_ATOMS

Source code in src/alienbio/bio/atom.py
def get_atom(symbol: str) -> AtomImpl:
    """Get an atom by its symbol.

    Args:
        symbol: Chemical symbol (e.g., 'C', 'H', 'Na')

    Returns:
        The AtomImpl for that element

    Raises:
        KeyError: If the symbol is not in COMMON_ATOMS
    """
    if symbol not in COMMON_ATOMS:
        raise KeyError(f"Unknown atom symbol: {symbol!r}")
    return COMMON_ATOMS[symbol]

compute_variance(timeline, window)

Compute variance of each molecule over the last window steps.

Parameters:

Name Type Description Default
timeline List[StateImpl]

List of states (length >= window)

required
window int

Number of trailing steps to analyze

required

Returns:

Type Description
Dict[str, float]

Dict mapping molecule name to variance of concentration

Source code in src/alienbio/bio/equilibrium.py
def compute_variance(timeline: List[StateImpl], window: int) -> Dict[str, float]:
    """Compute variance of each molecule over the last `window` steps.

    Args:
        timeline: List of states (length >= window)
        window: Number of trailing steps to analyze

    Returns:
        Dict mapping molecule name to variance of concentration
    """
    if len(timeline) < window:
        window = len(timeline)
    if window == 0:
        return {}

    tail = timeline[-window:]
    molecules = list(tail[0])
    result: Dict[str, float] = {}

    for mol in molecules:
        values = [s[mol] for s in tail]
        mean = sum(values) / len(values)
        var = sum((v - mean) ** 2 for v in values) / len(values)
        result[mol] = var

    return result

check_stability(timeline, window=100, threshold=0.0001)

Check whether a simulation has reached equilibrium.

Computes per-molecule variance over the trailing window. The system is considered stable when all variances are below the threshold.

Parameters:

Name Type Description Default
timeline List[StateImpl]

Full simulation timeline

required
window int

Number of trailing steps to check

100
threshold float

Maximum allowed variance per molecule

0.0001

Returns:

Type Description
StabilityResult

StabilityResult with per-molecule variance and stability flag

Source code in src/alienbio/bio/equilibrium.py
def check_stability(
    timeline: List[StateImpl],
    window: int = 100,
    threshold: float = 1e-4,
) -> StabilityResult:
    """Check whether a simulation has reached equilibrium.

    Computes per-molecule variance over the trailing window. The system
    is considered stable when all variances are below the threshold.

    Args:
        timeline: Full simulation timeline
        window: Number of trailing steps to check
        threshold: Maximum allowed variance per molecule

    Returns:
        StabilityResult with per-molecule variance and stability flag
    """
    variance = compute_variance(timeline, window)
    unstable = [mol for mol, var in variance.items() if var >= threshold]
    max_var = max(variance.values()) if variance else 0.0

    return StabilityResult(
        stable=len(unstable) == 0,
        variance=variance,
        max_variance=max_var,
        unstable_molecules=unstable,
        steps_run=len(timeline) - 1,
        window=min(window, len(timeline)),
    )

run_to_equilibrium(system, max_steps=10000, window=100, threshold=0.0001, check_interval=100)

Run a BioSystem until equilibrium is reached or max_steps exceeded.

Runs in chunks of check_interval steps, checking stability after each chunk. Returns the full timeline and the final stability result.

Parameters:

Name Type Description Default
system 'BioSystem'

BioSystem to simulate

required
max_steps int

Maximum total steps to run

10000
window int

Trailing window for variance check

100
threshold float

Variance threshold for stability

0.0001
check_interval int

Steps between stability checks

100

Returns:

Type Description
tuple[List[StateImpl], StabilityResult]

Tuple of (timeline, StabilityResult)

Source code in src/alienbio/bio/equilibrium.py
def run_to_equilibrium(
    system: "BioSystem",
    max_steps: int = 10000,
    window: int = 100,
    threshold: float = 1e-4,
    check_interval: int = 100,
) -> tuple[List[StateImpl], StabilityResult]:
    """Run a BioSystem until equilibrium is reached or max_steps exceeded.

    Runs in chunks of `check_interval` steps, checking stability after each
    chunk. Returns the full timeline and the final stability result.

    Args:
        system: BioSystem to simulate
        max_steps: Maximum total steps to run
        window: Trailing window for variance check
        threshold: Variance threshold for stability
        check_interval: Steps between stability checks

    Returns:
        Tuple of (timeline, StabilityResult)
    """
    timeline: List[StateImpl] = [system.state.copy()]
    steps_done = 0

    while steps_done < max_steps:
        chunk = min(check_interval, max_steps - steps_done)
        chunk_timeline = system.simulator.run(system.state, chunk)
        # chunk_timeline[0] is current state (duplicate), skip it
        timeline.extend(chunk_timeline[1:])
        system.state = chunk_timeline[-1].copy()
        steps_done += chunk

        if steps_done >= window:
            result = check_stability(timeline, window, threshold)
            if result.stable:
                return timeline, result

    return timeline, check_stability(timeline, window, threshold)

find_unstable_rates(system, steps=1000, window=100, threshold=0.0001)

Identify reactions whose rates may cause instability.

Runs the system, then for each reaction, checks if disabling it reduces the maximum variance. Returns reactions that contribute most to instability, mapped to their rate values.

Parameters:

Name Type Description Default
system 'BioSystem'

BioSystem to analyze

required
steps int

Steps to run for each test

1000
window int

Trailing window for stability check

100
threshold float

Variance threshold

0.0001

Returns:

Type Description
Dict[str, float]

Dict mapping reaction name to its rate constant for reactions

Dict[str, float]

that contribute to instability

Source code in src/alienbio/bio/equilibrium.py
def find_unstable_rates(
    system: "BioSystem",
    steps: int = 1000,
    window: int = 100,
    threshold: float = 1e-4,
) -> Dict[str, float]:
    """Identify reactions whose rates may cause instability.

    Runs the system, then for each reaction, checks if disabling it
    reduces the maximum variance. Returns reactions that contribute
    most to instability, mapped to their rate values.

    Args:
        system: BioSystem to analyze
        steps: Steps to run for each test
        window: Trailing window for stability check
        threshold: Variance threshold

    Returns:
        Dict mapping reaction name to its rate constant for reactions
        that contribute to instability
    """
    from .biosystem import BioSystem as _BioSystem

    # Baseline run
    baseline_sys = _BioSystem(
        system.chemistry, system.state.copy(),
        dt=system.simulator.dt,
    )
    baseline_timeline = baseline_sys.run(steps)
    baseline_result = check_stability(baseline_timeline, window, threshold)

    if baseline_result.stable:
        return {}

    unstable_rates: Dict[str, float] = {}

    for rxn_name, reaction in system.chemistry.reactions.items():
        rate = reaction.get_rate(system.state)
        if rate > 0:
            unstable_rates[rxn_name] = rate

    return unstable_rates

check_homeostasis(state, targets)

Check which homeostasis targets are met in the given state.

Parameters:

Name Type Description Default
state StateImpl

Current system state

required
targets List[HomeostasisTarget]

List of homeostasis targets to check

required

Returns:

Type Description
Dict[str, bool]

Dict mapping molecule name to whether target is met

Source code in src/alienbio/bio/equilibrium.py
def check_homeostasis(
    state: StateImpl,
    targets: List[HomeostasisTarget],
) -> Dict[str, bool]:
    """Check which homeostasis targets are met in the given state.

    Args:
        state: Current system state
        targets: List of homeostasis targets to check

    Returns:
        Dict mapping molecule name to whether target is met
    """
    return {t.molecule: t.check(state[t.molecule]) for t in targets}

run_experiment(interface, task, agent)

Run an experiment: have the agent attempt the task.

Parameters:

Name Type Description Default
interface 'AgentInterface'

Agent-facing API for the system

required
task 'Task'

The task to evaluate

required
agent AgentFn

A callable (interface, task) -> prediction

required

Returns:

Type Description
ExperimentResult

ExperimentResult with score and details

Source code in src/alienbio/bio/experiment.py
def run_experiment(
    interface: "AgentInterface",
    task: "Task",
    agent: AgentFn,
) -> ExperimentResult:
    """Run an experiment: have the agent attempt the task.

    Args:
        interface: Agent-facing API for the system
        task: The task to evaluate
        agent: A callable (interface, task) -> prediction

    Returns:
        ExperimentResult with score and details
    """
    prediction = agent(interface, task)
    result: TaskResult = task.score(interface, prediction)

    return ExperimentResult(
        task_name=task.name,
        score=result.score,
        prediction=prediction,
        details=result.details,
    )

generate_diagnosis_task(system, perturbations, *, difficulty=1, seed=None)

Generate a diagnosis task at a given difficulty level.

Difficulty controls the number of candidate perturbations: - difficulty=1: 2 candidates (easy — binary choice) - difficulty=2: 4 candidates - difficulty=N: min(2*N, len(perturbations)) candidates

Parameters:

Name Type Description Default
system 'BioSystem'

The biological system

required
perturbations List['Perturbation']

Pool of all possible perturbations

required
difficulty int

Difficulty level (1 = easiest)

1
seed Optional[int]

Random seed for reproducibility

None

Returns:

Type Description
'DiagnoseTask'

DiagnoseTask with appropriate number of candidates

Source code in src/alienbio/bio/difficulty.py
def generate_diagnosis_task(
    system: "BioSystem",
    perturbations: List["Perturbation"],
    *,
    difficulty: int = 1,
    seed: Optional[int] = None,
) -> "DiagnoseTask":
    """Generate a diagnosis task at a given difficulty level.

    Difficulty controls the number of candidate perturbations:
    - difficulty=1: 2 candidates (easy — binary choice)
    - difficulty=2: 4 candidates
    - difficulty=N: min(2*N, len(perturbations)) candidates

    Args:
        system: The biological system
        perturbations: Pool of all possible perturbations
        difficulty: Difficulty level (1 = easiest)
        seed: Random seed for reproducibility

    Returns:
        DiagnoseTask with appropriate number of candidates
    """
    from .diagnosis import DiagnoseTask

    rng = random.Random(seed)

    num_candidates = min(2 * difficulty, len(perturbations))
    num_candidates = max(2, num_candidates)  # at least 2

    candidates = rng.sample(perturbations, num_candidates)
    applied_index = rng.randrange(len(candidates))

    return DiagnoseTask(candidates, applied_index=applied_index)

run_suite(suite, agent)

Run all experiments in a test suite.

Parameters:

Name Type Description Default
suite TestSuite

The test suite to run

required
agent AgentFn

Agent function to evaluate

required

Returns:

Type Description
TestResults

TestResults with all scores

Source code in src/alienbio/bio/harness.py
def run_suite(
    suite: TestSuite,
    agent: AgentFn,
) -> TestResults:
    """Run all experiments in a test suite.

    Args:
        suite: The test suite to run
        agent: Agent function to evaluate

    Returns:
        TestResults with all scores
    """
    results = []
    for spec in suite.experiments:
        result = run_experiment(spec.interface, spec.task, agent)
        results.append(result)

    return TestResults(suite_name=suite.name, results=results)

compare(results, *, threshold=0.5)

Compare multiple agents' results.

Parameters:

Name Type Description Default
results Dict[str, TestResults]

Dict mapping agent_name -> TestResults

required
threshold float

Score threshold for pass/fail (default 0.5)

0.5

Returns:

Type Description
ComparisonTable

ComparisonTable with ranked agent statistics

Source code in src/alienbio/bio/comparison.py
def compare(
    results: Dict[str, TestResults],
    *,
    threshold: float = 0.5,
) -> ComparisonTable:
    """Compare multiple agents' results.

    Args:
        results: Dict mapping agent_name -> TestResults
        threshold: Score threshold for pass/fail (default 0.5)

    Returns:
        ComparisonTable with ranked agent statistics
    """
    agents = []
    for agent_name, test_results in results.items():
        stats = _compute_stats(agent_name, test_results.scores, threshold)
        agents.append(stats)

    return ComparisonTable(agents=agents)

compare_by_task(results, *, threshold=0.5)

Compare agents grouped by task type.

Parameters:

Name Type Description Default
results Dict[str, TestResults]

Dict mapping agent_name -> TestResults

required
threshold float

Score threshold for pass/fail

0.5

Returns:

Type Description
Dict[str, ComparisonTable]

Dict mapping task_name -> ComparisonTable

Source code in src/alienbio/bio/comparison.py
def compare_by_task(
    results: Dict[str, TestResults],
    *,
    threshold: float = 0.5,
) -> Dict[str, ComparisonTable]:
    """Compare agents grouped by task type.

    Args:
        results: Dict mapping agent_name -> TestResults
        threshold: Score threshold for pass/fail

    Returns:
        Dict mapping task_name -> ComparisonTable
    """
    # Collect scores by (task, agent)
    task_scores: Dict[str, Dict[str, List[float]]] = {}
    for agent_name, test_results in results.items():
        by_task = test_results.scores_by_task()
        for task_name, scores in by_task.items():
            if task_name not in task_scores:
                task_scores[task_name] = {}
            task_scores[task_name][agent_name] = scores

    # Build comparison table per task
    tables: Dict[str, ComparisonTable] = {}
    for task_name, agent_scores in task_scores.items():
        agents = []
        for agent_name, scores in agent_scores.items():
            stats = _compute_stats(agent_name, scores, threshold)
            agents.append(stats)
        tables[task_name] = ComparisonTable(agents=agents)

    return tables

run_until_quiet(system, *, measure='all_concentrations', measure_params=None, delta=0.01, span=50, timeout=10000)

Run simulation until a measurement stabilizes.

Runs the system step by step, checking whether the measurement has changed by less than delta over the last span consecutive steps.

Parameters:

Name Type Description Default
system 'BioSystem'

The biological system to simulate

required
measure str

Measurement name (via AgentInterface)

'all_concentrations'
measure_params Optional[Dict[str, Any]]

Parameters for the measurement

None
delta float

Maximum allowed change for stability

0.01
span int

Number of consecutive stable steps required

50
timeout int

Maximum steps before raising QuiescenceTimeout

10000

Returns:

Type Description
int

Number of steps taken to reach quiescence

Raises:

Type Description
QuiescenceTimeout

If quiescence not reached within timeout

Source code in src/alienbio/bio/quiescence.py
def run_until_quiet(
    system: "BioSystem",
    *,
    measure: str = "all_concentrations",
    measure_params: Optional[Dict[str, Any]] = None,
    delta: float = 0.01,
    span: int = 50,
    timeout: int = 10000,
) -> int:
    """Run simulation until a measurement stabilizes.

    Runs the system step by step, checking whether the measurement
    has changed by less than `delta` over the last `span` consecutive steps.

    Args:
        system: The biological system to simulate
        measure: Measurement name (via AgentInterface)
        measure_params: Parameters for the measurement
        delta: Maximum allowed change for stability
        span: Number of consecutive stable steps required
        timeout: Maximum steps before raising QuiescenceTimeout

    Returns:
        Number of steps taken to reach quiescence

    Raises:
        QuiescenceTimeout: If quiescence not reached within timeout
    """
    from .agent_interface import AgentInterface

    iface = AgentInterface(system)
    params = measure_params or {}

    prev_value = iface.measure(measure, **params)
    stable_count = 0

    for step in range(1, timeout + 1):
        system.step()
        current_value = iface.measure(measure, **params)

        change = _measure_change(prev_value, current_value)
        if change <= delta:
            stable_count += 1
        else:
            stable_count = 0

        if stable_count >= span:
            return step

        prev_value = current_value

    raise QuiescenceTimeout(
        f"Quiescence not reached after {timeout} steps "
        f"(delta={delta}, span={span})"
    )

generate_alien_name(base, *, seed=None)

Generate an opaque alien name from a base string.

The name is deterministic given the same base and seed.

Parameters:

Name Type Description Default
base str

The original name to obfuscate

required
seed Optional[int]

Random seed (defaults to hash of base)

None

Returns:

Type Description
str

An alien-sounding name

Source code in src/alienbio/bio/skinning.py
def generate_alien_name(base: str, *, seed: Optional[int] = None) -> str:
    """Generate an opaque alien name from a base string.

    The name is deterministic given the same base and seed.

    Args:
        base: The original name to obfuscate
        seed: Random seed (defaults to hash of base)

    Returns:
        An alien-sounding name
    """
    if seed is None:
        h = hashlib.md5(base.encode()).hexdigest()
        seed = int(h[:8], 16)

    rng = random.Random(seed)
    prefix = rng.choice(_PREFIXES)
    suffix = rng.choice(_SUFFIXES)
    connector = rng.choice(_CONNECTORS)

    return f"{prefix}{connector}{suffix}"

generate_description(system, *, detail_level=2, name_map=None, seed=None)

Generate a natural language description of a biological system.

Parameters:

Name Type Description Default
system 'BioSystem'

The system to describe

required
detail_level int

1=minimal hints, 2=moderate, 3=full explanation

2
name_map Optional[Dict[str, str]]

Optional mapping from real names to alien names

None
seed Optional[int]

Random seed for name generation

None

Returns:

Type Description
str

Description string using alien terminology

Source code in src/alienbio/bio/skinning.py
def generate_description(
    system: "BioSystem",
    *,
    detail_level: int = 2,
    name_map: Optional[Dict[str, str]] = None,
    seed: Optional[int] = None,
) -> str:
    """Generate a natural language description of a biological system.

    Args:
        system: The system to describe
        detail_level: 1=minimal hints, 2=moderate, 3=full explanation
        name_map: Optional mapping from real names to alien names
        seed: Random seed for name generation

    Returns:
        Description string using alien terminology
    """
    if name_map is None:
        name_map = generate_name_map(system, seed=seed)

    mols = list(system.chemistry.molecules.keys())
    rxns = list(system.chemistry.reactions.keys())

    lines = []

    if detail_level >= 1:
        lines.append(
            f"System contains {len(mols)} substances and {len(rxns)} processes."
        )
        alien_mols = [name_map.get(m) or m for m in mols]
        lines.append("Substances: " + ", ".join(alien_mols))

    if detail_level >= 2:
        lines.append("")
        lines.append("Processes:")
        for rxn_name, rxn in system.chemistry.reactions.items():
            reactant_names = [name_map.get(m.name, m.name) for m in rxn.reactants]
            product_names = [name_map.get(m.name, m.name) for m in rxn.products]

            r_str = " + ".join(reactant_names) if reactant_names else "(source)"
            p_str = " + ".join(product_names) if product_names else "(sink)"
            proc_name = name_map.get(rxn_name, rxn_name)
            lines.append(f"  {proc_name}: {r_str} -> {p_str}")

    if detail_level >= 3:
        lines.append("")
        lines.append("Current state:")
        for mol_name in mols:
            alien_name = name_map.get(mol_name, mol_name)
            conc = system.state[mol_name]
            lines.append(f"  {alien_name}: {conc:.2f}")

    return "\n".join(lines)

generate_name_map(system, *, seed=None)

Generate a mapping from real names to alien names.

Maps molecules and reactions to opaque alien names.

Parameters:

Name Type Description Default
system 'BioSystem'

The system whose names to map

required
seed Optional[int]

Random seed for deterministic names

None

Returns:

Type Description
Dict[str, str]

Dict mapping original names to alien names

Source code in src/alienbio/bio/skinning.py
def generate_name_map(
    system: "BioSystem",
    *,
    seed: Optional[int] = None,
) -> Dict[str, str]:
    """Generate a mapping from real names to alien names.

    Maps molecules and reactions to opaque alien names.

    Args:
        system: The system whose names to map
        seed: Random seed for deterministic names

    Returns:
        Dict mapping original names to alien names
    """
    name_map: Dict[str, str] = {}
    base_seed = seed if seed is not None else 0
    used: set = set()

    for i, mol_name in enumerate(system.chemistry.molecules):
        name = generate_alien_name(mol_name, seed=base_seed + i)
        while name in used:
            base_seed += 100
            name = generate_alien_name(mol_name, seed=base_seed + i)
        name_map[mol_name] = name
        used.add(name)

    for i, rxn_name in enumerate(system.chemistry.reactions):
        name = generate_alien_name(rxn_name, seed=base_seed + 1000 + i)
        while name in used:
            base_seed += 100
            name = generate_alien_name(rxn_name, seed=base_seed + 1000 + i)
        name_map[rxn_name] = name
        used.add(name)

    return name_map

skin_task_description(task, name_map)

Apply alien naming to a task description.

Replaces any known molecule/reaction names with alien equivalents.

Parameters:

Name Type Description Default
task 'Task'

The task to skin

required
name_map Dict[str, str]

Mapping from real names to alien names

required

Returns:

Type Description
str

Task description with alien terminology

Source code in src/alienbio/bio/skinning.py
def skin_task_description(
    task: "Task",
    name_map: Dict[str, str],
) -> str:
    """Apply alien naming to a task description.

    Replaces any known molecule/reaction names with alien equivalents.

    Args:
        task: The task to skin
        name_map: Mapping from real names to alien names

    Returns:
        Task description with alien terminology
    """
    desc = task.description
    # Replace real names with alien names (longest first to avoid partial matches)
    for real_name in sorted(name_map.keys(), key=len, reverse=True):
        alien_name = name_map[real_name]
        desc = desc.replace(real_name, alien_name)
    return desc

check_no_earth_terms(text)

Check that text contains no Earth biology terms.

Parameters:

Name Type Description Default
text str

Text to check

required

Returns:

Type Description
List[str]

List of Earth terms found (empty if clean)

Source code in src/alienbio/bio/skinning.py
def check_no_earth_terms(text: str) -> List[str]:
    """Check that text contains no Earth biology terms.

    Args:
        text: Text to check

    Returns:
        List of Earth terms found (empty if clean)
    """
    text_lower = text.lower()
    found = []
    for term in EARTH_TERMS:
        if term in text_lower:
            found.append(term)
    return sorted(found)

measure_baseline(system, steps=500)

Run system to steady state and measure baseline concentrations.

Parameters:

Name Type Description Default
system 'BioSystem'

A healthy biological system

required
steps int

Number of steps to reach steady state

500

Returns:

Type Description
Baseline

Baseline with steady-state concentrations and default ranges (±20%)

Source code in src/alienbio/bio/disease.py
def measure_baseline(system: "BioSystem", steps: int = 500) -> Baseline:
    """Run system to steady state and measure baseline concentrations.

    Args:
        system: A healthy biological system
        steps: Number of steps to reach steady state

    Returns:
        Baseline with steady-state concentrations and default ranges (±20%)
    """
    system.run(steps)

    steady = {}
    for mol_name in system.chemistry.molecules:
        steady[mol_name] = system.state[mol_name]

    ranges = []
    for name, val in steady.items():
        margin = max(abs(val) * 0.2, 0.1)  # ±20% or at least ±0.1
        ranges.append(HealthRange(name, val - margin, val + margin))

    return Baseline(steady_state=steady, ranges=ranges)

generate_perturbations(system, *, seed=None, kinds=None)

Generate perturbations from a system's reactions.

Creates one perturbation per reaction: either rate_change or reaction_removal.

Parameters:

Name Type Description Default
system 'BioSystem'

The healthy system to perturb

required
seed Optional[int]

Random seed for reproducibility

None
kinds Optional[List[str]]

Allowed perturbation kinds (default: both)

None

Returns:

Type Description
List[Perturbation]

List of possible perturbations

Source code in src/alienbio/bio/disease.py
def generate_perturbations(
    system: "BioSystem",
    *,
    seed: Optional[int] = None,
    kinds: Optional[List[str]] = None,
) -> List[Perturbation]:
    """Generate perturbations from a system's reactions.

    Creates one perturbation per reaction: either rate_change or reaction_removal.

    Args:
        system: The healthy system to perturb
        seed: Random seed for reproducibility
        kinds: Allowed perturbation kinds (default: both)

    Returns:
        List of possible perturbations
    """
    rng = random.Random(seed)
    allowed = kinds or ["rate_change", "reaction_removal"]

    perturbations = []
    for rxn_name in system.chemistry.reactions:
        kind = rng.choice(allowed)
        if kind == "rate_change":
            factor = rng.choice([0.1, 0.5, 2.0, 5.0])
            perturbations.append(Perturbation(
                name=f"{rxn_name}_rate_x{factor}",
                kind="rate_change",
                target_reaction=rxn_name,
                factor=factor,
            ))
        else:
            perturbations.append(Perturbation(
                name=f"{rxn_name}_removed",
                kind="reaction_removal",
                target_reaction=rxn_name,
            ))

    return perturbations

detect_symptoms(concentrations, baseline)

Detect symptoms by comparing concentrations to healthy ranges.

Parameters:

Name Type Description Default
concentrations Dict[str, float]

Current molecule concentrations

required
baseline Baseline

Healthy baseline with ranges

required

Returns:

Type Description
List[Symptom]

List of symptoms (molecules outside healthy range)

Source code in src/alienbio/bio/disease.py
def detect_symptoms(
    concentrations: Dict[str, float],
    baseline: Baseline,
) -> List[Symptom]:
    """Detect symptoms by comparing concentrations to healthy ranges.

    Args:
        concentrations: Current molecule concentrations
        baseline: Healthy baseline with ranges

    Returns:
        List of symptoms (molecules outside healthy range)
    """
    symptoms = []
    for r in baseline.ranges:
        val = concentrations.get(r.molecule, 0.0)
        if not r.contains(val):
            if val < r.low:
                deviation = r.low - val
            else:
                deviation = val - r.high
            symptoms.append(Symptom(
                molecule=r.molecule,
                value=val,
                healthy_range=r,
                deviation=deviation,
            ))
    return symptoms

generate_organism(chemistry, *, num_organs=3, seed=None, dt=1.0, transport_rate=0.01)

Generate a multi-compartment organism from a chemistry.

Creates an organism with: - A root compartment ("body") - Multiple organ compartments as children - Each organ gets a subset of reactions - Transport links between organs for shared molecules

Parameters:

Name Type Description Default
chemistry ChemistryImpl

Chemistry defining molecules and reactions

required
num_organs int

Number of organ compartments to create

3
seed Optional[int]

Random seed for reproducibility

None
dt float

Simulation time step

1.0
transport_rate float

Rate for inter-compartment transport

0.01

Returns:

Type Description
Organism

Organism with tree, state, simulator, and transport links

Source code in src/alienbio/bio/organ_generator.py
def generate_organism(
    chemistry: ChemistryImpl,
    *,
    num_organs: int = 3,
    seed: Optional[int] = None,
    dt: float = 1.0,
    transport_rate: float = 0.01,
) -> Organism:
    """Generate a multi-compartment organism from a chemistry.

    Creates an organism with:
    - A root compartment ("body")
    - Multiple organ compartments as children
    - Each organ gets a subset of reactions
    - Transport links between organs for shared molecules

    Args:
        chemistry: Chemistry defining molecules and reactions
        num_organs: Number of organ compartments to create
        seed: Random seed for reproducibility
        dt: Simulation time step
        transport_rate: Rate for inter-compartment transport

    Returns:
        Organism with tree, state, simulator, and transport links
    """
    rng = random.Random(seed)

    mol_names = list(chemistry.molecules.keys())
    mol_to_id = {name: i for i, name in enumerate(mol_names)}
    num_molecules = len(mol_names)

    # Build compartment tree
    tree = CompartmentTreeImpl()
    body = tree.add_root("body")
    organs: List[CompartmentId] = []
    for i in range(num_organs):
        organ = tree.add_child(body, f"organ_{i}")
        organs.append(organ)

    # Assign reactions to organs (each reaction goes to 1-2 organs)
    reaction_specs: List[ReactionSpec] = []
    for rxn_name, reaction in chemistry.reactions.items():
        # Convert to ReactionSpec
        reactants = {}
        products = {}
        for mol, stoich in reaction.reactants.items():
            mid = mol_to_id.get(mol.name)
            if mid is not None:
                reactants[mid] = stoich
        for mol, stoich in reaction.products.items():
            mid = mol_to_id.get(mol.name)
            if mid is not None:
                products[mid] = stoich

        rate = reaction.rate if isinstance(reaction.rate, (int, float)) else 1.0

        # Assign to random organs
        n_assigned = rng.randint(1, min(2, num_organs))
        assigned = rng.sample(organs, n_assigned)

        reaction_specs.append(ReactionSpec(
            name=rxn_name,
            reactants=reactants,
            products=products,
            rate_constant=rate,
            compartments=assigned,
        ))

    # Create transport flows between adjacent organs
    transport_links: List[TransportLink] = []
    flows: List[GeneralFlow] = []

    for i in range(len(organs) - 1):
        src = organs[i]
        tgt = organs[i + 1]
        # Transport a random molecule in both directions
        mol_id = rng.randrange(num_molecules)

        transport_links.append(TransportLink(src, tgt, mol_id, transport_rate))
        transport_links.append(TransportLink(tgt, src, mol_id, transport_rate))

        # Create GeneralFlow for transport
        def _make_flow(s: int, t: int, m: int, r: float) -> GeneralFlow:
            def apply_fn(state: WorldStateImpl, _tree: CompartmentTreeImpl, dt_: float) -> None:
                conc_src = state.get(s, m)
                transfer = conc_src * r * dt_
                state.set(s, m, max(0.0, conc_src - transfer))
                state.set(t, m, state.get(t, m) + transfer)
            return GeneralFlow(origin=s, apply_fn=apply_fn, name=f"transport_{s}_to_{t}_mol{m}")

        flows.append(_make_flow(src, tgt, mol_id, transport_rate))
        flows.append(_make_flow(tgt, src, mol_id, transport_rate))

    # Build simulator
    simulator = WorldSimulatorImpl(
        tree=tree,
        reactions=reaction_specs,
        flows=flows,
        num_molecules=num_molecules,
        dt=dt,
    )

    # Build initial state (small random concentrations in each organ)
    state = WorldStateImpl(tree=tree, num_molecules=num_molecules)
    for organ in organs:
        for mol_id in range(num_molecules):
            state.set(organ, mol_id, rng.uniform(0.0, 5.0))

    return Organism(
        tree=tree,
        state=state,
        simulator=simulator,
        transport_links=transport_links,
    )

inject_spike(system, molecule, amount, recovery_steps=50, tolerance=0.1)

Inject a concentration spike and observe recovery.

Runs the system for recovery_steps, then injects a spike, then runs for another recovery_steps. Checks if the system returns to within tolerance of the pre-spike concentrations.

Parameters:

Name Type Description Default
system 'BioSystem'

BioSystem to perturb

required
molecule str

Molecule to spike

required
amount float

Amount to add

required
recovery_steps int

Steps to run before and after spike

50
tolerance float

Fraction of pre-spike value considered "recovered"

0.1

Returns:

Type Description
PerturbationResult

PerturbationResult with recovery information

Source code in src/alienbio/bio/perturbation.py
def inject_spike(
    system: "BioSystem",
    molecule: str,
    amount: float,
    recovery_steps: int = 50,
    tolerance: float = 0.1,
) -> PerturbationResult:
    """Inject a concentration spike and observe recovery.

    Runs the system for recovery_steps, then injects a spike, then runs
    for another recovery_steps. Checks if the system returns to within
    tolerance of the pre-spike concentrations.

    Args:
        system: BioSystem to perturb
        molecule: Molecule to spike
        amount: Amount to add
        recovery_steps: Steps to run before and after spike
        tolerance: Fraction of pre-spike value considered "recovered"

    Returns:
        PerturbationResult with recovery information
    """
    from .biosystem import BioSystem as _BioSystem

    # Run to get baseline
    baseline_sys = _BioSystem(
        system.chemistry, system.state.copy(),
        dt=system.simulator.dt,
    )
    baseline_sys.run(recovery_steps)
    baseline_final = {name: baseline_sys.state[name] for name in baseline_sys.state}

    # Inject spike at initial state
    perturbed_sys = _BioSystem(
        system.chemistry, system.state.copy(),
        dt=system.simulator.dt,
    )
    perturbed_sys.run(recovery_steps)
    # Apply spike
    current = perturbed_sys.state[molecule]
    perturbed_sys.state[molecule] = current + amount

    # Run recovery
    recovery_timeline = perturbed_sys.run(recovery_steps)

    perturbed_final = {name: perturbed_sys.state[name] for name in perturbed_sys.state}

    # Check recovery
    max_dev = 0.0
    recovery_step = None
    recovered = True

    for name in baseline_final:
        base_val = baseline_final[name]
        pert_val = perturbed_final[name]
        if base_val > 0:
            dev = abs(pert_val - base_val) / base_val
        else:
            dev = abs(pert_val - base_val)
        max_dev = max(max_dev, dev)
        if dev > tolerance:
            recovered = False

    # Find recovery step (first step where all molecules are within tolerance)
    for step_idx, state in enumerate(recovery_timeline):
        all_ok = True
        for name in baseline_final:
            base_val = baseline_final[name]
            step_val = state[name]
            if base_val > 0:
                dev = abs(step_val - base_val) / base_val
            else:
                dev = abs(step_val - base_val)
            if dev > tolerance:
                all_ok = False
                break
        if all_ok:
            recovery_step = step_idx
            break

    return PerturbationResult(
        recovered=recovered,
        baseline_final=baseline_final,
        perturbed_final=perturbed_final,
        max_deviation=max_dev,
        recovery_step=recovery_step,
        steps_run=recovery_steps,
    )

remove_reaction_drift(system, reaction_name, steps=50, drift_threshold=0.01)

Remove a reaction and measure the resulting drift.

Runs the system with and without the named reaction for the same number of steps. Compares final states to measure drift.

Parameters:

Name Type Description Default
system 'BioSystem'

BioSystem to modify

required
reaction_name str

Name of reaction to remove

required
steps int

Steps to run

50
drift_threshold float

Minimum drift to consider "drifted"

0.01

Returns:

Type Description
DriftResult

DriftResult with per-molecule drift measurements

Source code in src/alienbio/bio/perturbation.py
def remove_reaction_drift(
    system: "BioSystem",
    reaction_name: str,
    steps: int = 50,
    drift_threshold: float = 0.01,
) -> DriftResult:
    """Remove a reaction and measure the resulting drift.

    Runs the system with and without the named reaction for the same
    number of steps. Compares final states to measure drift.

    Args:
        system: BioSystem to modify
        reaction_name: Name of reaction to remove
        steps: Steps to run
        drift_threshold: Minimum drift to consider "drifted"

    Returns:
        DriftResult with per-molecule drift measurements
    """
    from .biosystem import BioSystem as _BioSystem
    from .chemistry import ChemistryImpl

    # Run baseline
    baseline_sys = _BioSystem(
        system.chemistry, system.state.copy(),
        dt=system.simulator.dt,
    )
    baseline_sys.run(steps)
    baseline_final = {name: baseline_sys.state[name] for name in baseline_sys.state}

    # Create modified chemistry without the reaction
    remaining_reactions = {
        name: rxn for name, rxn in system.chemistry.reactions.items()
        if name != reaction_name
    }
    modified_chem = ChemistryImpl(
        system.chemistry.local_name + "_modified",
        atoms=system.chemistry.atoms,
        molecules=system.chemistry.molecules,
        reactions=remaining_reactions,
        dat=system.chemistry.dat(),
    )

    modified_state = StateImpl(modified_chem, initial={
        name: system.state[name] for name in system.state
    })
    modified_sys = _BioSystem(modified_chem, modified_state, dt=system.simulator.dt)
    modified_sys.run(steps)
    modified_final = {name: modified_sys.state[name] for name in modified_sys.state}

    # Compute per-molecule drift
    drift_per_mol: Dict[str, float] = {}
    for name in baseline_final:
        drift_per_mol[name] = abs(modified_final[name] - baseline_final[name])

    max_drift = max(drift_per_mol.values()) if drift_per_mol else 0.0

    return DriftResult(
        drifted=max_drift >= drift_threshold,
        baseline_final=baseline_final,
        modified_final=modified_final,
        drift_per_molecule=drift_per_mol,
        max_drift=max_drift,
        steps_run=steps,
    )

measure_intervention_response(system, intervention, steps=50)

Apply an intervention (set concentrations) and measure response.

Parameters:

Name Type Description Default
system 'BioSystem'

BioSystem to perturb

required
intervention Dict[str, float]

Dict mapping molecule names to new concentrations

required
steps int

Steps to run after intervention

50

Returns:

Type Description
Dict[str, float]

Dict mapping molecule names to change from pre-intervention values

Source code in src/alienbio/bio/perturbation.py
def measure_intervention_response(
    system: "BioSystem",
    intervention: Dict[str, float],
    steps: int = 50,
) -> Dict[str, float]:
    """Apply an intervention (set concentrations) and measure response.

    Args:
        system: BioSystem to perturb
        intervention: Dict mapping molecule names to new concentrations
        steps: Steps to run after intervention

    Returns:
        Dict mapping molecule names to change from pre-intervention values
    """
    from .biosystem import BioSystem as _BioSystem

    # Snapshot pre-intervention
    pre = {name: system.state[name] for name in system.state}

    # Create copy, apply intervention, run
    modified_sys = _BioSystem(
        system.chemistry, system.state.copy(),
        dt=system.simulator.dt,
    )
    for mol, value in intervention.items():
        modified_sys.state[mol] = value

    modified_sys.run(steps)

    # Compute deltas
    return {
        name: modified_sys.state[name] - pre[name]
        for name in pre
    }