Builder¶
System assembly: select every reaction, compile and cache its local product, execute the graph edits as one batch, then optionally finalize topology. Growing a chain and crosslinking a melt are the same algorithm with a different pairing rule, so there is one kernel and one variation point.
Quick reference¶
| Symbol | Summary | Preferred for |
|---|---|---|
GraphAssembler |
The kernel: assemble(world, selector) |
Crosslinking an existing graph |
PolymerBuilder |
Library + reaction; .build(cgsmiles) is the sole expand + assemble path; .build_* only format CGSmiles |
Ruled polymer topologies |
Finalization |
ATOMS, TOPOLOGY (default), or BONDED |
Choosing when topology is materialized |
StructureFinalizer |
Run the shared topology/bonded tail later | Deferred MD export for large systems |
AssemblyFinalizer |
Assembly finalizer with aromaticity perception | Molecular reaction products |
SiteMap |
Mark fields.SITE (and optional leaving H + charge fold) |
Naming reaction sites |
Replicas |
Grid / linear copies of a strand with mol_id |
Melt precursor before crosslinking |
MonomerLibrary |
Validated repeat-unit templates; .expand(topology) |
Naming your monomers |
Selector |
The one variation point: which matched sites pair up | Writing your own pairing rule |
TopologySelector |
Pairs adjacent residues (used by PolymerBuilder) |
Residue-edge pairing |
ExhaustiveSelector / SpacingSelector / ExplicitPairSelector |
Deterministic crosslink rules | Reproducible networks |
RandomSelector |
Random pairing to a target conversion, seeded |
Flory–Stockmayer networks |
ResiduePlacer |
Lays fresh template copies out in space | Building from templates |
SystemPlanner / PolydisperseChainGenerator |
Sample a polydisperse chain plan | Bulk / MW-distributed systems |
AmberPolymerBuilder |
GAFF-parameterised build via AmberTools | AMBER/LAMMPS-bound workflows |
CarbonTubeBuilder |
.build(n, m, ...) rolled-graphene topology |
Zigzag, armchair, and chiral nanotubes |
DrudeBuilder / Tip4pBuilder / VirtualSiteBuilder |
Virtual-site augmentation | Polarizable / 4-site models |
Canonical example¶
A repeat unit is an ordinary capped molecule with a few of its atoms named.
There is no port system and no direction: the reaction SMARTS is the only place
the chemistry lives, and %a / %b bind it to the atoms you marked.
import molpy as mp
from molpy.builder.assembly import MonomerLibrary, PolymerBuilder, ResiduePlacer, SiteMap
from molpy.conformer import Conformer
from molpy.core import fields
from molpy.parser import parse_molecule
eo, _ = Conformer(add_hydrogens=True, seed=42).generate(parse_molecule("OCCO"))
SiteMap(eo).label_elements("O", "a", "b")
ether = mp.Reaction("[O;%a:1][H].[C:2][O;%b][H]>>[O:1][C:2]")
builder = PolymerBuilder(
MonomerLibrary({"EO": eo}), ether, placer=ResiduePlacer()
)
chain = builder.build_linear("EO", 5)
assert chain.__class__.__name__ == "Atomistic"
assert len({int(a[fields.RES_ID]) for a in chain.atoms}) == 5
Each repeat unit is a residue, and that identity survives into a PDB or a prmtop — it is output, not a build-time marker to scrub afterwards.
Compile first; finalize when needed¶
typifier= never receives the growing polymer. The builder first compiles all
selected junction motifs against the intact monomer templates, caches distinct
rooted environments, and records scalar per-atom changes only. It then executes
all reactions in one batch. Local bond/angle/dihedral annotations are not copied
from a motif into the world.
The default finalization generates complete angle/dihedral topology once. For a
large system that will be written to an MD format later, select
Finalization.ATOMS while building and apply
StructureFinalizer(Finalization.TOPOLOGY) at export time. This keeps the public
examples focused on polymer architectures while still documenting the deferred
topology option.
Use Finalization.BONDED together with
bonded=ForceFieldParams(forcefield) when those generated terms also need type
and parameter annotations.
A full walk of architectures from one ethylene-glycol template — linear, ring,
star, comb, gels, dual network — is the user-guide section
Polymer Topologies (paired with
examples/topology/).
Nanostructure topology¶
Nanostructure builders keep their lattice planning private and expose one verb:
from molpy.builder import CarbonTubeBuilder
tube = CarbonTubeBuilder().build(6, 6, cells=2, periodic=True)
assert len(tube.atoms) == 48
assert len(tube.bonds) == 72
assert tube["box"].pbc.tolist() == [False, False, True]
The same call accepts zigzag (n, 0), armchair (n, n), and general chiral
(n, m) tubes. See Nanostructures for
open ends, length selection, and deferred topology.
Crosslinking is the same machine¶
Strip the library and the notation away and you have the kernel itself, which is all crosslinking needs: a graph you already have, plus a rule for which sites pair up.
from molpy.builder.assembly import GraphAssembler, RandomSelector
melt = mp.Atomistic()
for i in range(4):
melt.def_atom(element="N", x=float(i), y=0.0, z=0.0)
melt.def_atom(element="O", x=float(i), y=1.0, z=0.0)
gel = GraphAssembler(mp.Reaction("[N:1].[O:2]>>[N:1][O:2]")).assemble(
melt, RandomSelector(conversion=1.0, seed=1, cutoff=2.0)
)
assert len(list(gel.bonds)) == 4
No placer is passed here: the melt's coordinates are already meaningful and
must not be disturbed. That is a decision about your input, not about which
class you reached for, which is why it is an argument.
Polydisperse systems¶
Sample a chain plan, then loop build:
import numpy as np
from molpy.builder.polymer import (
PolydisperseChainGenerator,
SchulzZimmPolydisperse,
SystemPlanner,
WeightedSequenceGenerator,
)
planner = SystemPlanner(
PolydisperseChainGenerator(
WeightedSequenceGenerator({"EO": 1.0}),
{"EO": 44.05},
distribution=SchulzZimmPolydisperse(1500, 3000),
),
target_total_mass=5e3,
)
plan = planner.plan_system(np.random.default_rng(42))
chains = [builder.build("{[#EO]|%d}" % len(c.monomers)) for c in plan.chains[:2]]
assert len(chains) == 2
Your own pairing rule¶
You never subclass the assembler. You write a Selector, which answers the one
question that varies. The matching has already happened — the kernel does it
once, in linear time — so a selector never scans the system, it only decides.
from molpy.builder.assembly import Selector
class FirstPairSelector(Selector):
"""React exactly one pairing: the first site of each reactant."""
def select(self, context):
a_sites = context.occurrences[context.comp_a]
b_sites = context.occurrences[context.comp_b]
yield {**a_sites[0], **b_sites[0]}
one = GraphAssembler(mp.Reaction("[N:1].[O:2]>>[N:1][O:2]")).assemble(
melt, FirstPairSelector()
)
assert len(list(one.bonds)) == 1