Skip to content

molpy.reacter

Programmable Reacter Module for Chemical Transformations.

This module provides a framework for defining and executing chemical reactions within the molpy framework, following SMIRKS-style semantics but working entirely on native data structures (Atom, Bond, Struct, Monomer).

Core Concepts:

  • Reacter: Represents a single chemical reaction type
  • ProductSet: Container for reaction products and metadata
  • Selectors: Functions that identify anchor atoms and leaving groups
  • Transformers: Functions that create or modify bonds

Example Usage:

from molpy.reacter import Reacter, port_anchor_selector, remove_one_H, make_single_bond

# Define a C-C coupling reaction
cc_coupling = Reacter(
    name="C-C_coupling_with_H_loss",
    anchor_left=port_anchor_selector,
    anchor_right=port_anchor_selector,
    leaving_left=remove_one_H,
    leaving_right=remove_one_H,
    bond_maker=make_single_bond,
)

# Execute reaction between two monomers
product = cc_coupling.run(monoA, monoB, port_L="1", port_R="2")
print(f"Removed atoms: {product.notes['removed_atoms']}")
print(f"New bonds: {product.notes['new_bonds']}")

Design Goals:

  • Pure Python, framework-native (no RDKit)
  • Composable: reaction logic = modular functions
  • Stable indexing: atom deletion doesn't shift IDs
  • Single responsibility: one Reacter = one reaction type
  • Extensible: easy to subclass for specialized reactions
  • Auditable: all changes recorded in ProductSet.notes

ProductSet dataclass

ProductSet(product, notes=dict())

Container for reaction products and metadata.

Attributes:

Name Type Description
product Atomistic

The resulting Atomistic assembly after reaction

notes dict[str, Any]

Dictionary containing execution metadata: - 'reaction_name': str - 'removed_atoms': List of removed atom entities - 'removed_count': int - 'anchor_left': Entity - 'anchor_right': Entity - 'entity_maps': List of entity mappings from merge - 'new_bonds': List of newly created bonds - 'new_angles': List of newly created angles (if computed) - 'new_dihedrals': List of newly created dihedrals (if computed) - 'modified_atoms': List of atoms whose types may have changed - 'needs_retypification': bool indicating if retypification needed

Reacter

Reacter(name, anchor_left, anchor_right, leaving_left, leaving_right, bond_maker)

Programmable chemical reaction executor.

A Reacter represents one specific chemical reaction type by composing: 1. Anchor selectors - identify reactive atoms via ports 2. Leaving group selectors - identify atoms to remove 3. Bond maker - create new bonds between anchors

The reaction is executed on copies of input monomers, ensuring original structures remain unchanged.

Port Selection Philosophy: Reacter does NOT handle port selection. The caller (e.g., ReacterConnector) must explicitly specify which ports to connect via port_L and port_R. This makes the reaction execution deterministic and explicit.

Attributes:

Name Type Description
name

Descriptive name for this reaction type

anchor_left

Function to select left anchor atom

anchor_right

Function to select right anchor atom

leaving_left

Function to select left leaving group

leaving_right

Function to select right leaving group

bond_maker

Function to create bond between anchors

Example

from molpy.reacter import Reacter, port_anchor_selector, remove_one_H, make_single_bond

cc_coupling = Reacter( ... name="C-C_coupling_with_H_loss", ... anchor_left=port_anchor_selector, ... anchor_right=port_anchor_selector, ... leaving_left=remove_one_H, ... leaving_right=remove_one_H, ... bond_maker=make_single_bond, ... )

Port selection is explicit!

product = cc_coupling.run(monomerA, monomerB, port_L="1", port_R="2") print(product.notes['removed_atoms']) # [H1, H2]

Initialize a Reacter with reaction components.

Parameters:

Name Type Description Default
name str

Descriptive name for this reaction

required
anchor_left AnchorSelector

Selector for left anchor atom

required
anchor_right AnchorSelector

Selector for right anchor atom

required
leaving_left LeavingSelector

Selector for left leaving group

required
leaving_right LeavingSelector

Selector for right leaving group

required
bond_maker BondMaker

Function to create bond between anchors

required
Source code in src/molpy/reacter/base.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def __init__(
    self,
    name: str,
    anchor_left: AnchorSelector,
    anchor_right: AnchorSelector,
    leaving_left: LeavingSelector,
    leaving_right: LeavingSelector,
    bond_maker: BondMaker,
):
    """
    Initialize a Reacter with reaction components.

    Args:
        name: Descriptive name for this reaction
        anchor_left: Selector for left anchor atom
        anchor_right: Selector for right anchor atom
        leaving_left: Selector for left leaving group
        leaving_right: Selector for right leaving group
        bond_maker: Function to create bond between anchors
    """
    self.name = name
    self.anchor_left = anchor_left
    self.anchor_right = anchor_right
    self.leaving_left = leaving_left
    self.leaving_right = leaving_right
    self.bond_maker = bond_maker

