Skip to content

Models

BindingSite dataclass

A specific binding site on a specific component.

Parameters:

Name Type Description Default
component_id ID

The ID of the component this site belongs to.

required
site_id ID

The site identifier within the component.

required

Examples:

>>> from nasap_net import BindingSite
>>> BindingSite('M0', 0)  # site 0 on component M0

Creating a bond by passing binding sites explicitly:

>>> from nasap_net import Bond
>>> metal_bs = BindingSite('M0', 0)  # site 0 on component M0
>>> ligand_bs = BindingSite('L0', 0)  # site 0 on component L0
>>> Bond.from_sites(metal_bs, ligand_bs)
Source code in src/nasap_net/models/binding_site.py
@total_ordering
@dataclass(frozen=True)
class BindingSite(SupportsDunderLt):
    """A specific binding site on a specific component.

    Parameters
    ----------
    component_id : ID
        The ID of the component this site belongs to.
    site_id : ID
        The site identifier within the component.

    Examples
    --------
    >>> from nasap_net import BindingSite
    >>> BindingSite('M0', 0)  # site 0 on component M0

    Creating a bond by passing binding sites explicitly:

    >>> from nasap_net import Bond
    >>> metal_bs = BindingSite('M0', 0)  # site 0 on component M0
    >>> ligand_bs = BindingSite('L0', 0)  # site 0 on component L0
    >>> Bond.from_sites(metal_bs, ligand_bs)
    """
    component_id: ID
    site_id: ID

    def __lt__(self, other):
        if not isinstance(other, BindingSite):
            return NotImplemented
        self_values = (self.component_id, self.site_id)
        other_values = (other.component_id, other.site_id)
        return self_values < other_values

    def __repr__(self):
        return f'BindingSite({self.component_id!r}, {self.site_id!r})'

AuxEdge dataclass

An auxiliary edge between two binding sites on the same component.

Used to encode geometric constraints between sites, such as cis/trans relationships in octahedral metal complexes.

Parameters:

Name Type Description Default
site_id1 ID

The first site ID.

required
site_id2 ID

The second site ID. Must be different from site_id1.

required
kind str | None

The kind of geometric relationship (e.g., 'cis', 'trans'). Default is None.

None

Examples:

>>> from nasap_net import AuxEdge
>>> AuxEdge(0, 1)              # geometric adjacency between sites 0 and 1
>>> AuxEdge(0, 1, kind='cis')  # cis relationship between sites 0 and 1

Encoding square-planar geometry in a metal center (sites 0–3 form a ring):

>>> from nasap_net import Component
>>> M = Component(
...     kind='M', sites=[0, 1, 2, 3],
...     aux_edges=[AuxEdge(0, 1), AuxEdge(1, 2),
...                AuxEdge(2, 3), AuxEdge(3, 0)],
... )
Source code in src/nasap_net/models/aux_edge.py
@total_ordering
@dataclass(frozen=True, init=False)
class AuxEdge(SupportsDunderLt):
    """An auxiliary edge between two binding sites on the same component.

    Used to encode geometric constraints between sites, such as cis/trans
    relationships in octahedral metal complexes.

    Parameters
    ----------
    site_id1 : ID
        The first site ID.
    site_id2 : ID
        The second site ID. Must be different from ``site_id1``.
    kind : str | None, optional
        The kind of geometric relationship (e.g., ``'cis'``, ``'trans'``).
        Default is None.

    Examples
    --------
    >>> from nasap_net import AuxEdge
    >>> AuxEdge(0, 1)              # geometric adjacency between sites 0 and 1
    >>> AuxEdge(0, 1, kind='cis')  # cis relationship between sites 0 and 1

    Encoding square-planar geometry in a metal center (sites 0–3 form a ring):

    >>> from nasap_net import Component
    >>> M = Component(
    ...     kind='M', sites=[0, 1, 2, 3],
    ...     aux_edges=[AuxEdge(0, 1), AuxEdge(1, 2),
    ...                AuxEdge(2, 3), AuxEdge(3, 0)],
    ... )
    """
    site_ids: frozenset[ID]
    kind: str | None = field(kw_only=True, default=None)

    def __init__(
            self, site_id1: ID, site_id2: ID, kind: str | None = None):
        if site_id1 == site_id2:
            raise ValueError("Sites in an auxiliary edge must be different.")
        object.__setattr__(
            self, 'site_ids',
            frozenset({site_id1, site_id2}))
        object.__setattr__(self, 'kind', kind)

    def __lt__(self, other):
        if not isinstance(other, AuxEdge):
            return NotImplemented
        self_values = [sorted(self.site_ids), self.kind]
        other_values = [sorted(other.site_ids), other.kind]
        return self_values < other_values

    def get_binding_sites(
            self, comp_id: ID,
    ) -> frozenset[BindingSite]:
        """Return the binding sites of this auxiliary edge."""
        site_id1, site_id2 = self.site_ids
        return frozenset({
            BindingSite(component_id=comp_id, site_id=site_id1),
            BindingSite(component_id=comp_id, site_id=site_id2)
        })

    def to_tuple(self) -> tuple[ID, ID] | tuple[ID, ID, str]:
        """Return a tuple representation of the auxiliary edge."""
        site_id1, site_id2 = sorted(self.site_ids)
        if self.kind is None:
            return site_id1, site_id2
        return site_id1, site_id2, self.kind

get_binding_sites(comp_id)

Return the binding sites of this auxiliary edge.

Source code in src/nasap_net/models/aux_edge.py
def get_binding_sites(
        self, comp_id: ID,
) -> frozenset[BindingSite]:
    """Return the binding sites of this auxiliary edge."""
    site_id1, site_id2 = self.site_ids
    return frozenset({
        BindingSite(component_id=comp_id, site_id=site_id1),
        BindingSite(component_id=comp_id, site_id=site_id2)
    })

to_tuple()

Return a tuple representation of the auxiliary edge.

Source code in src/nasap_net/models/aux_edge.py
def to_tuple(self) -> tuple[ID, ID] | tuple[ID, ID, str]:
    """Return a tuple representation of the auxiliary edge."""
    site_id1, site_id2 = sorted(self.site_ids)
    if self.kind is None:
        return site_id1, site_id2
    return site_id1, site_id2, self.kind

Component dataclass

A component in a self-assembly system.

Parameters:

Name Type Description Default
kind str

The kind identifier for this component (e.g., 'M', 'L', 'X').

required
sites Iterable[ID]

The site IDs available for bonding on this component.

required
aux_edges Iterable[AuxEdge] | None

