Skip to content

Reaction Classification

classify_reactions(reactions, classifier)

Classify reactions using a user-defined classifier function.

Parameters:

Name Type Description Default
reactions Iterable[Reaction]

The reactions to classify.

required
classifier Callable[[Reaction], str]

A function that takes a reaction and returns its class as a string. Should raise :class:IncompleteReactionClassifierError for unhandled reactions.

required

Returns:

Type Description
dict[Reaction, str]

A mapping from each reaction to its assigned class.

Examples:

>>> from nasap_net import classify_reactions, IncompleteReactionClassifierError
>>> def classifier(reaction):
...     r = reaction.as_reaction_to_classify()
...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
...         return 'X-to-L'
...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
...         return 'L-to-X'
...     raise IncompleteReactionClassifierError(reaction)
>>> reaction_to_class = classify_reactions(reactions, classifier)
Source code in src/nasap_net/reaction_classification/execution.py
def classify_reactions(
        reactions: Iterable[Reaction],
        classifier: Callable[[Reaction], str],
) -> dict[Reaction, str]:
    """Classify reactions using a user-defined classifier function.

    Parameters
    ----------
    reactions : Iterable[Reaction]
        The reactions to classify.
    classifier : Callable[[Reaction], str]
        A function that takes a reaction and returns its class as a string.
        Should raise :class:`IncompleteReactionClassifierError` for unhandled
        reactions.

    Returns
    -------
    dict[Reaction, str]
        A mapping from each reaction to its assigned class.

    Examples
    --------
    >>> from nasap_net import classify_reactions, IncompleteReactionClassifierError
    >>> def classifier(reaction):
    ...     r = reaction.as_reaction_to_classify()
    ...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
    ...         return 'X-to-L'
    ...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
    ...         return 'L-to-X'
    ...     raise IncompleteReactionClassifierError(reaction)
    >>> reaction_to_class = classify_reactions(reactions, classifier)
    """
    result = {}
    for reaction in reactions:
        cls = classifier(reaction)
        result[reaction] = cls

        logger.info("%s -> %s", reaction, cls)

    return result

ReactionToClassify

A reaction object passed to user-defined classifier functions.

Extends :class:Reaction with additional cached properties for classification: component kinds, ring formation/breaking, and connection counts.

Attributes:

Name Type Description
metal_kind str

Kind of the metal binding site (e.g. 'M').

leaving_kind str

Kind of the leaving binding site (e.g. 'L', 'X').

entering_kind str

Kind of the entering binding site (e.g. 'L', 'X').

forming_ring_size int or None

Minimum size of rings formed in this reaction, or None.

breaking_ring_size int or None

Minimum size of rings broken in this reaction, or None.

forming_ring_size_including_temporary int or None

Like forming_ring_size, but also counts rings that are immediately broken again within the same reaction step.

init_ligand_count_on_metal int

Number of components of the entering kind bound to the metal before the reaction.

init_metal_count_on_ligand int

Number of metals bound to the entering component before the reaction.

sample_rev ReactionToClassify or None

A representative reverse reaction, or None if this object is itself a sample reverse.

Notes

Do not instantiate directly. Use Reaction.as_reaction_to_classify() or ReactionToClassify.from_reaction() instead.

Examples:

Access classification properties inside a classifier function:

>>> from nasap_net import classify_reactions, IncompleteReactionClassifierError
>>> def classifier(reaction):
...     r = reaction.as_reaction_to_classify()
...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
...         if r.forms_ring():
...             return f'X-to-L (ring, size={r.forming_ring_size})'
...         return 'X-to-L'
...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
...         return 'L-to-X'
...     raise IncompleteReactionClassifierError(reaction)
>>> reaction_to_class = classify_reactions(reactions, classifier)

Use init_ligand_count_on_metal to distinguish reactions by coordination number, and sample_rev to inspect properties of the reverse reaction:

>>> def classifier(reaction):
...     r = reaction.as_reaction_to_classify()
...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
...         n = r.init_ligand_count_on_metal
...         return f'X-to-L (coord={n})'
...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
...         rev = r.sample_rev
...         if rev is not None and rev.init_ligand_count_on_metal == 0:
...             return 'L-to-X (last ligand)'
...         return 'L-to-X'
...     raise IncompleteReactionClassifierError(reaction)
Source code in src/nasap_net/reaction_classification/models.py
class ReactionToClassify(Reaction):
    """A reaction object passed to user-defined classifier functions.

    Extends :class:`Reaction` with additional cached properties for
    classification: component kinds, ring formation/breaking, and
    connection counts.

    Attributes
    ----------
    metal_kind : str
        Kind of the metal binding site (e.g. ``'M'``).
    leaving_kind : str
        Kind of the leaving binding site (e.g. ``'L'``, ``'X'``).
    entering_kind : str
        Kind of the entering binding site (e.g. ``'L'``, ``'X'``).
    forming_ring_size : int or None
        Minimum size of rings formed in this reaction, or ``None``.
    breaking_ring_size : int or None
        Minimum size of rings broken in this reaction, or ``None``.
    forming_ring_size_including_temporary : int or None
        Like ``forming_ring_size``, but also counts rings that are
        immediately broken again within the same reaction step.
    init_ligand_count_on_metal : int
        Number of components of the entering kind bound to the metal
        before the reaction.
    init_metal_count_on_ligand : int
        Number of metals bound to the entering component before the reaction.
    sample_rev : ReactionToClassify or None
        A representative reverse reaction, or ``None`` if this object
        is itself a sample reverse.

    Notes
    -----
    Do not instantiate directly. Use ``Reaction.as_reaction_to_classify()``
    or ``ReactionToClassify.from_reaction()`` instead.

    Examples
    --------
    Access classification properties inside a classifier function:

    >>> from nasap_net import classify_reactions, IncompleteReactionClassifierError
    >>> def classifier(reaction):
    ...     r = reaction.as_reaction_to_classify()
    ...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
    ...         if r.forms_ring():
    ...             return f'X-to-L (ring, size={r.forming_ring_size})'
    ...         return 'X-to-L'
    ...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
    ...         return 'L-to-X'
    ...     raise IncompleteReactionClassifierError(reaction)
    >>> reaction_to_class = classify_reactions(reactions, classifier)

    Use ``init_ligand_count_on_metal`` to distinguish reactions by coordination
    number, and ``sample_rev`` to inspect properties of the reverse reaction:

    >>> def classifier(reaction):
    ...     r = reaction.as_reaction_to_classify()
    ...     if r.leaving_kind == 'X' and r.entering_kind == 'L':
    ...         n = r.init_ligand_count_on_metal
    ...         return f'X-to-L (coord={n})'
    ...     if r.leaving_kind == 'L' and r.entering_kind == 'X':
    ...         rev = r.sample_rev
    ...         if rev is not None and rev.init_ligand_count_on_metal == 0:
    ...             return 'L-to-X (last ligand)'
    ...         return 'L-to-X'
    ...     raise IncompleteReactionClassifierError(reaction)
    """

    _is_sample_rev: bool

    def __init__(
            self,
            init_assem,
            entering_assem,
            product_assem,
            leaving_assem,
            metal_bs,
            leaving_bs,
            entering_bs,
            duplicate_count=None,
            id_=None,
            _is_sample_rev=False,
    ):
        super().__init__(
            init_assem=init_assem,
            entering_assem=entering_assem,
            product_assem=product_assem,
            leaving_assem=leaving_assem,
            metal_bs=metal_bs,
            leaving_bs=leaving_bs,
            entering_bs=entering_bs,
            duplicate_count=duplicate_count,
            id_=id_,
        )
        object.__setattr__(self, '_is_sample_rev', _is_sample_rev)

    @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)

    @property
    def leaving_kind(self) -> str:
        """Return the kind of the leaving binding site."""
        return self.init_assem.get_component_kind_of_site(self.leaving_bs)

    @property
    def entering_kind(self) -> str:
        """Return the kind of the entering binding site."""
        return self.assem_with_entering_bs.get_component_kind_of_site(self.entering_bs)

    def forms_ring(self) -> bool:
        """Whether this reaction forms any rings."""
        return self.forming_ring_size is not None

    def breaks_ring(self) -> bool:
        """Whether this reaction breaks any rings."""
        return self.breaking_ring_size is not None

    @cached_property
    def forming_ring_size(self) -> int | None:
        """The minimum size of rings formed in this reaction,
        or None if no rings are formed.
        """
        return get_min_forming_ring_size(self)

    @cached_property
    def breaking_ring_size(self) -> int | None:
        """The minimum size of rings broken in this reaction,
        or None if no rings are broken.
        """
        return get_min_breaking_ring_size(self)

    @cached_property
    def forming_ring_size_including_temporary(self) -> int | None:
        """The minimum size of rings formed in this reaction,
        including temporary rings, or None if no rings are formed.

        Notes
        -----
        "Temporary ring" includes rings that may be broken later in the reaction.
        Example:
        X0(0)-(0)M0(1)-(0)L0(1)-(0)M1(1)-(0)L1(1)
        metal_bs = M0(1), leaving_bs = L0(0), entering_bs = L1(1)
        This reaction temporarily forms a ring of size 2, even though the ring is broken
        when L0 leaves.
        """
        return get_min_forming_ring_size_including_temporary(self)

    @cached_property
    def init_ligand_count_on_metal(self) -> int:
        """Return the number of components of the entering kind bound to the metal before the reaction."""
        return get_connection_count_of_kind(
            assembly=self.init_assem,
            source_component_id=self.metal_bs.component_id,
            target_kind=self.entering_kind,
        )

    @cached_property
    def init_metal_count_on_ligand(self) -> int:
        """Return the number of metals bound to the entering component before the reaction."""
        return get_connection_count_of_kind(
            assembly=self.assem_with_entering_bs,
            source_component_id=self.entering_bs.component_id,
            target_kind=self.metal_kind,
        )

    @cached_property
    def sample_rev(self) -> 'ReactionToClassify | None':
        """Return the reverse reaction.

        Returns None if this reaction is already a sample reverse reaction,
        to prevent infinite recursion.
        """
        if self._is_sample_rev:
            return None
        reverse_reaction = generate_sample_rev_reaction(self)
        # Create reverse reaction with _is_sample_rev=True
        result = ReactionToClassify.from_reaction(reverse_reaction)
        object.__setattr__(result, '_is_sample_rev', True)
        return result

    @property
    def assem_with_entering_bs(self) -> Assembly:
        """Return the assembly that contains the entering binding site."""
        if self.is_inter():
            return self.entering_assem_strict
        return self.init_assem

    @classmethod
    def from_reaction(cls, reaction: Reaction) -> Self:
        """Create a ReactionToClassify from a Reaction."""
        return cls(
            init_assem=reaction.init_assem,
            entering_assem=reaction.entering_assem,
            product_assem=reaction.product_assem,
            leaving_assem=reaction.leaving_assem,
            metal_bs=reaction.metal_bs,
            leaving_bs=reaction.leaving_bs,
            entering_bs=reaction.entering_bs,
            _is_sample_rev=False,
        )