run

run(left, right, port_L, port_R, compute_topology=True, record_intermediates=False)

Execute the reaction between two monomers.

IMPORTANT: port_L and port_R must be explicitly specified. No automatic port selection is performed.

Workflow (STRICT ORDER): 1. Check ports exist on monomers 2. Select anchors via ports 3. Merge right into left (direct transfer, no copy) 4. Create bond between anchors 5. Remove leaving groups from MERGED assembly 6. (Optional) Compute new angles/dihedrals 7. Return ProductSet with metadata

Parameters:

Name Type Description Default
left Monomer

Left reactant monomer

required
right Monomer

Right reactant monomer

required
port_L str

Port name on left monomer (REQUIRED - must be explicit)

required
port_R str

Port name on right monomer (REQUIRED - must be explicit)

required
compute_topology bool

If True, compute new angles/dihedrals (default True)

True
record_intermediates bool

If True, record intermediate states in notes

False

Returns:

Type Description
ProductSet

ProductSet containing: - product: Final product assembly - notes: Metadata including intermediate states if requested

Raises:

Type Description
ValueError

If ports not found or anchors invalid

Source code in src/molpy/reacter/base.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def run(
    self,
    left: Monomer,
    right: Monomer,
    port_L: str,
    port_R: str,
    compute_topology: bool = True,
    record_intermediates: bool = False,
) -> ProductSet:
    """
    Execute the reaction between two monomers.

    **IMPORTANT: port_L and port_R must be explicitly specified.**
    No automatic port selection is performed.

    Workflow (STRICT ORDER):
    1. Check ports exist on monomers
    2. Select anchors via ports
    3. Merge right into left (direct transfer, no copy)
    4. Create bond between anchors
    5. Remove leaving groups from MERGED assembly
    6. (Optional) Compute new angles/dihedrals
    7. Return ProductSet with metadata

    Args:
        left: Left reactant monomer
        right: Right reactant monomer
        port_L: Port name on left monomer (REQUIRED - must be explicit)
        port_R: Port name on right monomer (REQUIRED - must be explicit)
        compute_topology: If True, compute new angles/dihedrals (default True)
        record_intermediates: If True, record intermediate states in notes

    Returns:
        ProductSet containing:
            - product: Final product assembly
            - notes: Metadata including intermediate states if requested

    Raises:
        ValueError: If ports not found or anchors invalid
    """
    intermediates: list[dict] | None = [] if record_intermediates else None

    # Step 1: Check ports exist on monomers
    left_port = left.get_port_def(port_L)
    right_port = right.get_port_def(port_R)

    if left_port is None:
        raise ValueError(f"Port '{port_L}' not found on left monomer")
    if right_port is None:
        raise ValueError(f"Port '{port_R}' not found on right monomer")

    # Step 2: Select anchors via ports
    anchor_L = left_port.target
    anchor_R = right_port.target

    # Get unwrapped assemblies
    left_asm = left.unwrap()
    right_asm = right.unwrap()

    if intermediates is not None:
        # Record initial state (copy first, then gen_topo on the copy!)
        left_copy = left_asm.copy()
        right_copy = right_asm.copy()
        if compute_topology:
            left_copy.get_topo(gen_angle=True, gen_dihe=True)
            right_copy.get_topo(gen_angle=True, gen_dihe=True)

        intermediates.append(
            {
                "step": "initial",
                "description": "Initial reactants (Step 1-2: validated ports and anchors)",
                "left": left_copy,
                "right": right_copy,
            }
        )

    # Step 3: Merge right into left (direct transfer, entities are SAME objects)
    left_asm.merge(right_asm)

    if intermediates is not None:
        product_copy = left_asm.copy()
        if compute_topology:
            product_copy.get_topo(gen_angle=True, gen_dihe=True)

        intermediates.append(
            {
                "step": "merge",
                "description": "After merging right into left (Step 3)",
                "product": product_copy,
            }
        )

    # Step 4: Create bond between anchors
    new_bond = self.bond_maker(left_asm, anchor_L, anchor_R)
    if new_bond is not None:
        left_asm.add_link(new_bond, include_endpoints=False)

    if intermediates is not None:
        product_copy = left_asm.copy()
        if compute_topology:
            product_copy.get_topo(gen_angle=True, gen_dihe=True)

        intermediates.append(
            {
                "step": "bond_formation",
                "description": "After forming new bond between anchors (Step 4)",
                "product": product_copy,
                "new_bond": new_bond,
            }
        )

    # Step 5: Remove leaving groups from MERGED assembly
    # IMPORTANT: After merge, right_asm's entities are moved to left_asm,
    # so we need to wrap left_asm in a temporary monomer for the selector
    merged_monomer = Monomer(left_asm)
    leaving_L = self.leaving_left(merged_monomer, anchor_L)
    leaving_R = self.leaving_right(merged_monomer, anchor_R)

    if intermediates is not None:
        intermediates.append(
            {
                "step": "identify_leaving",
                "description": f"Identified leaving groups (Step 5a): {len(leaving_L)} from left anchor, {len(leaving_R)} from right anchor",
                "leaving_L": leaving_L,
                "leaving_R": leaving_R,
            }
        )

    removed_atoms = []
    if leaving_L:
        left_asm.remove_entity(*leaving_L, drop_incident_links=True)
        removed_atoms.extend(leaving_L)
    if leaving_R:
        left_asm.remove_entity(*leaving_R, drop_incident_links=True)
        removed_atoms.extend(leaving_R)

    if intermediates is not None:
        product_copy = left_asm.copy()
        if compute_topology:
            product_copy.get_topo(gen_angle=True, gen_dihe=True)

        intermediates.append(
            {
                "step": "remove_leaving",
                "description": f"After removing {len(removed_atoms)} leaving atoms (Step 5b)",
                "product": product_copy,
            }
        )

    # Step 6: (Optional) Compute new angles/dihedrals
    if compute_topology:
        topo = left_asm.get_topo(gen_angle=True, gen_dihe=True)

        if intermediates is not None:
            product_copy = left_asm.copy()
            product_copy.get_topo(gen_angle=True, gen_dihe=True)

            intermediates.append(
                {
                    "step": "topology",
                    "description": "Final topology computation (Step 6)",
                    "product": product_copy,
                    "n_angles": topo.n_angles,
                    "n_dihedrals": topo.n_dihedrals,
                }
            )

    # Step 7: Return ProductSet with metadata
    notes = {
        "reaction_name": self.name,
        "removed_atoms": removed_atoms,
        "removed_count": len(removed_atoms),
        "anchor_left": anchor_L,
        "anchor_right": anchor_R,
        "port_L": port_L,
        "port_R": port_R,
        "new_bonds": [new_bond] if new_bond else [],
        "needs_retypification": True,
    }

    if record_intermediates:
        notes["intermediates"] = intermediates

    return ProductSet(product=left_asm, notes=notes)