Auxiliary edges representing geometric constraints between sites (e.g., cis/trans relationships). Default is None.

None

Examples:

>>> from nasap_net import Component
>>> M = Component(kind='M', sites=[0, 1])  # metal with 2 binding sites
>>> L = Component(kind='L', sites=[0, 1])  # bridging ligand with 2 binding sites
>>> X = Component(kind='X', sites=[0])     # monodentate leaving ligand

With auxiliary edges encoding geometric constraints:

>>> from nasap_net import AuxEdge
>>> M = Component(
...     kind='M', sites=[0, 1, 2, 3],
...     aux_edges=[AuxEdge(0, 1), AuxEdge(1, 2),
...                AuxEdge(2, 3), AuxEdge(3, 0)],
... )
Source code in src/nasap_net/models/component.py
@dataclass(frozen=True, init=False)
class Component:
    """A component in a self-assembly system.

    Parameters
    ----------
    kind : str
        The kind identifier for this component (e.g., ``'M'``, ``'L'``, ``'X'``).
    sites : Iterable[ID]
        The site IDs available for bonding on this component.
    aux_edges : Iterable[AuxEdge] | None, optional
        Auxiliary edges representing geometric constraints between sites
        (e.g., cis/trans relationships). Default is None.

    Examples
    --------
    >>> from nasap_net import Component
    >>> M = Component(kind='M', sites=[0, 1])  # metal with 2 binding sites
    >>> L = Component(kind='L', sites=[0, 1])  # bridging ligand with 2 binding sites
    >>> X = Component(kind='X', sites=[0])     # monodentate leaving ligand

    With auxiliary edges encoding geometric constraints:

    >>> from nasap_net import AuxEdge
    >>> M = Component(
    ...     kind='M', sites=[0, 1, 2, 3],
    ...     aux_edges=[AuxEdge(0, 1), AuxEdge(1, 2),
    ...                AuxEdge(2, 3), AuxEdge(3, 0)],
    ... )
    """
    kind: str
    site_ids: frozenset[ID]
    aux_edges: frozenset[AuxEdge]

    def __init__(
            self, kind: str, sites: Iterable[ID],
            aux_edges: Iterable[AuxEdge] | None = None
            ):
        object.__setattr__(self, 'kind', kind)
        object.__setattr__(self, 'site_ids', frozenset(sites))
        if aux_edges is None:
            aux_edges = frozenset()
        else:
            aux_edges = frozenset(aux_edges)
        object.__setattr__(self, 'aux_edges', aux_edges)

    def __repr__(self):
        fields: dict[str, Any] = {}
        fields['kind'] = self.kind
        fields['site_ids'] = sorted(self.site_ids)
        if self.aux_edges:
            fields['aux_edges'] = [
                aux_edge.to_tuple() for aux_edge in sorted(self.aux_edges)
            ]
        return construct_repr(self.__class__, fields)

    def get_binding_sites(self, comp_id: ID) -> frozenset[BindingSite]:
        """Return the binding sites of this component."""
        return frozenset(
            BindingSite(component_id=comp_id, site_id=site_id)
            for site_id in self.site_ids
        )

get_binding_sites(comp_id)

Return the binding sites of this component.

Source code in src/nasap_net/models/component.py
def get_binding_sites(self, comp_id: ID) -> frozenset[BindingSite]:
    """Return the binding sites of this component."""
    return frozenset(
        BindingSite(component_id=comp_id, site_id=site_id)
        for site_id in self.site_ids
    )

Bond dataclass

A bond between two binding sites on two components.

Parameters:

Name Type Description Default
comp_id1 ID

The ID of the first component.

required
site1 ID

The site ID on the first component.

required
comp_id2 ID

The ID of the second component.

required
site2 ID

The site ID on the second component.

required

Examples:

>>> from nasap_net import Bond
>>> Bond('M0', 0, 'L0', 0)  # M0's site 0 bonded to L0's site 0
Source code in src/nasap_net/models/bond.py
@total_ordering
@dataclass(frozen=True, init=False)
class Bond(Iterable, SupportsDunderLt):
    """A bond between two binding sites on two components.

    Parameters
    ----------
    comp_id1 : ID
        The ID of the first component.
    site1 : ID
        The site ID on the first component.
    comp_id2 : ID
        The ID of the second component.
    site2 : ID
        The site ID on the second component.

    Examples
    --------
    >>> from nasap_net import Bond
    >>> Bond('M0', 0, 'L0', 0)  # M0's site 0 bonded to L0's site 0
    """
    sites: frozenset[BindingSite]

    def __init__(self, comp_id1: ID, site1: ID, comp_id2: ID, site2: ID):
        if comp_id1 == comp_id2:
            raise ValueError("Components in a bond must be different.")
        comp_and_site1 = BindingSite(component_id=comp_id1, site_id=site1)
        comp_and_site2 = BindingSite(component_id=comp_id2, site_id=site2)
        object.__setattr__(
            self, 'sites',
            frozenset((comp_and_site1, comp_and_site2))
        )

    def __iter__(self) -> Iterator[BindingSite]:
        return iter(sorted(self.sites))

    def __lt__(self, other):
        if not isinstance(other, Bond):
            return NotImplemented
        self_values = sorted(self.sites)
        other_values = sorted(other.sites)
        return self_values < other_values

    @property
    def component_ids(self) -> frozenset[ID]:
        """Return the component IDs involved in the bond."""
        return frozenset(site.component_id for site in self.sites)

    def to_tuple(self) -> tuple[ID, ID, ID, ID]:
        """Return the bond as a tuple of component and site IDs."""
        site1, site2 = sorted(self.sites)
        return (
            site1.component_id, site1.site_id,
            site2.component_id, site2.site_id,
        )

    @classmethod
    def from_sites(cls, site1: BindingSite, site2: BindingSite) -> Self:
        """Create a Bond from two BindingSite instances."""
        return cls(
            comp_id1=site1.component_id,
            comp_id2=site2.component_id,
            site1=site1.site_id,
            site2=site2.site_id
            )

component_ids property

Return the component IDs involved in the bond.

from_sites(site1, site2) classmethod

Create a Bond from two BindingSite instances.

Source code in src/nasap_net/models/bond.py
@classmethod
def from_sites(cls, site1: BindingSite, site2: BindingSite) -> Self:
    """Create a Bond from two BindingSite instances."""
    return cls(
        comp_id1=site1.component_id,
        comp_id2=site2.component_id,
        site1=site1.site_id,
        site2=site2.site_id
        )

to_tuple()

Return the bond as a tuple of component and site IDs.

