Skip to content

Assembly Enumeration

enumerate_assemblies(template, *, leaving_ligand, leaving_ligand_site=None, metal_kinds, symmetry_operations=None, comp_kind_order_in_formula=None)

Enumerate assemblies which can be formed by adding the leaving ligand to the fragments of the template assembly.

This function generates all possible assemblies by attaching the specified leaving ligand to each fragment in the template assembly. It ensures that the resulting assemblies are unique by excluding duplicates based on symmetry operations and isomorphism checks.

Any two assemblies that meet either of the following conditions are considered duplicates, and only one of them is included in the output list: - One can be transformed into the other by applying a symmetry operation from the provided list. - They are isomorphic, i.e., they have the same connectivity structure.

Providing symmetry operations is strongly recommended to reduce the computational cost of duplicate exclusion.

Parameters:

Name Type Description Default
template Assembly

The template assembly to base the enumeration on.

required
leaving_ligand Component

The ligand to be added to the fragments of the template assembly.

required
leaving_ligand_site ID | None

The site ID on the leaving ligand where it will be attached. If None, the first site ID of the leaving ligand will be used. Default is None.

None
metal_kinds Iterable[str]

The kinds of metal components in the assembly. All binding sites on these components will be considered for attaching the leaving ligand.

required
symmetry_operations Iterable[Mapping[Any, ID]] | None

A list of symmetry operations to consider when excluding duplicate assemblies. Each symmetry operation is represented as a mapping from original site IDs to transformed site IDs.

None
comp_kind_order_in_formula Sequence[str] | None

The order of component kinds to use when assigning composition formula IDs to the assemblies. If None, the kinds will be sorted alphabetically. Default is None.

None

Returns:

Type Description
list[Assembly]

A list of unique assemblies formed by adding the leaving ligand to the fragments of the template assembly. Also includes the free leaving ligand as an assembly.

Examples:

>>> from nasap_net import Component, Assembly, Bond, enumerate_assemblies
>>> M = Component(kind='M', sites=[0, 1])
>>> L = Component(kind='L', sites=[0, 1])
>>> X = Component(kind='X', sites=[0])
>>> 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),
...     ]
... )
>>> assemblies = enumerate_assemblies(M2L2, leaving_ligand=X, metal_kinds='M')
Source code in src/nasap_net/assembly_enumeration/core.py
def enumerate_assemblies(
        template: Assembly,
        *,
        leaving_ligand: Component,
        leaving_ligand_site: ID | None = None,
        metal_kinds: Iterable[str],
        symmetry_operations: Iterable[Mapping[Any, ID]] | None = None,
        comp_kind_order_in_formula: Sequence[str] | None = None,
) -> list[Assembly]:
    """Enumerate assemblies which can be formed by adding the leaving ligand
    to the fragments of the template assembly.

    This function generates all possible assemblies by attaching the
    specified leaving ligand to each fragment in the template assembly. It
    ensures that the resulting assemblies are unique by excluding duplicates
    based on symmetry operations and isomorphism checks.

    Any two assemblies that meet either of the following conditions are
    considered duplicates, and only one of them is included in the output list:
    - One can be transformed into the other by applying a symmetry operation
      from the provided list.
    - They are isomorphic, i.e., they have the same connectivity structure.

    Providing symmetry operations is strongly recommended to reduce the
    computational cost of duplicate exclusion.

    Parameters
    ----------
    template : Assembly
        The template assembly to base the enumeration on.
    leaving_ligand : Component
        The ligand to be added to the fragments of the template assembly.
    leaving_ligand_site : ID | None, optional
        The site ID on the leaving ligand where it will be attached.
        If None, the first site ID of the leaving ligand will be used.
        Default is None.
    metal_kinds : Iterable[str]
        The kinds of metal components in the assembly. All binding sites on
        these components will be considered for attaching the leaving ligand.
    symmetry_operations : Iterable[Mapping[Any, ID]] | None, optional
        A list of symmetry operations to consider when excluding duplicate
        assemblies. Each symmetry operation is represented as a mapping from
        original site IDs to transformed site IDs.
    comp_kind_order_in_formula : Sequence[str] | None, optional
        The order of component kinds to use when assigning composition formula
        IDs to the assemblies. If None, the kinds will be sorted alphabetically.
        Default is None.

    Returns
    -------
    list[Assembly]
        A list of unique assemblies formed by adding the leaving ligand
        to the fragments of the template assembly.
        Also includes the free leaving ligand as an assembly.

    Examples
    --------
    >>> from nasap_net import Component, Assembly, Bond, enumerate_assemblies
    >>> M = Component(kind='M', sites=[0, 1])
    >>> L = Component(kind='L', sites=[0, 1])
    >>> X = Component(kind='X', sites=[0])
    >>> 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),
    ...     ]
    ... )
    >>> assemblies = enumerate_assemblies(M2L2, leaving_ligand=X, metal_kinds='M')
    """
    logger.info('Enumerating substructure fragments...')
    fragments = enumerate_fragments(
        template,
        symmetry_operations=symmetry_operations,
    )
    logger.info('Removing symmetry-equivalent and isomorphic duplicates...')
    unique_fragments = extract_unique_assemblies(
        fragments,
    )
    logger.info('%d unique fragment(s) remain.', len(unique_fragments))
    logger.info('Attaching leaving ligands to free metal sites...')
    capped_assemblies = cap_assemblies_with_ligand(
        unique_fragments,
        component=leaving_ligand,
        component_site_id=leaving_ligand_site,
        metal_kinds=metal_kinds,
    )
    # Also include the free leaving ligand as an assembly
    capped_assemblies.add(
        Assembly(
            components={f'{leaving_ligand.kind}0': leaving_ligand},
            bonds=[],
        )
    )
    assemblies_with_ids = assign_composition_formula_ids(
        assemblies=capped_assemblies,
        order=comp_kind_order_in_formula,
    )
    logger.info('Enumeration complete. Total assemblies: %d.', len(assemblies_with_ids))
    return assemblies_with_ids

SymmetryOperations dataclass

Source code in src/nasap_net/assembly_enumeration/lib/symmetry_operation.py
@dataclass
class SymmetryOperations(UserDict):
    data: dict[str, Mapping[Any, ID]] = field(default_factory=dict)

    def add_mapping(self, name: str, mapping: Mapping[Any, ID]) -> None:
        self.data[name] = mapping

    def add_cyclic_permutation(
            self,
            name: str,
            cyclic_permutation: Iterable[Sequence[ID]]
    ) -> None:
        self.add_mapping(name, cyclic_perms_to_map(cyclic_permutation))

    def add_product(
            self,
            name: str,
            mapping_names: Iterable[str]
    ) -> None:
        mappings = [
            self.data[mapping_name]
            for mapping_name in list(mapping_names)
        ]
        self.add_mapping(name, resolve_chain_map(*mappings))

enumerate_assemblies_capped_with_assembly(assemblies, component_kind, capping_assembly, capping_assembly_site)

Source code in src/nasap_net/assembly_enumeration/lib/capping_with_assembly.py
def enumerate_assemblies_capped_with_assembly(
        assemblies: Iterable[Assembly],
        component_kind: str,
        capping_assembly: Assembly,
        capping_assembly_site: BindingSite,
) -> set[Assembly]:
    result: set[Assembly] = set()
    for assembly in assemblies:
        result.update(_enumerate_capped(
            assembly=assembly,
            component_kind=component_kind,
            capping_assembly=capping_assembly,
            capping_assembly_site=capping_assembly_site,
        ))
    return extract_unique_assemblies(result)