ReacterConnector

ReacterConnector(default, overrides=None)

Connector adapter that manages Reacter instances for polymer assembly.

This connector allows specifying a default reaction for most connections, with the ability to override specific monomer pair connections with specialized reacters.

Port Selection Philosophy: This connector does NOT handle port selection. The caller must explicitly provide port_L and port_R when calling connect(). Port selection should be handled by the higher-level builder logic.

Example

from molpy.reacter import Reacter, ReacterConnector

Default reaction for most connections

default_reacter = Reacter(...)

Special reaction for A-B connection

special_reacter = Reacter(...)

connector = ReacterConnector( ... default=default_reacter, ... overrides={('A', 'B'): special_reacter} ... )

Explicit port specification required

product = connector.connect( ... left=monomer_a, right=monomer_b, ... left_type='A', right_type='B', ... port_L='1', port_R='2' # REQUIRED ... )

Initialize connector with default and override reacters.

Parameters:

Name Type Description Default
default Reacter

Default Reacter used for most connections

required
overrides dict[tuple[str, str], Reacter] | None

Dict mapping (left_type, right_type) -> specialized Reacter

None
Source code in src/molpy/reacter/connector.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    default: Reacter,
    overrides: dict[tuple[str, str], Reacter] | None = None,
):
    """
    Initialize connector with default and override reacters.

    Args:
        default: Default Reacter used for most connections
        overrides: Dict mapping (left_type, right_type) -> specialized Reacter
    """
    self.default = default
    self.overrides = overrides or {}
    self._history: list[ProductSet] = []

clear_history

clear_history()

Clear reaction history.

Source code in src/molpy/reacter/connector.py
166
167
168
def clear_history(self):
    """Clear reaction history."""
    self._history.clear()

connect

connect(left, right, port_L, port_R, left_type=None, right_type=None)

Connect two monomers using appropriate reacter.

IMPORTANT: port_L and port_R must be explicitly specified. No automatic port selection or fallback is performed.

Parameters:

Name Type Description Default
left Monomer

Left monomer

required
right Monomer

Right monomer

required
port_L str

Port name on left monomer (REQUIRED)

required
port_R str

Port name on right monomer (REQUIRED)

required
left_type str | None

Type label for left monomer (e.g., 'A', 'B')

None
right_type str | None

Type label for right monomer

None

Returns:

Type Description
Atomistic

Connected Atomistic assembly

Raises:

Type Description
ValueError

If ports not found on monomers