Source code in src/nasap_net/models/bond.py
def to_tuple(self) -> tuple[ID, ID, ID, ID]:
    """Return the bond as a tuple of component and site IDs."""
    site1, site2 = sorted(self.sites)
    return (
        site1.component_id, site1.site_id,
        site2.component_id, site2.site_id,
    )

Assembly dataclass

An assembly of components connected by bonds.

Parameters:

Name Type Description Default
components Mapping[C, Component[S]]

A mapping from component IDs to their corresponding components.

required
bonds Iterable[Bond[C, S]]

An iterable of bonds connecting the components.

required

Raises:

Type Description
-InconsistentComponentError

If components with the same kind have different structures.

-InvalidBondError
  • If any bond references a non-existent component or site.
  • If a component bonds to itself.
  • If a site is used more than once.
  • If the assembly is not connected.
-ParallelBondError

If there are multiple bonds between the same pair of components.

Warnings
  • The assembly does not enforce connectivity; it is the user's responsibility to ensure that the assembly is connected as needed.
Notes

Currently, the assembly does not support parallel bonds (multiple bonds between the same pair of components), e.g., chelate complexes.

Examples:

A simple M2L2 ring:

>>> from nasap_net import Component, Assembly, Bond
>>> M = Component(kind='M', sites=[0, 1])
>>> L = Component(kind='L', sites=[0, 1])
>>> M2L2 = Assembly(
...     components={'M0': M, 'M1': M, 'L0': L, 'L1': L},
...     bonds=[
...         Bond('M0', 1, 'L0', 0), Bond('L0', 1, 'M1', 0),
...         Bond('M1', 1, 'L1', 0), Bond('L1', 1, 'M0', 0),
...     ]
... )

A free ligand as a single-component assembly:

>>> X = Component(kind='X', sites=[0])
>>> free_X = Assembly(id_='free_X', components={'X0': X}, bonds=[])
Source code in src/nasap_net/models/assembly.py
@total_ordering
@dataclass(frozen=True, init=False)
class Assembly:
    """An assembly of components connected by bonds.

    Parameters
    ----------
    components : Mapping[C, nasap_net.models.component.Component[S]]
        A mapping from component IDs to their corresponding components.
    bonds : Iterable[Bond[C, S]]
        An iterable of bonds connecting the components.

    Raises
    ------
    - InconsistentComponentError
        If components with the same kind have different structures.
    - InvalidBondError
        - If any bond references a non-existent component or site.
        - If a component bonds to itself.
        - If a site is used more than once.
        - If the assembly is not connected.
    - ParallelBondError
        If there are multiple bonds between the same pair of components.

    Warnings
    --------
    - The assembly does not enforce connectivity; it is the user's
      responsibility to ensure that the assembly is connected as needed.

    Notes
    -----
    Currently, the assembly does not support parallel bonds (multiple bonds
    between the same pair of components), e.g., chelate complexes.

    Examples
    --------
    A simple M2L2 ring:

    >>> from nasap_net import Component, Assembly, Bond
    >>> M = Component(kind='M', sites=[0, 1])
    >>> L = Component(kind='L', sites=[0, 1])
    >>> M2L2 = Assembly(
    ...     components={'M0': M, 'M1': M, 'L0': L, 'L1': L},
    ...     bonds=[
    ...         Bond('M0', 1, 'L0', 0), Bond('L0', 1, 'M1', 0),
    ...         Bond('M1', 1, 'L1', 0), Bond('L1', 1, 'M0', 0),
    ...     ]
    ... )

    A free ligand as a single-component assembly:

    >>> X = Component(kind='X', sites=[0])
    >>> free_X = Assembly(id_='free_X', components={'X0': X}, bonds=[])
    """
    _components: frozendict[ID, Component]
    bonds: frozenset[Bond]
    _id: ID | None

    def __init__(
            self, components: Mapping[ID, Component],
            bonds: Iterable[Bond],
            *,
            id_: ID | None = None
            ):
        if components is None:
            raise TypeError("components cannot be None")
        if bonds is None:
            raise TypeError("bonds cannot be None")

        object.__setattr__(self, '_components', frozendict(components))
        object.__setattr__(self, 'bonds', frozenset(bonds))
        object.__setattr__(self, '_id', id_)
        self._validate()

    def __lt__(self, other):
        if not isinstance(other, Assembly):
            return NotImplemented
        # 1. number of components
        # 2. number of bonds
        # 3. component IDs (sorted)
        # 4. bonds (sorted)
        if len(self._components) != len(other._components):
            return len(self._components) < len(other._components)
        if len(self.bonds) != len(other.bonds):
            return len(self.bonds) < len(other.bonds)
        self_comp_ids = sorted(self._components.keys())
        other_comp_ids = sorted(other._components.keys())
        if self_comp_ids != other_comp_ids:
            return self_comp_ids < other_comp_ids
        self_bonds = sorted(self.bonds)
        other_bonds = sorted(other.bonds)
        return self_bonds < other_bonds

    def __repr__(self):
        fields: dict[str, Any] = {}
        if self._id is not None:
            fields['id_'] = self._id
        fields['components'] = dict(sorted(self.component_id_to_kind.items()))
        fields['bonds'] = [bond.to_tuple() for bond in sorted(self.bonds)]
        return construct_repr(self.__class__, fields)

    @property
    def id_(self) -> ID:
        """Return the ID of the assembly."""
        if self._id is None:
            raise IDNotSetError("Assembly ID is not set.")
        return self._id

    @property
    def id_or_none(self) -> ID | None:
        """Return the ID of the assembly, or None if not set."""
        return self._id

    @property
    def components(self) -> Mapping[ID, Component]:
        """Return the components in the assembly as an immutable mapping."""
        return MappingProxyType(self._components)

    @cached_property
    def component_id_to_kind(self) -> Mapping[ID, str]:
        """Return a mapping from component IDs to their kinds."""
        return MappingProxyType({
            comp_id: comp.kind for comp_id, comp
            in self._components.items()
        })

    @property
    def component_kind_counts(self) -> dict[str, int]:
        """Return a mapping from component kinds to their counts."""
        kind_counts: defaultdict[str, int] = defaultdict(int)
        for comp in self._components.values():
            kind_counts[comp.kind] += 1
        return kind_counts

    def get_component_kind_of_site(self, site: BindingSite) -> str:
        """Return the component kind of the given binding site."""
        return self._components[site.component_id].kind

    def find_sites(
            self, *, has_bond: bool | None = None,
            component_kind: str | None = None
            ) -> frozenset[BindingSite]:
        """Return binding sites based on their bond status.
        """
        if has_bond is None and component_kind is None:
            return self._all_sites

        sites = set()
        for site in self._all_sites:
            if has_bond is not None:
                if has_bond != self.has_bond(site):
                    continue
            if component_kind is not None:
                if self.get_component_kind_of_site(site) != component_kind:
                    continue
            sites.add(site)
        return frozenset(sites)

    def has_bond(self, site: BindingSite) -> bool:
        """Check if a binding site has a bond."""
        return site in self._sites_with_bond

    def has_bond_between_components(self, comp_id1: ID, comp_id2: ID) -> bool:
        """Check if there is a bond between two components."""
        return frozenset({comp_id1, comp_id2}) in self._component_connection

    def get_bond_by_comp_ids(self, comp_id1: ID, comp_id2: ID) -> Bond:
        """Return the bond between two components.

        Parameters
        ----------
        comp_id1 : ID
            The ID of the first component.
        comp_id2 : ID
            The ID of the second component.

        Returns
        -------
        Bond
            The bond between the two components.

        Raises
        ------
        BondNotFoundError
            If there is no bond between the two components.

        Notes
        -----
        This method assumes that there is no parallel bonds between the
        same pair of components.
        """
        for bond in self.bonds:
            if bond.component_ids == frozenset({comp_id1, comp_id2}):
                return bond
        raise BondNotFoundError(comp_id1=comp_id1, comp_id2=comp_id2)

    def composition_formula(
            self,
            *,
            order: Sequence[str] | None = None,
            show_one: bool = False,
    ) -> str:
        """Return the composition formula string for this assembly.

        Parameters
        ----------
        order : Sequence[str], optional
            The order of component kinds to use in the formula.
            Kinds not listed here will appear at the end sorted alphabetically.
        show_one : bool
            If True, always show the count even when it is 1 (e.g., ``M1``
            instead of ``M``). Defaults to False.

        Returns
        -------
        str
            The composition formula string.

        Examples
        --------
        >>> M = Component(kind='M', sites=[0, 1])
        >>> L = Component(kind='L', sites=[0, 1])
        >>> M2L2 = Assembly(
        ...     components={'M0': M, 'M1': M, 'L0': L, 'L1': L},
        ...     bonds=[
        ...         Bond('M0', 1, 'L0', 0), Bond('L0', 1, 'M1', 0),
        ...         Bond('M1', 1, 'L1', 0), Bond('L1', 1, 'M0', 0),
        ...     ]
        ... )
        >>> M2L2.composition_formula()
        'L2M2'
        >>> M2L2.composition_formula(order=['M', 'L'])
        'M2L2'
        >>> M2L2.composition_formula(order=['M', 'L'], show_one=True)
        'M2L2'
        """
        from nasap_net.helpers.composition_formula import _build_formula
        kind_counter: Counter[str] = Counter(
            comp.kind for comp in self._components.values()
        )
        return _build_formula(kind_counter, order=order, show_one=show_one)

    def add_bond(self, site1: BindingSite, site2: BindingSite):
        """Return a new assembly with an additional bond."""
        new_bond = Bond.from_sites(site1, site2)
        new_bonds = set(self.bonds)
        new_bonds.add(new_bond)
        return self.copy_with(bonds=new_bonds)

    def remove_bond(self, site1: BindingSite, site2: BindingSite):
        """Return a new assembly with a bond removed."""
        bond_to_remove = Bond.from_sites(site1, site2)
        new_bonds = set(self.bonds)
        new_bonds.remove(bond_to_remove)
        return self.copy_with(bonds=new_bonds)

    def copy_with(
            self,
            *,
            components: Mapping[ID, Component] | Missing = MISSING,
            bonds: Iterable[Bond] | Missing = MISSING,
            id_: ID | None | Missing = MISSING,
            ) -> Self:
        """Return a copy of the assembly with optional modifications.

        Fields not provided will default to the current values, except for the
        ID, which will be set to None if not provided.

        If you want to copy the current ID, specify it explicitly,
        e.g., `copied = assembly.copy_with(id_=assembly.id_or_none)`.
        """
        return self.__class__(
            components=default_if_missing(components, self._components),
            bonds=default_if_missing(bonds, self.bonds),
            id_=default_if_missing(id_, None),
        )

    @cached_property
    def _all_sites(self) -> frozenset[BindingSite]:
        """Return all binding sites in the assembly."""
        sites = set()
        for comp_id, component in self._components.items():
            for site in component.site_ids:
                sites.add(BindingSite(component_id=comp_id, site_id=site))
        return frozenset(sites)

    @cached_property
    def _sites_with_bond(self) -> frozenset[BindingSite]:
        """Return a set of binding sites that have bonds."""
        site_with_bonds = set()
        for bond in self.bonds:
            for site in bond.sites:
                site_with_bonds.add(site)
        return frozenset(site_with_bonds)

    @cached_property
    def _component_connection(self) -> frozenset[frozenset[ID]]:
        connections = set()
        for bond in self.bonds:
            comp_ids = frozenset(bond.component_ids)
            connections.add(comp_ids)
        return frozenset(connections)

    def _get_component_of_site(self, site: BindingSite) -> Component:
        """Return the component corresponding to the given binding site."""
        return self._components[site.component_id]

    def _validate(self):
        self._validate_components()
        self._validate_bonds()
        self._validate_parallel_bonds()

    def _validate_components(self):
        # Components with the same kind should have the same structure.
        comp_kind_to_obj: dict[str, Component] = {}
        for comp in self._components.values():
            if comp.kind in comp_kind_to_obj:
                if comp != comp_kind_to_obj[comp.kind]:
                    raise InconsistentComponentError(
                        component_kind=comp.kind,
                        component1=comp,
                        component2=comp_kind_to_obj[comp.kind],
                    )
            else:
                comp_kind_to_obj[comp.kind] = comp

    def _validate_bonds(self):
        component_keys = set(self._components.keys())
        used_sites = set()
        for bond in self.bonds:
            # Validate that the components exist
            for comp_id in bond.component_ids:
                if comp_id not in component_keys:
                    raise InvalidBondError(
                        bond=bond,
                        detail=f"Component {comp_id} not found in assembly.")

            # Validate that the sites exist in the respective components
            for site in bond.sites:
                component = self._components[site.component_id]
                if site.site_id not in component.site_ids:
                    raise InvalidBondError(
                        bond=bond,
                        detail=(
                            f"Site {site.site_id} not found in component "
                            f"{site.component_id}."))

                # Validate that the site is not already used
                if site in used_sites:
                    raise InvalidBondError(
                        bond=bond,
                        detail=f"Site {site} is already used in another bond.")
                used_sites.add(site)

    def _validate_parallel_bonds(self):
        # Currently, parallel bonds (multiple bonds between the same pair
        # of components) are not supported.
        bonded_pairs: dict[frozenset[ID], Bond] = {}
        for bond in self.bonds:
            comp_pair = frozenset(bond.component_ids)
            if comp_pair in bonded_pairs:
                raise ParallelBondError(
                    bond1=bonded_pairs[comp_pair], bond2=bond
                )
            bonded_pairs[comp_pair] = bond