assem_with_entering_bs property

Return the assembly that contains the entering binding site.

breaking_ring_size cached property

The minimum size of rings broken in this reaction, or None if no rings are broken.

entering_kind property

Return the kind of the entering binding site.

forming_ring_size cached property

The minimum size of rings formed in this reaction, or None if no rings are formed.

forming_ring_size_including_temporary cached property

The minimum size of rings formed in this reaction, including temporary rings, or None if no rings are formed.

Notes

"Temporary ring" includes rings that may be broken later in the reaction. Example: X0(0)-(0)M0(1)-(0)L0(1)-(0)M1(1)-(0)L1(1) metal_bs = M0(1), leaving_bs = L0(0), entering_bs = L1(1) This reaction temporarily forms a ring of size 2, even though the ring is broken when L0 leaves.

init_ligand_count_on_metal cached property

Return the number of components of the entering kind bound to the metal before the reaction.

init_metal_count_on_ligand cached property

Return the number of metals bound to the entering component before the reaction.

leaving_kind property

Return the kind of the leaving binding site.

metal_kind property

Return the kind of the metal binding site.

sample_rev cached property

Return the reverse reaction.

Returns None if this reaction is already a sample reverse reaction, to prevent infinite recursion.

breaks_ring()

Whether this reaction breaks any rings.

Source code in src/nasap_net/reaction_classification/models.py
def breaks_ring(self) -> bool:
    """Whether this reaction breaks any rings."""
    return self.breaking_ring_size is not None

forms_ring()

Whether this reaction forms any rings.

Source code in src/nasap_net/reaction_classification/models.py
def forms_ring(self) -> bool:
    """Whether this reaction forms any rings."""
    return self.forming_ring_size is not None

from_reaction(reaction) classmethod

Create a ReactionToClassify from a Reaction.

Source code in src/nasap_net/reaction_classification/models.py
@classmethod
def from_reaction(cls, reaction: Reaction) -> Self:
    """Create a ReactionToClassify from a Reaction."""
    return cls(
        init_assem=reaction.init_assem,
        entering_assem=reaction.entering_assem,
        product_assem=reaction.product_assem,
        leaving_assem=reaction.leaving_assem,
        metal_bs=reaction.metal_bs,
        leaving_bs=reaction.leaving_bs,
        entering_bs=reaction.entering_bs,
        _is_sample_rev=False,
    )