Source code in src/molpy/reacter/connector.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def connect(
    self,
    left: Monomer,
    right: Monomer,
    port_L: str,
    port_R: str,
    left_type: str | None = None,
    right_type: str | None = None,
) -> Atomistic:
    """
    Connect two monomers using appropriate reacter.

    **IMPORTANT: port_L and port_R must be explicitly specified.**
    No automatic port selection or fallback is performed.

    Args:
        left: Left monomer
        right: Right monomer
        port_L: Port name on left monomer (REQUIRED)
        port_R: Port name on right monomer (REQUIRED)
        left_type: Type label for left monomer (e.g., 'A', 'B')
        right_type: Type label for right monomer

    Returns:
        Connected Atomistic assembly

    Raises:
        ValueError: If ports not found on monomers
    """
    # Validate ports exist
    if port_L not in left.ports:
        raise ValueError(
            f"Port '{port_L}' not found on left monomer. "
            f"Available ports: {list(left.ports.keys())}"
        )
    if port_R not in right.ports:
        raise ValueError(
            f"Port '{port_R}' not found on right monomer. "
            f"Available ports: {list(right.ports.keys())}"
        )

    # Select reacter based on monomer types
    reacter = self._select_reacter(left_type, right_type)

    # Execute reaction
    product = reacter.run(left, right, port_L=port_L, port_R=port_R)

    # Store in history for retypification
    self._history.append(product)

    return product.product

get_all_modified_atoms

get_all_modified_atoms()

Get all atoms that have been modified across all reactions.

Returns:

Type Description
set

Set of all atoms that need retypification

Source code in src/molpy/reacter/connector.py
145
146
147
148
149
150
151
152
153
154
155
def get_all_modified_atoms(self) -> set:
    """
    Get all atoms that have been modified across all reactions.

    Returns:
        Set of all atoms that need retypification
    """
    modified = set()
    for product in self._history:
        modified.update(product.notes.get("modified_atoms", []))
    return modified

get_history

get_history()

Get history of all reactions performed.

Useful for batch retypification after polymer assembly.

Returns:

Type Description
list[ProductSet]

List of ProductSet for each connection made

Source code in src/molpy/reacter/connector.py
134
135
136
137
138
139
140
141
142
143
def get_history(self) -> list[ProductSet]:
    """
    Get history of all reactions performed.

    Useful for batch retypification after polymer assembly.

    Returns:
        List of ProductSet for each connection made
    """
    return self._history.copy()

needs_retypification

needs_retypification()

Check if any reactions require retypification.

Returns:

Type Description
bool

True if retypification needed

Source code in src/molpy/reacter/connector.py
157
158
159
160
161
162
163
164
def needs_retypification(self) -> bool:
    """
    Check if any reactions require retypification.

    Returns:
        True if retypification needed
    """
    return any(p.notes.get("needs_retypification", False) for p in self._history)

break_bond

break_bond(assembly, i, j)

Remove existing bond between two atoms.

Opposite of bond makers - used for bond-breaking reactions.

Parameters:

Name Type Description Default
assembly Atomistic

Struct containing the bond

required
i Entity

First atom

required
j Entity

Second atom

required
Side effects

Removes bond from assembly.links

Example

break_bond(assembly, carbon1, oxygen1)

Breaks C-O bond