component_id_to_kind cached property

Return a mapping from component IDs to their kinds.

component_kind_counts property

Return a mapping from component kinds to their counts.

components property

Return the components in the assembly as an immutable mapping.

id_ property

Return the ID of the assembly.

id_or_none property

Return the ID of the assembly, or None if not set.

add_bond(site1, site2)

Return a new assembly with an additional bond.

Source code in src/nasap_net/models/assembly.py
def add_bond(self, site1: BindingSite, site2: BindingSite):
    """Return a new assembly with an additional bond."""
    new_bond = Bond.from_sites(site1, site2)
    new_bonds = set(self.bonds)
    new_bonds.add(new_bond)
    return self.copy_with(bonds=new_bonds)

composition_formula(*, order=None, show_one=False)

Return the composition formula string for this assembly.

Parameters:

Name Type Description Default
order Sequence[str]

The order of component kinds to use in the formula. Kinds not listed here will appear at the end sorted alphabetically.

None
show_one bool

If True, always show the count even when it is 1 (e.g., M1 instead of M). Defaults to False.

False

Returns:

Type Description
str

The composition formula string.

Examples:

>>> M = Component(kind='M', sites=[0, 1])
>>> L = Component(kind='L', sites=[0, 1])
>>> M2L2 = Assembly(
...     components={'M0': M, 'M1': M, 'L0': L, 'L1': L},
...     bonds=[
...         Bond('M0', 1, 'L0', 0), Bond('L0', 1, 'M1', 0),
...         Bond('M1', 1, 'L1', 0), Bond('L1', 1, 'M0', 0),
...     ]
... )
>>> M2L2.composition_formula()
'L2M2'
>>> M2L2.composition_formula(order=['M', 'L'])
'M2L2'
>>> M2L2.composition_formula(order=['M', 'L'], show_one=True)
'M2L2'
Source code in src/nasap_net/models/assembly.py
def composition_formula(
        self,
        *,
        order: Sequence[str] | None = None,
        show_one: bool = False,
) -> str:
    """Return the composition formula string for this assembly.

    Parameters
    ----------
    order : Sequence[str], optional
        The order of component kinds to use in the formula.
        Kinds not listed here will appear at the end sorted alphabetically.
    show_one : bool
        If True, always show the count even when it is 1 (e.g., ``M1``
        instead of ``M``). Defaults to False.

    Returns
    -------
    str
        The composition formula string.

    Examples
    --------
    >>> M = Component(kind='M', sites=[0, 1])
    >>> L = Component(kind='L', sites=[0, 1])
    >>> M2L2 = Assembly(
    ...     components={'M0': M, 'M1': M, 'L0': L, 'L1': L},
    ...     bonds=[
    ...         Bond('M0', 1, 'L0', 0), Bond('L0', 1, 'M1', 0),
    ...         Bond('M1', 1, 'L1', 0), Bond('L1', 1, 'M0', 0),
    ...     ]
    ... )
    >>> M2L2.composition_formula()
    'L2M2'
    >>> M2L2.composition_formula(order=['M', 'L'])
    'M2L2'
    >>> M2L2.composition_formula(order=['M', 'L'], show_one=True)
    'M2L2'
    """
    from nasap_net.helpers.composition_formula import _build_formula
    kind_counter: Counter[str] = Counter(
        comp.kind for comp in self._components.values()
    )
    return _build_formula(kind_counter, order=order, show_one=show_one)

copy_with(*, components=MISSING, bonds=MISSING, id_=MISSING)

Return a copy of the assembly with optional modifications.

Fields not provided will default to the current values, except for the ID, which will be set to None if not provided.

If you want to copy the current ID, specify it explicitly, e.g., copied = assembly.copy_with(id_=assembly.id_or_none).

Source code in src/nasap_net/models/assembly.py
def copy_with(
        self,
        *,
        components: Mapping[ID, Component] | Missing = MISSING,
        bonds: Iterable[Bond] | Missing = MISSING,
        id_: ID | None | Missing = MISSING,
        ) -> Self:
    """Return a copy of the assembly with optional modifications.

    Fields not provided will default to the current values, except for the
    ID, which will be set to None if not provided.

    If you want to copy the current ID, specify it explicitly,
    e.g., `copied = assembly.copy_with(id_=assembly.id_or_none)`.
    """
    return self.__class__(
        components=default_if_missing(components, self._components),
        bonds=default_if_missing(bonds, self.bonds),
        id_=default_if_missing(id_, None),
    )

find_sites(*, has_bond=None, component_kind=None)

Return binding sites based on their bond status.

Source code in src/nasap_net/models/assembly.py
def find_sites(
        self, *, has_bond: bool | None = None,
        component_kind: str | None = None
        ) -> frozenset[BindingSite]:
    """Return binding sites based on their bond status.
    """
    if has_bond is None and component_kind is None:
        return self._all_sites

    sites = set()
    for site in self._all_sites:
        if has_bond is not None:
            if has_bond != self.has_bond(site):
                continue
        if component_kind is not None:
            if self.get_component_kind_of_site(site) != component_kind:
                continue
        sites.add(site)
    return frozenset(sites)

get_bond_by_comp_ids(comp_id1, comp_id2)

Return the bond between two components.

Parameters:

Name Type Description Default
comp_id1 ID

The ID of the first component.

required
comp_id2 ID

The ID of the second component.

required

Returns:

Type Description
Bond

The bond between the two components.

Raises:

Type Description
BondNotFoundError

If there is no bond between the two components.

Notes

This method assumes that there is no parallel bonds between the same pair of components.

Source code in src/nasap_net/models/assembly.py
def get_bond_by_comp_ids(self, comp_id1: ID, comp_id2: ID) -> Bond:
    """Return the bond between two components.

    Parameters
    ----------
    comp_id1 : ID
        The ID of the first component.
    comp_id2 : ID
        The ID of the second component.

    Returns
    -------
    Bond
        The bond between the two components.

    Raises
    ------
    BondNotFoundError
        If there is no bond between the two components.

    Notes
    -----
    This method assumes that there is no parallel bonds between the
    same pair of components.
    """
    for bond in self.bonds:
        if bond.component_ids == frozenset({comp_id1, comp_id2}):
            return bond
    raise BondNotFoundError(comp_id1=comp_id1, comp_id2=comp_id2)