Source code in src/molpy/reacter/transformers.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def break_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Remove existing bond between two atoms.

    Opposite of bond makers - used for bond-breaking reactions.

    Args:
        assembly: Struct containing the bond
        i: First atom
        j: Second atom

    Side effects:
        Removes bond from assembly.links

    Example:
        >>> break_bond(assembly, carbon1, oxygen1)
        >>> # Breaks C-O bond
    """
    existing = get_bond_between(assembly, i, j)
    if existing is not None:
        assembly.remove_link(existing)

create_atom_mapping

create_atom_mapping(pre_atoms, post_atoms)

Create atom mapping between pre-reaction and post-reaction states.

Creates a mapping of atom IDs from pre-reaction template to post-reaction template. This is used to generate map files for LAMMPS fix bond/react.

Parameters:

Name Type Description Default
pre_atoms list

List of atoms in pre-reaction state

required
post_atoms list

List of atoms in post-reaction state

required

Returns:

Type Description
dict[int, int]

Dictionary mapping pre-reaction atom indices to post-reaction indices

dict[int, int]

(1-indexed for LAMMPS)

Note

Atoms that are deleted during the reaction will not appear in the mapping. Atoms are matched by their 'id' attribute if present, otherwise by position.

Example

from molpy.reacter.utils import create_atom_mapping mapping = create_atom_mapping(pre_atoms, post_atoms)

Write to map file

with open("reaction.map", 'w') as f: ... for pre_id, post_id in sorted(mapping.items()): ... f.write(f"{pre_id} {post_id}\n")

Source code in src/molpy/reacter/utils.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def create_atom_mapping(pre_atoms: list, post_atoms: list) -> dict[int, int]:
    """Create atom mapping between pre-reaction and post-reaction states.

    Creates a mapping of atom IDs from pre-reaction template to post-reaction
    template. This is used to generate map files for LAMMPS fix bond/react.

    Args:
        pre_atoms: List of atoms in pre-reaction state
        post_atoms: List of atoms in post-reaction state

    Returns:
        Dictionary mapping pre-reaction atom indices to post-reaction indices
        (1-indexed for LAMMPS)

    Note:
        Atoms that are deleted during the reaction will not appear in the mapping.
        Atoms are matched by their 'id' attribute if present, otherwise by position.

    Example:
        >>> from molpy.reacter.utils import create_atom_mapping
        >>> mapping = create_atom_mapping(pre_atoms, post_atoms)
        >>> # Write to map file
        >>> with open("reaction.map", 'w') as f:
        ...     for pre_id, post_id in sorted(mapping.items()):
        ...         f.write(f"{pre_id} {post_id}\\n")
    """
    mapping = {}

    # Try matching by atom 'id' first
    pre_id_map = {}
    for i, atom in enumerate(pre_atoms):
        atom_id = atom.get("id")
        if atom_id is not None:
            pre_id_map[atom_id] = i + 1

    post_id_map = {}
    for i, atom in enumerate(post_atoms):
        atom_id = atom.get("id")
        if atom_id is not None:
            post_id_map[atom_id] = i + 1

    # Match atoms by 'id' attribute
    for atom_id, pre_idx in pre_id_map.items():
        if atom_id in post_id_map:
            mapping[pre_idx] = post_id_map[atom_id]

    # If no matches by ID, try matching by position (for atoms without IDs)
    if not mapping:
        for i, pre_atom in enumerate(pre_atoms):
            pre_pos = pre_atom.get("xyz", pre_atom.get("xyz"))
            if pre_pos is None:
                continue

            for j, post_atom in enumerate(post_atoms):
                post_pos = post_atom.get("xyz", post_atom.get("xyz"))
                if post_pos is None:
                    continue

                # Check if positions are close (within 0.01 Angstrom)
                dist_sq = sum((pre_pos[k] - post_pos[k]) ** 2 for k in range(3))
                if dist_sq < 1e-4:
                    mapping[i + 1] = j + 1
                    break

    return mapping

find_neighbors

find_neighbors(assembly, atom, *, element=None)

Find neighboring atoms of a given atom.

Parameters:

Name Type Description Default
assembly Atomistic

Atomistic assembly containing the atom

required
atom Entity

Atom entity to find neighbors of

required
element str | None

Optional element symbol to filter by (e.g., 'H', 'C')

None

Returns:

Type Description
list[Entity]

List of neighboring atom entities

Example

h_neighbors = find_neighbors(asm, carbon_atom, element='H') all_neighbors = find_neighbors(asm, carbon_atom)

Source code in src/molpy/reacter/utils.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def find_neighbors(
    assembly: Atomistic,
    atom: Entity,
    *,
    element: str | None = None,
) -> list[Entity]:
    """
    Find neighboring atoms of a given atom.

    Args:
        assembly: Atomistic assembly containing the atom
        atom: Atom entity to find neighbors of
        element: Optional element symbol to filter by (e.g., 'H', 'C')

    Returns:
        List of neighboring atom entities

    Example:
        >>> h_neighbors = find_neighbors(asm, carbon_atom, element='H')
        >>> all_neighbors = find_neighbors(asm, carbon_atom)
    """
    neighbors: list[Entity] = []

    # Look through all bonds
    for bond in assembly.links.bucket(Bond):
        # Use identity check (is) not equality check (==)
        if any(ep is atom for ep in bond.endpoints):
            # Found a bond involving this atom
            for endpoint in bond.endpoints:
                if endpoint is not atom:
                    # Filter by element if specified
                    if element is None or endpoint.get("symbol") == element:
                        neighbors.append(endpoint)

    return neighbors

make_aromatic_bond

make_aromatic_bond(assembly, i, j)

Create an aromatic bond between two atoms.

If a bond already exists, updates it to aromatic (order=1.5 by convention).

Parameters:

Name Type Description Default
assembly Atomistic

Struct to add bond to

required
i Entity

First atom

required
j Entity

Second atom

required
Side effects

Adds Bond(i, j, order=1.5, kind=':') to assembly.links

Example

make_aromatic_bond(merged, carbon1, carbon2)

Creates C:C aromatic bond

Source code in src/molpy/reacter/transformers.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def make_aromatic_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Create an aromatic bond between two atoms.

    If a bond already exists, updates it to aromatic (order=1.5 by convention).

    Args:
        assembly: Struct to add bond to
        i: First atom
        j: Second atom

    Side effects:
        Adds Bond(i, j, order=1.5, kind=':') to assembly.links

    Example:
        >>> make_aromatic_bond(merged, carbon1, carbon2)
        >>> # Creates C:C aromatic bond
    """
    existing = get_bond_between(assembly, i, j)

    if existing is not None:
        existing["order"] = 1.5
        existing["kind"] = ":"
        existing["aromatic"] = True
    else:
        bond = Bond(cast(Atom, i), cast(Atom, j), order=1.5, kind=":", aromatic=True)
        assembly.add_link(bond, include_endpoints=False)

make_bond_by_order

make_bond_by_order(order)

Factory function to create bond maker with specific order.

Parameters:

Name Type Description Default
order int

Bond order (1, 2, 3, or 1.5 for aromatic)

required

Returns:

Type Description
BondMaker

BondMaker function that creates bonds with specified order

Example

double_bond_maker = make_bond_by_order(2) reacter = Reacter( ... bond_maker=double_bond_maker, ... ... ... )

Source code in src/molpy/reacter/transformers.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def make_bond_by_order(order: int) -> BondMaker:
    """
    Factory function to create bond maker with specific order.

    Args:
        order: Bond order (1, 2, 3, or 1.5 for aromatic)

    Returns:
        BondMaker function that creates bonds with specified order

    Example:
        >>> double_bond_maker = make_bond_by_order(2)
        >>> reacter = Reacter(
        ...     bond_maker=double_bond_maker,
        ...     ...
        ... )
    """

    def bond_maker(assembly: Atomistic, i: Entity, j: Entity) -> None:
        existing = get_bond_between(assembly, i, j)

        # Determine kind symbol
        kind_map = {1: "-", 2: "=", 3: "#", 1.5: ":"}
        kind = kind_map.get(order, "-")

        if existing is not None:
            existing["order"] = order
            existing["kind"] = kind
            if order == 1.5:
                existing["aromatic"] = True
        else:
            attrs = {"order": order, "kind": kind}
            if order == 1.5:
                attrs["aromatic"] = True
            bond = Bond(cast(Atom, i), cast(Atom, j), **attrs)
            assembly.add_link(bond, include_endpoints=False)

    return bond_maker

make_double_bond

make_double_bond(assembly, i, j)

Create a double bond between two atoms.

If a bond already exists, updates it to double bond (order=2).

Parameters:

Name Type Description Default
assembly Atomistic

Struct to add bond to

required
i Entity

First atom

required
j Entity

Second atom

required
Side effects

Adds Bond(i, j, order=2) to assembly.links

Example

make_double_bond(merged, carbon1, carbon2)

Creates C=C double bond

Source code in src/molpy/reacter/transformers.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def make_double_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Create a double bond between two atoms.

    If a bond already exists, updates it to double bond (order=2).

    Args:
        assembly: Struct to add bond to
        i: First atom
        j: Second atom

    Side effects:
        Adds Bond(i, j, order=2) to assembly.links

    Example:
        >>> make_double_bond(merged, carbon1, carbon2)
        >>> # Creates C=C double bond
    """
    existing = get_bond_between(assembly, i, j)

    if existing is not None:
        existing["order"] = 2
        existing["kind"] = "="
    else:
        bond = Bond(cast(Atom, i), cast(Atom, j), order=2, kind="=")
        assembly.add_link(bond, include_endpoints=False)

make_single_bond

make_single_bond(assembly, i, j)

Create a single bond between two atoms.

If a bond already exists, updates it to single bond (order=1).

Parameters:

Name Type Description Default
assembly Atomistic

Struct to add bond to

required
i Entity

First atom

required
j Entity

Second atom

required
Side effects

Adds Bond(i, j, order=1) to assembly.links

Example

make_single_bond(merged, carbon1, carbon2)

Creates C-C single bond

Source code in src/molpy/reacter/transformers.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def make_single_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Create a single bond between two atoms.

    If a bond already exists, updates it to single bond (order=1).

    Args:
        assembly: Struct to add bond to
        i: First atom
        j: Second atom

    Side effects:
        Adds Bond(i, j, order=1) to assembly.links

    Example:
        >>> make_single_bond(merged, carbon1, carbon2)
        >>> # Creates C-C single bond
    """
    # Check if bond exists
    existing = get_bond_between(assembly, i, j)

    if existing is not None:
        # Update existing bond
        existing["order"] = 1
        existing["kind"] = "-"
    else:
        # Create new bond
        bond = Bond(cast(Atom, i), cast(Atom, j), order=1, kind="-")
        assembly.add_link(bond, include_endpoints=False)

make_triple_bond

make_triple_bond(assembly, i, j)

Create a triple bond between two atoms.

If a bond already exists, updates it to triple bond (order=3).

Parameters:

Name Type Description Default
assembly Atomistic

Struct to add bond to

required
i Entity

First atom

required
j Entity

Second atom

required
Side effects

Adds Bond(i, j, order=3) to assembly.links

Example

make_triple_bond(merged, carbon1, carbon2)