get_component_kind_of_site(site)

Return the component kind of the given binding site.

Source code in src/nasap_net/models/assembly.py
def get_component_kind_of_site(self, site: BindingSite) -> str:
    """Return the component kind of the given binding site."""
    return self._components[site.component_id].kind

has_bond(site)

Check if a binding site has a bond.

Source code in src/nasap_net/models/assembly.py
def has_bond(self, site: BindingSite) -> bool:
    """Check if a binding site has a bond."""
    return site in self._sites_with_bond

has_bond_between_components(comp_id1, comp_id2)

Check if there is a bond between two components.

Source code in src/nasap_net/models/assembly.py
def has_bond_between_components(self, comp_id1: ID, comp_id2: ID) -> bool:
    """Check if there is a bond between two components."""
    return frozenset({comp_id1, comp_id2}) in self._component_connection

remove_bond(site1, site2)

Return a new assembly with a bond removed.

Source code in src/nasap_net/models/assembly.py
def remove_bond(self, site1: BindingSite, site2: BindingSite):
    """Return a new assembly with a bond removed."""
    bond_to_remove = Bond.from_sites(site1, site2)
    new_bonds = set(self.bonds)
    new_bonds.remove(bond_to_remove)
    return self.copy_with(bonds=new_bonds)

Reaction dataclass

A single elementary reaction between assemblies.

Represents a metal–ligand exchange (MLE) in which a leaving ligand is replaced by an entering ligand at a specific metal binding site. Reaction objects are typically produced by :func:nasap_net.enumerate_reactions rather than constructed directly.

Parameters:

Name Type Description Default
init_assem Assembly

The initial assembly that undergoes the reaction.

required
entering_assem Assembly | None

The assembly supplying the entering ligand. None for intra-molecular reactions.

required
product_assem Assembly

The product assembly after the reaction.

required
leaving_assem Assembly | None

The assembly receiving the leaving ligand. None for intra-molecular reactions.

required
metal_bs BindingSite

The binding site on the metal where the exchange occurs.

required
leaving_bs BindingSite

The binding site of the leaving ligand.

required
entering_bs BindingSite

The binding site of the entering ligand.

required
duplicate_count int | None

The number of symmetry-equivalent ways this reaction can occur. Default is None.

None
id_ ID | None

An optional identifier for this reaction. Default is None.

None

Examples:

>>> from nasap_net import (
...     Component, Assembly, Bond, BindingSite, Reaction
... )
>>> M = Component(kind='M', sites=[0, 1])
>>> L = Component(kind='L', sites=[0, 1])
>>> X = Component(kind='X', sites=[0])
>>> MX2 = Assembly(
...     id_='MX2',
...     components={'M0': M, 'X0': X, 'X1': X},
...     bonds=[Bond('M0', 0, 'X0', 0), Bond('M0', 1, 'X1', 0)],
... )
>>> free_L = Assembly(id_='free_L', components={'L0': L}, bonds=[])
>>> MLX = Assembly(
...     id_='MLX',
...     components={'M0': M, 'X0': X, 'L0': L},
...     bonds=[Bond('M0', 0, 'X0', 0), Bond('M0', 1, 'L0', 0)],
... )
>>> free_X = Assembly(id_='free_X', components={'X0': X}, bonds=[])
>>> reaction = Reaction(
...     init_assem=MX2,
...     entering_assem=free_L,
...     product_assem=MLX,
...     leaving_assem=free_X,
...     metal_bs=BindingSite('M0', 1),    # M0's site 1 (the active site)
...     leaving_bs=BindingSite('X1', 0),  # X1's site 0
...     entering_bs=BindingSite('L0', 0), # L0's site 0
...     duplicate_count=2,
... )
>>> reaction.equation_str
'MX2 + free_L -> MLX + free_X'
Source code in src/nasap_net/models/reaction.py
@total_ordering
@dataclass(frozen=True, init=False)
class Reaction:
    """A single elementary reaction between assemblies.

    Represents a metal–ligand exchange (MLE) in which a leaving ligand is
    replaced by an entering ligand at a specific metal binding site.
    Reaction objects are typically produced by :func:`nasap_net.enumerate_reactions`
    rather than constructed directly.

    Parameters
    ----------
    init_assem : Assembly
        The initial assembly that undergoes the reaction.
    entering_assem : Assembly | None
        The assembly supplying the entering ligand.
        None for intra-molecular reactions.
    product_assem : Assembly
        The product assembly after the reaction.
    leaving_assem : Assembly | None
        The assembly receiving the leaving ligand.
        None for intra-molecular reactions.
    metal_bs : BindingSite
        The binding site on the metal where the exchange occurs.
    leaving_bs : BindingSite
        The binding site of the leaving ligand.
    entering_bs : BindingSite
        The binding site of the entering ligand.
    duplicate_count : int | None, optional
        The number of symmetry-equivalent ways this reaction can occur.
        Default is None.
    id_ : ID | None, optional
        An optional identifier for this reaction. Default is None.

    Examples
    --------
    >>> from nasap_net import (
    ...     Component, Assembly, Bond, BindingSite, Reaction
    ... )
    >>> M = Component(kind='M', sites=[0, 1])
    >>> L = Component(kind='L', sites=[0, 1])
    >>> X = Component(kind='X', sites=[0])
    >>> MX2 = Assembly(
    ...     id_='MX2',
    ...     components={'M0': M, 'X0': X, 'X1': X},
    ...     bonds=[Bond('M0', 0, 'X0', 0), Bond('M0', 1, 'X1', 0)],
    ... )
    >>> free_L = Assembly(id_='free_L', components={'L0': L}, bonds=[])
    >>> MLX = Assembly(
    ...     id_='MLX',
    ...     components={'M0': M, 'X0': X, 'L0': L},
    ...     bonds=[Bond('M0', 0, 'X0', 0), Bond('M0', 1, 'L0', 0)],
    ... )
    >>> free_X = Assembly(id_='free_X', components={'X0': X}, bonds=[])
    >>> reaction = Reaction(
    ...     init_assem=MX2,
    ...     entering_assem=free_L,
    ...     product_assem=MLX,
    ...     leaving_assem=free_X,
    ...     metal_bs=BindingSite('M0', 1),    # M0's site 1 (the active site)
    ...     leaving_bs=BindingSite('X1', 0),  # X1's site 0
    ...     entering_bs=BindingSite('L0', 0), # L0's site 0
    ...     duplicate_count=2,
    ... )
    >>> reaction.equation_str
    'MX2 + free_L -> MLX + free_X'
    """

    init_assem: Assembly
    entering_assem: Assembly | None
    product_assem: Assembly
    leaving_assem: Assembly | None
    metal_bs: BindingSite
    leaving_bs: BindingSite
    entering_bs: BindingSite
    _duplicate_count: int | None
    _id: ID | None

    def __init__(
            self,
            init_assem: Assembly,
            entering_assem: Assembly | None,
            product_assem: Assembly,
            leaving_assem: Assembly | None,
            metal_bs: BindingSite,
            leaving_bs: BindingSite,
            entering_bs: BindingSite,
            duplicate_count: int | None = None,
            id_: ID | None = None,
    ):
        if init_assem is None:
            raise TypeError("init_assem cannot be None")
        if product_assem is None:
            raise TypeError("product_assem cannot be None")
        if metal_bs is None:
            raise TypeError("metal_bs cannot be None")
        if leaving_bs is None:
            raise TypeError("leaving_bs cannot be None")
        if entering_bs is None:
            raise TypeError("entering_bs cannot be None")
        if duplicate_count is not None and duplicate_count <= 0:
            raise ValueError("duplicate_count must be a positive integer")

        object.__setattr__(self, 'init_assem', init_assem)
        object.__setattr__(self, 'entering_assem', entering_assem)
        object.__setattr__(self, 'product_assem', product_assem)
        object.__setattr__(self, 'leaving_assem', leaving_assem)
        object.__setattr__(self, 'metal_bs', metal_bs)
        object.__setattr__(self, 'leaving_bs', leaving_bs)
        object.__setattr__(self, 'entering_bs', entering_bs)
        object.__setattr__(self, '_duplicate_count', duplicate_count)
        object.__setattr__(self, '_id', id_)

    def __lt__(self, other):
        if not isinstance(other, Reaction):
            return NotImplemented
        def nullable(v):
            return (v is None, v or '')
        def key(reaction: Reaction) -> tuple:
            return (
                reaction.init_assem_id,
                nullable(reaction.entering_assem_id),
                reaction.product_assem_id,
                nullable(reaction.leaving_assem_id),
                reaction.metal_bs,
                reaction.leaving_bs,
                reaction.entering_bs,
                nullable(reaction.duplicate_count_or_none),
                nullable(reaction.id_or_none),
            )
        return key(self) < key(other)

    def __str__(self):
        equation = self.equation_str
        dup = self.duplicate_count
        return f'{equation} (x{dup})'

    def __repr__(self):
        equation = self.equation_str
        if self._id is None:
            return f'<{self.__class__.__name__} {equation}>'
        return f'<{self.__class__.__name__} ID={self._id} {equation}>'

    @property
    def id_(self) -> ID:
        """Return the ID of the reaction."""
        if self._id is None:
            raise IDNotSetError("Reaction ID is not set.")
        return self._id

    @property
    def id_or_none(self) -> ID | None:
        """Return the ID of the reaction, or None if not set."""
        return self._id

    @property
    def duplicate_count(self) -> int:
        """Return the duplicate count of the reaction."""
        if self._duplicate_count is None:
            raise DuplicateCountNotSetError()
        return self._duplicate_count

    @property
    def duplicate_count_or_none(self) -> int | None:
        """Return the duplicate count of the reaction, or None if not set."""
        return self._duplicate_count

    @property
    def equation_str(self) -> str:
        """Return a string representation of the reaction equation.

        If an assembly ID is not set, '??' is used in its place.
        """
        init = _assembly_to_id(self.init_assem)
        entering = _assembly_to_id(self.entering_assem)
        product = _assembly_to_id(self.product_assem)
        leaving = _assembly_to_id(self.leaving_assem)

        left = f'{init}' if entering is None else f'{init} + {entering}'
        right = f'{product}' if leaving is None else f'{product} + {leaving}'

        return f'{left} -> {right}'

    @property
    def entering_assem_strict(self) -> Assembly:
        """Return the entering assembly.

        Errors if there is no entering assembly.
        """
        if self.entering_assem is None:
            raise ValueError("No entering assembly in this reaction.")
        return self.entering_assem

    @property
    def leaving_assem_strict(self) -> Assembly:
        """Return the leaving assembly.

        Errors if there is no leaving assembly.
        """
        if self.leaving_assem is None:
            raise ValueError("No leaving assembly in this reaction.")
        return self.leaving_assem

    @property
    def init_assem_id(self) -> ID:
        """Return the ID of the initial assembly.

        Errors if the ID is not set.
        """
        return self.init_assem.id_

    @property
    def entering_assem_id(self) -> ID | None:
        """Return the ID of the entering assembly, or None if there is none.

        Errors if the ID is not set.
        """
        if self.entering_assem is None:
            return None
        return self.entering_assem.id_

    @property
    def product_assem_id(self) -> ID:
        """Return the ID of the product assembly.

        Errors if the ID is not set.
        """
        return self.product_assem.id_

    @property
    def leaving_assem_id(self) -> ID | None:
        """Return the ID of the leaving assembly, or None if there is none.

        Errors if the ID is not set.
        """
        if self.leaving_assem is None:
            return None
        return self.leaving_assem.id_

    @property
    def mle(self) -> MLE:
        """Return the MLE (metal, leaving, entering binding sites) of the reaction."""
        return MLE(
            metal=self.metal_bs,
            leaving=self.leaving_bs,
            entering=self.entering_bs,
        )

    def is_inter(self) -> bool:
        """Return True if the reaction is an inter-molecular reaction."""
        return self.entering_assem is not None

    def is_intra(self) -> bool:
        """Return True if the reaction is an intra-molecular reaction."""
        return self.entering_assem is None

    @property
    def metal_kind(self) -> str:
        """Return the kind of the metal binding site."""
        return self.init_assem.get_component_kind_of_site(self.metal_bs)

    def copy_with(
            self,
            *,
            init_assem: Assembly | Missing = MISSING,
            entering_assem: Assembly | None | Missing = MISSING,
            product_assem: Assembly | Missing = MISSING,
            leaving_assem: Assembly | None | Missing = MISSING,
            metal_bs: BindingSite | Missing = MISSING,
            leaving_bs: BindingSite | Missing = MISSING,
            entering_bs: BindingSite | Missing = MISSING,
            duplicate_count: int | Missing = MISSING,
            id_: ID | None | Missing = MISSING,
            ) -> Self:
        """Return a copy of the reaction with optional modifications.

        - Fields not provided will default to the current values,
            except for the ID, which will be set to None if not provided.
        - Fields explicitly set to None will overwrite with None.
            (only applies to fields that can be None)

        If you want to copy the current ID, specify it explicitly,
        e.g., `copied = reaction.copy_with(id_=reaction.id_or_none)`.
        """
        return self.__class__(
            init_assem=default_if_missing(init_assem, self.init_assem),
            entering_assem=default_if_missing(entering_assem, self.entering_assem),
            product_assem=default_if_missing(product_assem, self.product_assem),
            leaving_assem=default_if_missing(leaving_assem, self.leaving_assem),
            metal_bs=default_if_missing(metal_bs, self.metal_bs),
            leaving_bs=default_if_missing(leaving_bs, self.leaving_bs),
            entering_bs=default_if_missing(entering_bs, self.entering_bs),
            duplicate_count=default_if_missing(duplicate_count, self.duplicate_count),
            id_=default_if_missing(id_, None),
        )

    def as_reaction_to_classify(self) -> 'ReactionToClassify':
        """Return a ReactionToClassify version of this reaction."""
        from nasap_net.reaction_classification import ReactionToClassify
        return ReactionToClassify.from_reaction(self)