Creates C≡C triple bond

Source code in src/molpy/reacter/transformers.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def make_triple_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Create a triple bond between two atoms.

    If a bond already exists, updates it to triple bond (order=3).

    Args:
        assembly: Struct to add bond to
        i: First atom
        j: Second atom

    Side effects:
        Adds Bond(i, j, order=3) to assembly.links

    Example:
        >>> make_triple_bond(merged, carbon1, carbon2)
        >>> # Creates C≡C triple bond
    """
    existing = get_bond_between(assembly, i, j)

    if existing is not None:
        existing["order"] = 3
        existing["kind"] = "#"
    else:
        bond = Bond(cast(Atom, i), cast(Atom, j), order=3, kind="#")
        assembly.add_link(bond, include_endpoints=False)

no_leaving_group

no_leaving_group(monomer, anchor)

No leaving group - returns empty list.

Useful for addition reactions where nothing is eliminated.

Parameters:

Name Type Description Default
monomer Monomer

Monomer (ignored)

required
anchor Entity

Anchor atom (ignored)

required

Returns:

Type Description
list[Entity]

Empty list

Example

reacter = Reacter( ... leaving_left=no_leaving_group, ... leaving_right=remove_one_H, ... ... ... )

Source code in src/molpy/reacter/selectors.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def no_leaving_group(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    No leaving group - returns empty list.

    Useful for addition reactions where nothing is eliminated.

    Args:
        monomer: Monomer (ignored)
        anchor: Anchor atom (ignored)

    Returns:
        Empty list

    Example:
        >>> reacter = Reacter(
        ...     leaving_left=no_leaving_group,
        ...     leaving_right=remove_one_H,
        ...     ...
        ... )
    """
    return []

no_new_bond

no_new_bond(assembly, i, j)

Do not create any bond.

Useful for reactions that only remove atoms without forming new bonds.

Parameters:

Name Type Description Default
assembly Atomistic

Struct (ignored)

required
i Entity

First atom (ignored)

required
j Entity

Second atom (ignored)

required
Side effects

None

Example