duplicate_count property

Return the duplicate count of the reaction.

duplicate_count_or_none property

Return the duplicate count of the reaction, or None if not set.

entering_assem_id property

Return the ID of the entering assembly, or None if there is none.

Errors if the ID is not set.

entering_assem_strict property

Return the entering assembly.

Errors if there is no entering assembly.

equation_str property

Return a string representation of the reaction equation.

If an assembly ID is not set, '??' is used in its place.

id_ property

Return the ID of the reaction.

id_or_none property

Return the ID of the reaction, or None if not set.

init_assem_id property

Return the ID of the initial assembly.

Errors if the ID is not set.

leaving_assem_id property

Return the ID of the leaving assembly, or None if there is none.

Errors if the ID is not set.

leaving_assem_strict property

Return the leaving assembly.

Errors if there is no leaving assembly.

metal_kind property

Return the kind of the metal binding site.

mle property

Return the MLE (metal, leaving, entering binding sites) of the reaction.

product_assem_id property

Return the ID of the product assembly.

Errors if the ID is not set.

as_reaction_to_classify()

Return a ReactionToClassify version of this reaction.

Source code in src/nasap_net/models/reaction.py
def as_reaction_to_classify(self) -> 'ReactionToClassify':
    """Return a ReactionToClassify version of this reaction."""
    from nasap_net.reaction_classification import ReactionToClassify
    return ReactionToClassify.from_reaction(self)

copy_with(*, init_assem=MISSING, entering_assem=MISSING, product_assem=MISSING, leaving_assem=MISSING, metal_bs=MISSING, leaving_bs=MISSING, entering_bs=MISSING, duplicate_count=MISSING, id_=MISSING)

Return a copy of the reaction with optional modifications.

  • Fields not provided will default to the current values, except for the ID, which will be set to None if not provided.
  • Fields explicitly set to None will overwrite with None. (only applies to fields that can be None)

If you want to copy the current ID, specify it explicitly, e.g., copied = reaction.copy_with(id_=reaction.id_or_none).

Source code in src/nasap_net/models/reaction.py
def copy_with(
        self,
        *,
        init_assem: Assembly | Missing = MISSING,
        entering_assem: Assembly | None | Missing = MISSING,
        product_assem: Assembly | Missing = MISSING,
        leaving_assem: Assembly | None | Missing = MISSING,
        metal_bs: BindingSite | Missing = MISSING,
        leaving_bs: BindingSite | Missing = MISSING,
        entering_bs: BindingSite | Missing = MISSING,
        duplicate_count: int | Missing = MISSING,
        id_: ID | None | Missing = MISSING,
        ) -> Self:
    """Return a copy of the reaction with optional modifications.

    - Fields not provided will default to the current values,
        except for the ID, which will be set to None if not provided.
    - Fields explicitly set to None will overwrite with None.
        (only applies to fields that can be None)

    If you want to copy the current ID, specify it explicitly,
    e.g., `copied = reaction.copy_with(id_=reaction.id_or_none)`.
    """
    return self.__class__(
        init_assem=default_if_missing(init_assem, self.init_assem),
        entering_assem=default_if_missing(entering_assem, self.entering_assem),
        product_assem=default_if_missing(product_assem, self.product_assem),
        leaving_assem=default_if_missing(leaving_assem, self.leaving_assem),
        metal_bs=default_if_missing(metal_bs, self.metal_bs),
        leaving_bs=default_if_missing(leaving_bs, self.leaving_bs),
        entering_bs=default_if_missing(entering_bs, self.entering_bs),
        duplicate_count=default_if_missing(duplicate_count, self.duplicate_count),
        id_=default_if_missing(id_, None),
    )

is_inter()

Return True if the reaction is an inter-molecular reaction.

Source code in src/nasap_net/models/reaction.py
def is_inter(self) -> bool:
    """Return True if the reaction is an inter-molecular reaction."""
    return self.entering_assem is not None

is_intra()

Return True if the reaction is an intra-molecular reaction.

Source code in src/nasap_net/models/reaction.py
def is_intra(self) -> bool:
    """Return True if the reaction is an intra-molecular reaction."""
    return self.entering_assem is None

MLEKind dataclass

Specifies the kind of metal–ligand exchange (MLE) reaction.

Parameters:

Name Type Description Default
metal str

The kind of the metal component.

required
leaving str

The kind of the leaving ligand component.

required
entering str

The kind of the entering ligand component.

required

Examples:

>>> from nasap_net import MLEKind
>>> MLEKind(metal='M', leaving='X', entering='L')  # X-to-L
>>> MLEKind(metal='M', leaving='L', entering='X')  # L-to-X
Source code in src/nasap_net/models/mle.py
@dataclass(frozen=True)
class MLEKind:
    """Specifies the kind of metal–ligand exchange (MLE) reaction.

    Parameters
    ----------
    metal : str
        The kind of the metal component.
    leaving : str
        The kind of the leaving ligand component.
    entering : str
        The kind of the entering ligand component.

    Examples
    --------
    >>> from nasap_net import MLEKind
    >>> MLEKind(metal='M', leaving='X', entering='L')  # X-to-L
    >>> MLEKind(metal='M', leaving='L', entering='X')  # L-to-X
    """

    metal: str
    leaving: str
    entering: str