reacter = Reacter( ... bond_maker=no_new_bond, # Just remove leaving groups ... ... ... )

Source code in src/molpy/reacter/transformers.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def no_new_bond(assembly: Atomistic, i: Entity, j: Entity) -> None:
    """
    Do not create any bond.

    Useful for reactions that only remove atoms without forming new bonds.

    Args:
        assembly: Struct (ignored)
        i: First atom (ignored)
        j: Second atom (ignored)

    Side effects:
        None

    Example:
        >>> reacter = Reacter(
        ...     bond_maker=no_new_bond,  # Just remove leaving groups
        ...     ...
        ... )
    """
    return None

port_anchor_selector

port_anchor_selector(monomer, port_name)

Select anchor atom from a port's target.

This is the standard selector for reactions that connect via ports. It simply returns the atom that the port points to.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the port

required
port_name str

Name of the port to use

required

Returns:

Type Description
Entity

The atom entity targeted by the port

Raises:

Type Description
ValueError

If port not found

Example

anchor = port_anchor_selector(monomer, "1") print(anchor.get('symbol')) # 'C'

Source code in src/molpy/reacter/selectors.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def port_anchor_selector(monomer: Monomer, port_name: str) -> Entity:
    """
    Select anchor atom from a port's target.

    This is the standard selector for reactions that connect via ports.
    It simply returns the atom that the port points to.

    Args:
        monomer: Monomer containing the port
        port_name: Name of the port to use

    Returns:
        The atom entity targeted by the port

    Raises:
        ValueError: If port not found

    Example:
        >>> anchor = port_anchor_selector(monomer, "1")
        >>> print(anchor.get('symbol'))  # 'C'
    """
    port = monomer.get_port(port_name)
    if port is None:
        raise ValueError(f"Port '{port_name}' not found in monomer")
    return port.target

remove_OH

remove_OH(monomer, anchor)

Remove hydroxyl group (-OH) bonded to the anchor.

Useful for esterification and condensation reactions. Finds O neighbor, then finds H bonded to that O.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the atoms

required
anchor Entity

Anchor atom (e.g., C in -COOH)

required

Returns:

Type Description
list[Entity]

List containing O and H atoms [O, H], or empty list

Example

leaving = remove_OH(monomer, carboxyl_carbon)

[O_atom, H_atom]

Source code in src/molpy/reacter/selectors.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def remove_OH(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    Remove hydroxyl group (-OH) bonded to the anchor.

    Useful for esterification and condensation reactions.
    Finds O neighbor, then finds H bonded to that O.

    Args:
        monomer: Monomer containing the atoms
        anchor: Anchor atom (e.g., C in -COOH)

    Returns:
        List containing O and H atoms [O, H], or empty list

    Example:
        >>> leaving = remove_OH(monomer, carboxyl_carbon)
        >>> # [O_atom, H_atom]
    """
    assembly = monomer.unwrap()
    if not isinstance(assembly, Atomistic):
        return []

    # Find O neighbor
    o_neighbors = find_neighbors(assembly, anchor, element="O")
    if not o_neighbors:
        return []

    o_atom = o_neighbors[0]

    # Find H bonded to O
    h_neighbors = find_neighbors(assembly, o_atom, element="H")
    if not h_neighbors:
        return [o_atom]  # Just remove O if no H found

    return [o_atom, h_neighbors[0]]

remove_all_H

remove_all_H(monomer, anchor)

Remove all hydrogen atoms bonded to the anchor.

Useful for reactions that eliminate all hydrogens from a carbon.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the atoms

required
anchor Entity

Anchor atom to find H neighbors of

required

Returns:

Type Description
list[Entity]

List of all H atoms bonded to anchor

Example

leaving = remove_all_H(monomer, carbon_atom)

[H1, H2, H3] for CH3

Source code in src/molpy/reacter/selectors.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def remove_all_H(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    Remove all hydrogen atoms bonded to the anchor.

    Useful for reactions that eliminate all hydrogens from a carbon.

    Args:
        monomer: Monomer containing the atoms
        anchor: Anchor atom to find H neighbors of

    Returns:
        List of all H atoms bonded to anchor

    Example:
        >>> leaving = remove_all_H(monomer, carbon_atom)
        >>> # [H1, H2, H3] for CH3
    """
    assembly = monomer.unwrap()
    if not isinstance(assembly, Atomistic):
        return []

    return find_neighbors(assembly, anchor, element="H")

remove_dummy_atoms

remove_dummy_atoms(monomer, anchor)

Remove dummy atoms (*) bonded to the anchor.

Useful for BigSMILES-style reactions where * marks connection points.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the atoms

required
anchor Entity

Anchor atom (usually ignored, kept for signature compatibility)

required

Returns:

Type Description
list[Entity]

List of dummy atoms (symbol='*') bonded to anchor

Example

leaving = remove_dummy_atoms(monomer, carbon_atom)

[*_atom]

Source code in src/molpy/reacter/selectors.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def remove_dummy_atoms(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    Remove dummy atoms (*) bonded to the anchor.

    Useful for BigSMILES-style reactions where * marks connection points.

    Args:
        monomer: Monomer containing the atoms
        anchor: Anchor atom (usually ignored, kept for signature compatibility)

    Returns:
        List of dummy atoms (symbol='*') bonded to anchor

    Example:
        >>> leaving = remove_dummy_atoms(monomer, carbon_atom)
        >>> # [*_atom]
    """
    assembly = monomer.unwrap()
    if not isinstance(assembly, Atomistic):
        return []

    neighbors = find_neighbors(assembly, anchor)
    return [n for n in neighbors if n.get("symbol") == "*"]

remove_one_H

remove_one_H(monomer, anchor)

Remove one hydrogen atom bonded to the anchor.

Useful for condensation reactions where H is eliminated. Returns the first H neighbor found, or empty list if none.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the atoms

required
anchor Entity

Anchor atom to find H neighbors of

required

Returns:

Type Description
list[Entity]

List containing one H atom, or empty list

Example

leaving = remove_one_H(monomer, carbon_atom)

[H_atom] or []

Source code in src/molpy/reacter/selectors.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def remove_one_H(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    Remove one hydrogen atom bonded to the anchor.

    Useful for condensation reactions where H is eliminated.
    Returns the first H neighbor found, or empty list if none.

    Args:
        monomer: Monomer containing the atoms
        anchor: Anchor atom to find H neighbors of

    Returns:
        List containing one H atom, or empty list

    Example:
        >>> leaving = remove_one_H(monomer, carbon_atom)
        >>> # [H_atom] or []
    """
    assembly = monomer.unwrap()
    if not isinstance(assembly, Atomistic):
        return []

    h_neighbors = find_neighbors(assembly, anchor, element="H")
    if h_neighbors:
        return [h_neighbors[0]]
    return []

remove_water

remove_water(monomer, anchor)

Remove water molecule (H2O) formed during condensation.

Alternative name for remove_OH for clarity in certain contexts.

Parameters:

Name Type Description Default
monomer Monomer

Monomer containing the atoms

required
anchor Entity

Anchor atom

required

Returns:

Type Description
list[Entity]

List containing O and H atoms forming water

Source code in src/molpy/reacter/selectors.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def remove_water(monomer: Monomer, anchor: Entity) -> list[Entity]:
    """
    Remove water molecule (H2O) formed during condensation.

    Alternative name for remove_OH for clarity in certain contexts.

    Args:
        monomer: Monomer containing the atoms
        anchor: Anchor atom

    Returns:
        List containing O and H atoms forming water
    """
    return remove_OH(monomer, anchor)