CAPSChain Assembly and Packing Suite

Python · import caps

Python API

The same core as the command line and the Studio, through its C library: open and build structures, assign force fields, relax, run, crosslink, analyse, export, and write pipeline steps the Studio runs. Every entry below is the docstring of the installed package (ABI 67).

Setup

The package is plain Python 3 (standard library only; NumPy for pipeline steps) and ships with the program. Put its folder on PYTHONPATH; it finds the CAPS library beside it.

SystemPYTHONPATH
macOS/Applications/CAPS Studio.app/Contents/Resources/data/python
Windows<install folder>\data\python
Linux (.deb)/opt/caps/data/python
Linux (tarball)<unpacked folder>/data/python
export PYTHONPATH="/Applications/CAPS Studio.app/Contents/Resources/data/python"
python3 -c "import caps; print(caps.abi_version())"

CAPS_LIB overrides the library's path (a development build, for instance).

A first script

Grow a natural-rubber cell, type it with OPLS-AA, cure it with a peroxide template, and write LAMMPS and GROMACS inputs. Every call is a method of a Document (one structure with its frames, force field and provenance).

import caps

d = caps.polymer("*CC(C)=CC*", dp=10, chains=6, density=0.6, seed=1)   # cis-1,4-polyisoprene
d.relax(max_iterations=300)
d.field.assign("opls2005")                     # types and charges from the library
print(d.react("peroxide_allylic", cycles=5, per_cycle=4, target=0.3, seed=1))
d.save("nr_xl.data")
d.export_engines("engines", stem="nr", run="npt", temperature=300, steps=5000)

The crosslinking tutorial goes through each step, and the LAMMPS fix bond/react export.

Pipeline steps in Python

The Studio's analysis pipeline (and caps pipeline) runs Python steps in a separate process. A step is one function decorated with @step; it reads the frame's particles and writes attributes, tables or new per-particle properties.

import numpy as np
from caps.pipeline import step

@step(name="Backbone height")
def modify(frame, data):
    pos = data.particles.positions_unwrapped
    data.attributes["MeanZ"] = float(np.mean(pos[:, 2]))

The reference below is generated from the installed package's docstrings.

Functions

functioncaps.label_kinds() -> 'dict'

The atom and bond properties that can label a structure: {"atom": [{id, title, group}], "bond": [...]}.

functioncaps.protocol_text(name: 'str', temperature: 'float' = 300.0, t_max: 'float' = 600.0, pressure: 'float' = 1.0, p_max: 'float' = 49346.2, time_scale: 'float' = 1.0, cycles: 'int' = 3, t_low: 'float' = 300.0, t_high: 'float' = 600.0, ramp_ps: 'float' = 50.0, hold_ps: 'float' = 50.0) -> 'str'

The stages of a named equilibration protocol ("larsen21", "annealing", "pushoff"), one per line (K, atm, ps).

functioncaps.water_models() -> 'list'

The water models CAPS has (SPC, SPC/E, SPC/Fw, TIP3P and its CHARMM and Ewald forms, TIP4P, TIP4P-Ew, TIP4P/2005, TIP4P/Ice, OPC): id, name, citation, sites, geometry, charges and Lennard-Jones. Apply one with doc.edit(op="water_model", model=id) and type the waters with it by a group {"molecules": "water", "water": id}.

functioncaps.open(path: 'str', topology: 'Optional[str]' = None, first: 'int' = 0, last: 'Optional[int]' = None, stride: 'int' = 1) -> 'Document'

Opens a structure or trajectory (LAMMPS data/dump, GROMACS .gro, PDB, XYZ, mol2, CIF …). first, last (inclusive) and stride keep only those file frames (counted from 0) of a trajectory: a LAMMPS dump's other frames are passed over unread and reading stops after last, so a long run is held at the size of what is kept.

functioncaps.pack(molecules=None, box=30.0, tolerance: 'float' = 2.0, seed: 'int' = 1, density: 'Optional[float]' = None, inp: 'Optional[str]' = None, forcefield: 'Optional[str]' = None, relax: 'bool' = False, base_dir: 'Optional[str]' = None) -> 'Document'

Molecules packed into a periodic box with no two atoms of different molecules closer than tolerance (Å), as packmol does: molecules = [("CCO", 50), ("mol.pdb", 3), (doc, 10) …] — SMILES, structure files or documents with their counts — in box = edge or (x, y, z) Å; density compresses the packed cell to that g/cm³. Or inp = a packmol input file. forcefield types the cell, relax minimises it.

functioncaps.potentials() -> 'list'

The library of literature many-body potentials (data/potentials): [{id, name, style, file, elements, for, citation, units}], file as an absolute path. A group of Field.assign_groups takes one by its id.

functioncaps.current() -> 'Document'

The structure a CAPS Studio macro runs on (Macro › Target: the open structure): the Studio saves it and names the file in CAPS_DOC. Outside the Studio, set CAPS_DOC to a structure file.

functioncaps.hand_back(doc: "'Document'") -> 'str'

Gives the macro's result back to CAPS Studio (it opens it when the macro ends): saved to CAPS_OUT, else to result.data beside the script. Returns the path.

functioncaps.import_file(path: 'str', bonds: 'str' = 'perceive', tolerance: 'float' = 0.45, bond_orders: 'bool' = True, split: 'bool' = True, unwrap: 'bool' = True, use_cell: 'bool' = True, topology: 'Optional[str]' = None) -> 'Document'

Opens a file that has no topology (XYZ, PDB, CIF …) with the Import choices: bonds "perceive" (covalent radii + tolerance Å), "file" or "none"; bond orders and aromaticity; molecules by connectivity; unwrapping; the cell.

functioncaps.import_preview(path: 'str', **options) -> 'dict'

What import_file would make of frame 0: counts, cell, bond orders and the first lines.

functioncaps.run(recipe, out_dir: 'str' = '.', seed: 'Optional[int]' = None, threads: 'int' = 0, progress=None, base_dir: 'Optional[str]' = None) -> 'Document'

Runs a recipe (a dict, YAML/JSON text, or a path to one) as caps run does: build → type → grow → relax → md → equilibrate → analyze → export. Returns the structure; .properties and .files hold what the recipe analysed and wrote. progress(event) gets {stage, stages, name, status, detail, fraction}; return False to stop.

functioncaps.polymer(smiles, dp: 'int' = 20, chains: 'int' = 1, tacticity: 'str' = 'atactic', seed: 'int' = 1, density: 'Optional[float]' = None, forcefield: 'Optional[str]' = None, relax: 'bool' = False, sequence: 'str' = 'homopolymer', trials: 'int' = 120, blocks: 'Optional[list]' = None, weights: 'Optional[list]' = None, pattern: 'str' = '', r1: 'Optional[float]' = None, r2: 'Optional[float]' = None, pm: 'Optional[float]' = None, p_mr: 'Optional[float]' = None, p_rm: 'Optional[float]' = None, lengths: 'Optional[dict]' = None, chain_dp: 'Optional[list]' = None, architecture: 'str' = 'linear', arms: 'Optional[int]' = None, arm_dp: 'Optional[int]' = None, spacing: 'Optional[int]' = None, branch_probability: 'Optional[float]' = None, generations: 'Optional[int]' = None, region: 'Optional[dict]' = None, method: 'str' = 'trials', method_temperature: 'float' = 450.0, orientation: 'Optional[dict]' = None, lookahead: 'int' = 1, head_cap: 'str' = '', tail_cap: 'str' = '') -> 'Document'

Chains of a repeat unit (SMILES with two * points, or a list of them for copolymers — sequence alternating, block with blocks=[…], random with weights=[…], gradient, pattern="AAB", terminal with r1, r2 and weights=[f1, f2]) grown in a periodic cell: one chain in a roomy cell by default (0.1 g/cm³), a melt with chains=… density=…. Atactic chains: pm (Bernoulli) or p_mr, p_rm (first-order Markov). Polydisperse: lengths={"distribution": "schulz-zimm", "nn": 40, "pdi": 1.1, "seed": 1} or chain_dp=[…]; the provenance records the sample drawn. Branched molecules: architecture="star" with arms=3|4 (each arm dp units on one core carbon), "comb" with arm_dp and spacing, or "branched" with arm_dp and branch_probability, "dendrimer" with arms, arm_dp and generations (each end splits in two branches of arm_dp units per generation); chains counts molecules. forcefield types it (default: the built-in GAFF for C and H, else UFF); relax=True minimises. region={"shape": "slab", "thickness": 30, "vacuum": 30} grows a film, {"shape": "cylinder" | "around_cylinder", "radius": 10} chains in or around a cylinder along z. method: "trials" (the roomiest of k trials), "rosenbluth" (a trial drawn by its Boltzmann weight: soft spheres and butane torsions) or "rosenbluth_lj" (the same with UFF Lennard-Jones), at method_temperature (K). orientation={"axis": "z", "strength": 4} grows oriented chains (an aligning field −s P₂ in kT on each unit's backbone chord; the report gives ⟨P₂⟩). head_cap / tail_cap: end groups in place of the end hydrogens (hydrogen, methyl, ethyl, tert-butyl, sec-butyl, phenyl, hydroxyl, carboxyl, vinyl, amine, or a SMILES with one *), bonded to the atom at the unit's first / second *.

functioncaps.table(cells, properties) -> 'Table'

functioncaps.library() -> 'C.CDLL'

The loaded libcaps (loaded on first use).

functioncaps.abi_version() -> 'int'

functioncaps.space_groups() -> 'list'

functioncaps.provenance_file(path: 'str') -> 'dict'

The provenance saved beside a file (<file>.provenance.json); {"ok": False, ...} without one.

functioncaps.compare_provenance(a: 'dict', b: 'dict') -> 'dict'

Two manifests step by step: the parameters and seeds that differ.

functioncaps.bibtex(manifest: 'dict') -> 'str'

BibTeX of every method a manifest cites.

functioncaps.methods(manifest: 'dict', replicas: 'Optional[list]' = None) -> 'dict'

A methods paragraph for a paper from a manifest: {"text": ..., "refs": [...]} with numbered references.

functioncaps.chain_lengths(distribution: 'str' = 'schulz-zimm', nn: 'float' = 40, pdi: 'float' = 1.1, count: 'int' = 20, seed: 'int' = 2026, m0: 'float' = 104.15, best_of: 'int' = 1) -> 'dict'

Chain lengths drawn from a distribution (monodisperse, schulz-zimm, flory, poisson): {lengths, sample {nn, mn, mw, pdi, min, max, sum}, target {…}, curve {n, number, weight}}. m0 is the repeat unit's molar mass (g/mol).

functioncaps.bead_templates(forcefield: 'str') -> 'dict'

{name: bead SMILES} for a coarse-grained force field (its sources' molecule templates).

functioncaps.copolymer_model(r1: 'float', r2: 'float', f1: 'float', dp: 'int' = 80, seed: 'int' = 1) -> 'dict'

The terminal (Mayo–Lewis) model: {F1, paa, pbb, run_a, run_b, azeotrope, curve {f1, F1}, sequence, chain {…}}; sequence is what polymer(…, sequence="terminal") grows for its first chain at this seed.

functioncaps.stereo(pm: 'float' = 0.5, p_mr: 'Optional[float]' = None, p_rm: 'Optional[float]' = None, dyads: 'str' = '', measured: 'Optional[list]' = None) -> 'dict'

Bernoulli (pm) or first-order Markov (p_mr, p_rm) triads and pentads; with dyads ("mrrm…", e.g. from Document.tacticity()) the counted values; with measured (ten pentad fractions, NMR) Bernoulli and Markov fits.

functioncaps.blend_phase(na: 'float', nb: 'float', a: 'float', b: 'float', t: 'float' = 300.0) -> 'dict'

Flory–Huggins binary blend with χ = a + b/T: {chi_c, phi_c, tc, chi_t, coexist, spinodal, binodal {phi, t}, spinodal_curve {phi, t}}.

functioncaps.solvent_chi(delta_polymer: 'float', solvents: 'list', t: 'float' = 298.15) -> 'list'

Hildebrand χ ≈ V(δs − δp)²/RT + 0.34 for [{name, v (cm³/mol), delta (MPa½)}]: [{name, chi, predicted}]. It ignores polarity and hydrogen bonding: check it against known behaviour.

functioncaps.ewald_params(cutoff: 'float' = 12.0, tolerance: 'float' = 1e-05, spacing: 'float' = 1.2, order: 'int' = 4, edges: 'Optional[list]' = None, doc: "Optional['Document']" = None) -> 'dict'

β from erfc(β rc) = tolerance and the PME mesh (FFT sizes with factors 2, 3, 5, 7) for edges or doc's cell.

functioncaps.chi_by_md(polymer, solvent: 'Optional[str]' = None, polymer_b=None, dp: 'int' = 10, chains: 'int' = 6, temperature: 'float' = 300.0, eq_ps: 'float' = 200.0, prod_ps: 'float' = 300.0, seed: 'int' = 1) -> 'dict'

Flory–Huggins χ from the energy of mixing by MD (core chimd.hpp): A alone, B alone and a mixture, each relaxed and run NPT, χ = V_ref (φ_A CED_A + φ_B CED_B − CED_mix) / (RT φ_A φ_B). Enthalpic only and noisy: natural rubber against itself (χ must be 0) gives −1.2 ± 0.5 after 300 ps per cell, so run long, use larger cells and run the self-mixing control (polymer_b = polymer) beside it. polymer / polymer_b: repeat-unit SMILES; solvent: SMILES. Takes minutes to hours.

functioncaps.chi_by_contacts(a: 'str', b: 'str', forcefield: 'Optional[str]' = 'gaff2', samples: 'int' = 1000000, pack_trials: 'int' = 5000, temperatures=(250, 275, 300, 325, 350, 375, 400), t: 'float' = 298.15, seed: 'int' = 1) -> 'dict'

Flory–Huggins χ(T) from pair contacts (Fan, Olafson, Blanco & Hsu, Macromolecules 1992; core chipair.hpp): pair energies of rigid molecules at van der Waals contact, Boltzmann-averaged, and coordination numbers from packing; χ = ½(Z_AB E_AB + Z_BA E_BA − Z_AA E_AA − Z_BB E_BB)/RT, fitted to A + B/T. a, b: SMILES of molecules or repeat units (* ends capped with H). forcefield: a library id (gaff2 by default) or a path; None for the built-in GAFF subset / UFF. A screen, measured on known cases in the README (wrong for PS/THF and PS/PVME). Seconds.

functioncaps.reaction_templates() -> 'list'

The names of the built-in reaction templates (Document.react).

functioncaps.reaction_template(name: 'str') -> 'str'

The text of a built-in reaction template (edit it and pass the text to Document.react).

functioncaps.bond_react_template(pre: 'str', post: 'str', map: 'str', masses_from: 'str' = '', name: 'str' = '', capture: 'float' = 0.0) -> 'dict'

A LAMMPS fix bond/react set (pre- and post-reaction molecule files and the map file) as a CAPS reaction template: {text, notes}; text goes to Document.react(). masses_from: a data file whose Masses give the elements.

functioncaps.reaction_library(path: 'str' = '') -> 'list'

The reaction library (data/reactions/library.json): every scheme — name, category, description, mapped reactant and product SMILES, what each map number marks — with its CAPS template (template, for Document.react) or why it does not convert (error; steps names the schemes that make it step by step).

caps.Document

A structure or trajectory: frames, a current frame, and everything CAPS does to it.

methodDocument.add_hydrogens(self) -> 'int'

Adds the hydrogens every atom lacks (valence rules); returns how many.

methodDocument.adsorption(self, adsorbates, cycles: 'int' = 3, steps: 'int' = 20000, t_high: 'float' = 10000.0, t_low: 'float' = 100.0, region: 'str' = 'cell', cutoff: 'float' = 12.0, coulomb: 'bool' = True, keep: 'int' = 10, seed: 'int' = 1) -> 'dict'

Adsorption locator: adsorbates [(smiles, count), …] added after the structure and placed by Monte Carlo simulated annealing as rigid bodies on the fixed structure (region "cell" or "above" its top face). The document's frames become the configurations kept, the lowest last. Returns {adsorption_energy, adsorbate_substrate, adsorbate_adsorbate, components: [{name, molecules, de_dn}], configs, notes}.

methodDocument.analyze(self, properties='density', first: 'int' = 0, last: 'int' = -1, stride: 'int' = 1, blocks: 'int' = 5, threads: 'int' = 0, **options) -> 'list'

Properties of the frames (the ids of caps analyze: density, rdf, sq, xray, electron, neutron, rg, ree, cn, persistence, msd, diffusion, ced, ffv (radii="bondi" | "uff" | "forcefield"), zprofile / adhesion / interaction / orientation (surface="1-3,7" molecule ids, axis="x" | "y" | "z", zbin=0.5) …; tg runs a stepwise cooling of a copy, t_start/t_end/t_step/ps_per_step in options). A list of {id, name, value, error, unit, ...}.

methodDocument.animate_mode(self, mode: 'int', amplitude: 'float' = 0.3, frames: 'int' = 20, selection: 'bool' = False) -> 'dict'

The document's frames become one period of normal mode `mode` (1 = the lowest), the largest atom displacement `amplitude` Å. Returns {wavenumber, modes, reduced_mass, ir, frames, notes}.

methodDocument.atom(self, i: 'int') -> 'dict'

methodDocument.atom_labels(self, kind: 'str' = 'element') -> 'list'

One text per atom of the frame: any kind of label_kinds()["atom"] (element, element_name, charge, formal_charge, oxidation_state, hybridisation, mass_number, xyz, …; also "rs", "ez").

methodDocument.atom_states(self) -> 'list'

Each atom's view state: 0 shown, 1 ghost, 2 hidden.

propertyDocument.atoms

methodDocument.backmap_kg(self, unit: 'str', name: 'str' = 'unit', relax: 'bool' = True, seed: 'int' = 1) -> "'Document'"

A Kremer–Grest melt (mapped to real units) back to all atoms, one repeat unit (SMILES with two *) per bead: all-atom chains of the melt's lengths grown, each unit carried onto its bead, relaxed with the default force field. A new Document; assign a force field (e.g. gaff2) on it for runs.

methodDocument.bond_labels(self, kind: 'str' = 'length') -> 'list'

(i, j, text) per bond: any kind of label_kinds()["bond"] (order, chemical, length, ff_r0, energy, …).

methodDocument.bond_react(self, templates, directory: 'str', **options) -> 'dict'

The reactions as a LAMMPS fix bond/react set in directory (templates as for react()): STEM.data, STEM.in and per template and environment _pre.mol, _post.mol, _map.txt, typed with the force field before and after the reaction. Options: stem, radius, variants, keep_byproducts, between_chains, weights, nevery, rmax, temperature, steps, dt, seed; forcefield: type the reactions with this force field (a library id such as opls2005, pcff, compass, or a path) instead of the assigned one, charges: auto | forcefield | gasteiger | qeq; lammps_styles (native: the force field's own styles, as export_engines | exact), kspace (auto | pppm | ewald | dsf | cut), kspace_accuracy, lammps_cutoff — the same force-field part as export_engines writes; survey_after: [0.25, 0.5] — templates also cut from copies cured by CAPS React to these conversions (steps whose groups appear only as the cure goes on; "first" / "second" variants of a two-step crosslinker), survey_relax; type_groups: [{"name": "PBS", "molecules": "33-50"}, …] (or "atoms": "1-407", "tag": name) — atom types split by component, numbered in that order, in the data file and every template, each a LAMMPS group; targets: [0.2, 0.4, …], limiting, link_reactions: ["enr_acid_ester_*"], check_every, max_steps — the input runs to each crosslink density, writes crosslink_progress.dat and STEM_XL20.data …; stall_chunks, rmax_step, rmax_limit raise Rmax when no reaction happens (the check uses the absolute Rmax and stops at the limit); stabilize_steps (200): steps each reaction's atoms stay under nve/limit afterwards (react … stabilize_steps); h_transfer_max (3.5 Å; 0: none): a hydrogen that changes partner must be this close to its new partner before the reaction happens (a distance constraint in each map file); every template is checked for LAMMPS's rule — each atom linked to an initiator without passing an edge atom — and atoms beyond an edge are left out (the notes say how many); mol_ids: reset | keep | molmap (keep_chain_ids=True: molmap — chains keep their ids, a crosslinker takes the id of the chain it first bonds to; LAMMPS 2 Apr 2025 or later). Returns {files, notes, variants, steps (coverage per reaction step), frames, link_reactions, candidates, covered}.

methodDocument.bond_rules(self, rules: 'list', apply: 'bool' = False) -> 'dict'

Bonds from rules [{"a": "C", "b": "C", "max": 1.68}, {"a": "Zn", "b": "O", "never": True}]: what they would give (bonds now and after, added, removed); apply=True sets them (one undoable edit).

methodDocument.cg_map(self, scheme: 'str' = 'unit', per_bead: 'int' = 3, temperature: 'float' = 300.0, ibi: 'Optional[dict]' = None, rules=None) -> "'Document'"

This all-atom structure (every frame) mapped to beads — scheme unit | backbone_side | backbone_n | rules — with a bead model: Boltzmann-inverted harmonic bonds and angles, a repulsive WCA from the non-bonded bead g(r). rules (scheme "rules"): a preset id of data/cg/mapping_rules.json ("ester-cut") or a dict {"cut": [SMARTS], "names": {"B": SMARTS}, "position": "com"} (also "beads": {name: fragment SMARTS} or "explicit": [bead per atom]). A new Document (its model assigned); .report holds the inverted parameters (JSON).

methodDocument.checks(self) -> 'list'

methodDocument.close(self) -> 'None'

methodDocument.conformers(self, trials: 'int' = 50, method: 'str' = 'torsions', selection: 'bool' = False, window: 'float' = 10.0, rmsd: 'float' = 0.5, temperature: 'float' = 298.15, seed: 'int' = 1) -> 'dict'

Conformer search (in vacuum) of the frame shown or of the selected atoms: random staggered torsions ("torsions") or quenched snapshots of a 1000 K run ("anneal"), minimised and clustered by heavy-atom RMSD. The document's frames become the conformers, lowest first. Returns {conformers: [{energy, relative, population, found}], rotors, trials, notes}.

methodDocument.convert(self, to: 'str' = 'united-atom', per_bead: 'int' = 5) -> "'Document'"

A new document at another resolution: "united-atom" (H on carbon folded in) or "coarse-grained" (beads of per_bead backbone atoms at their centre of mass); this one is unchanged.

methodDocument.edit(self, **op) -> 'dict'

One structure edit (the Studio's builder tools): edit(op="add_h"), edit(op="attach", target=5, smiles="*C(=O)O*") …

methodDocument.energy(self) -> 'dict'

The current frame's energy by term (kcal/mol) with the force field runs use: bond, angle, dihedral, improper, vdw, coulomb, total — computed now (a Field report's energy is the one at assignment).

methodDocument.equilibrate(self, protocol: 'str' = 'larsen21', temperature: 'float' = 300.0, t_max: 'float' = 600.0, pressure: 'float' = 1.0, p_max: 'float' = 49346.2, time_scale: 'float' = 1.0, cycles: 'int' = 3, t_low: 'float' = 300.0, t_high: 'float' = 600.0, ramp_ps: 'float' = 50.0, hold_ps: 'float' = 50.0, dt: 'float' = 1.0, thermostat: 'str' = 'bussi', barostat: 'str' = 'crescale', tau_t: 'float' = 100.0, tau_p: 'float' = 1000.0, seed: 'int' = 1, cutoff: 'float' = 10.0, coulomb: 'bool' = True, frame_ps: 'float' = 10.0, thermo_ps: 'float' = 0.5, until_converged: 'bool' = False, block_ps: 'float' = 20.0, max_blocks: 'int' = 20, constraints: 'str' = 'none', constraint_solver: 'str' = 'shake', tail: 'bool' = True) -> 'bool'

Runs an equilibration protocol from the current frame: "larsen21" (Larsen et al. 2011, 21 steps: t_max, p_max ramps down to temperature and pressure), "annealing" (cycles between t_low and t_high, ramp_ps and hold_ps), "pushoff", or protocol text written by hand (one stage per line, as protocol_text gives). until_converged adds NPT blocks of block_ps until density, energy and Rg stop drifting (at most max_blocks); .checks then holds the checks. Returns True when converged (or when no checks were asked for); the recorded frames become the document's frames.

methodDocument.export_engines(self, folder: 'str', stem: 'str' = 'system', lammps: 'bool' = True, gromacs: 'bool' = True, run: 'str' = 'check', **opts) -> 'dict'

The simulation files for LAMMPS (stem.data, stem.in with every pair_coeff and the run) and GROMACS (stem.top, stem.itp, stem.gro, stem.mdp; stem_em.mdp when a run minimises first) from the assigned force field, which must be complete. run: check | none | minimize | nvt | npt | tensile | creep | shear (LAMMPS: axis "x"/"y"/"z", strain_rate 1/ps and max_strain; stress_mpa; shear_rate 1/ps — stress_strain.dat, creep.dat, viscosity.dat) | protocol (protocol "larsen21", "annealing", "pushoff" or a protocol's text, t_max, p_max, production_ps: CAPS's stages in LAMMPS, -var scale shortens them); opts: minimize_first, temperature (K), pressure (atm), dt (fs), steps, thermo_every, dump_every, seed, units (auto | real | metal: LAMMPS in eV, ps, bar — automatic for AIREBO / REBO, which LAMMPS reads in metal units only), amber=True (stem.prmtop and stem.inpcrd for AMBER / OpenMM / ParmEd; a force field AMBER cannot hold gives amber_error), dlpoly=True; type_groups=[{"name": "PBS", "molecules": "7-10"}, …] ("atoms": "1-407" or "tag": name) splits the LAMMPS files' atom types by component (each group's types numbered in that order, the same coefficients; each a LAMMPS group by type). Returns {folder, files, notes, checks}; raises CapsError when refused.

methodDocument.export_scene(self, path: 'str', format: 'str' = '', width: 'int' = 1280, height: 'int' = 800, background: 'str' = 'white', style: 'str' = 'ball_and_stick', colour: 'str' = 'molecule', yaw: 'float' = 0.55, pitch: 'float' = 0.4, zoom: 'float' = 1.0) -> 'dict'

The view's scene for other tools: "pov" (POV-Ray, with this camera), "glb" (glTF: Blender, ParaView) or "obj" (+ .mtl); the format follows the file's extension when not given. Returns {spheres, cylinders, triangles}.

methodDocument.fragment_smiles(self, atoms=None) -> 'str'

A fragment's SMILES from atoms of the frame (0-based; None: the selection): those atoms and their hydrogens, each bond leaving them an attachment point * — "c1(ccccc1)*" for the ring of ethylbenzene.

propertyDocument.frames

methodDocument.ghost(self, atoms=None) -> 'int'

Ghost these atoms: drawn faint, never picked (the view and its images only).

methodDocument.hide(self, atoms=None) -> 'int'

Hide these atoms (indices; None: all) in the view and its images. The structure, its exports and calculations keep them. Returns how many changed.

methodDocument.hold(self, molecule: 'int' = 0, atoms: 'Optional[list]' = None) -> 'None'

Holds atoms in place in relax, md and equilibration (no force, no motion; a freeze group in GROMACS files): molecule > 0 holds that molecule (an interface's surface is 1), atoms (indices from 0) any others; hold() frees them.

methodDocument.hydrogen_plan(self) -> 'dict'

What add_h would add, by kind of atom: {rows: [{label, atoms, hydrogens}], heavy, h, add, …} (bond orders from the geometry when the structure has no H and no multiple bonds).

methodDocument.insert(self, smiles: 'str', count: 'int', tolerance: 'float' = 2.0, seed: 'int' = 1) -> 'str'

Inserts count copies of a molecule (SMILES; hydrogens added, UFF-cleaned) into the free space of the current frame — curatives before a cure, e.g. insert("SS", 40) for the sulfur_allylic template. The document becomes that frame and its force-field assignment is cleared (assign again after).

methodDocument.interactions(self, **options) -> 'dict'

methodDocument.layers(self) -> 'dict'

The structure's layers: molecules grouped by kind (residue name and formula), each molecule with its atom count, a profile along z (8 bins, peak 1), its view state (shown, ghost, hidden, mixed), lock and selected atoms.

methodDocument.lock(self, atoms=None, locked: 'bool' = True) -> 'int'

Lock these atoms (None: all) against picking and edits in the Studio, or free them (locked=False).

methodDocument.md(self, steps: 'int' = 10000, dt: 'float' = 1.0, temperature: 'float' = 300.0, thermostat: 'str' = 'bussi', barostat: 'str' = 'none', pressure: 'float' = 1.0, seed: 'int' = 1, frame_every: 'int' = 1000, thermo_every: 'int' = 100, cutoff: 'float' = 10.0, respa: 'int' = 1, constraints: 'str' = 'none', constraint_solver: 'str' = 'shake', couple_axes: 'str' = '', efield: 'tuple' = (0.0, 0.0, 0.0), full_shape: 'bool' = False) -> 'str'

Molecular dynamics from the current frame; the frames recorded become the document's frames. respa > 1: r-RESPA, the bonded forces every dt / respa (e.g. dt=2, respa=4 with hydrogens). constraints "h-bonds" (bonds to hydrogen, rigid water) or "all-bonds": SHAKE/RATTLE, for dt=2 (the alternative to respa). thermostat "nose-hoover" with barostat "mtk" runs as LAMMPS's fix nvt / fix npt iso. constraint_solver "lincs" puts the positions back on the constraints with LINCS (as GROMACS), "shake" with SHAKE (as LAMMPS); the same result to the tolerance.

methodDocument.normal_modes(self, temperature: 'float' = 298.15) -> 'dict'

The harmonic vibrations of the frame shown (minimise it tightly first): {value: ZPE kcal/mol, extra: {modes, imaginary modes, S_vib, Cv_vib …}, series: [IR spectrum (fixed charges), density of states, mode wavenumbers]}.

methodDocument.pair_histograms(self, lo: 'float' = 0.8, hi: 'float' = 3.2, bin: 'float' = 0.04) -> 'list'

Bond rules: per pair of elements, the distances between their atoms as a histogram (counts from lo in steps of bin), the bonds the structure has between them, and the suggested cut-off (the first gap after the bonded peak).

methodDocument.positions(self) -> 'list'

methodDocument.probe(self, kind: 'str', atoms) -> 'dict'

A probe from atoms (0-based) in the frame shown: "point" (mass-weighted centre), "plane", "axis" or "ellipsoid" — its centre, principal directions (largest first), semi-axes and rms (a plane's flatness).

methodDocument.probe_series(self, a: 'tuple', b: 'Optional[tuple]' = None, measure: 'str' = 'distance') -> 'list'

A probe measured against another over every frame: a, b as (kind, atoms); measure "distance" (Å; above a plane along its normal), "angle" (degrees), "rms" or "size". One value per frame.

propertyDocument.provenance

The steps that produced this structure: doc.provenance() is the manifest (caps-manifest/1.0, written beside the file on save); doc.provenance.methods(), .citations(fmt="bibtex"|"text"), .bibtex().

methodDocument.query(self, query: 'str', op: 'str' = 'replace') -> 'int'

Selects by the query grammar (design/boards/SmartSelect): smarts "c1ccccc1", element C N O, type c3, chain 1-4, index 1-20, ring 5, stereo R|S|*, within 5.0 of <query>, sel, combined with and / or / not / ( ). op "preview" counts without selecting. Returns the number of atoms.

methodDocument.react(self, templates, cycles: 'int' = 50, per_cycle: 'int' = 5, target: 'float' = 1.0, capture: 'float' = 0.0, relax: 'bool' = True, relax_iterations: 'int' = 500, md_ps: 'float' = 0.0, temperature: 'float' = 500.0, cutoff: 'float' = 10.0, seed: 'int' = 1, during_md: 'bool' = False, between_chains: 'bool' = False, keep_byproducts: 'bool' = False, weights=None, auto_capture: 'bool' = False, capture_max: 'float' = 0.0, capture_step: 'float' = 0.0, crosslinks=None, default_field: 'bool' = False, sites_per_chain: 'int' = 0) -> 'str'

Crosslinks the current frame cycle by cycle (Polymatic-style; REACTER-style capture and probability) with reaction templates: built-in names ("cc_crosslink", "sulfur_allylic", "peroxide_allylic", "polysulfide_allylic", "epoxy_amine_primary", "enr_acid_ester", "ester_condensation", "anhydride_alcohol", …; see reaction_templates()) or template text, one or a list. target is the conversion to stop at (0 … 1); crosslinks=("per_chain", 1.5) (or "crosslinks", "density" in mol/m³, "mc" in g/mol) stops at a number of links between chains instead. between_chains: bonds only between different chains; keep_byproducts: H2 / H2O stay as molecules; weights: one relative rate per template (else the closest pairs first); auto_capture widens the capture when no pair is found. The assigned force field types and relaxes every cycle and is re-assigned to the product (default_field=True: the built-in default, the assignment dropped). sites_per_chain: at most this many of each chain's reactive sites react (react_sites() counts them). Returns the report; network numbers in react_summary().

methodDocument.react_sites(self, templates) -> 'dict'

Each chain's reactive sites for the templates (built-in names or text, as for react()): chains [{chain, sites, units, atoms, mass}], total_sites, chains_n, units (repeat units), mass and repeat_unit_mass (g/mol).

methodDocument.react_summary(self) -> 'dict'

The network of the last react(): chains, crosslinks (links between chains), target, density (mol/m³), per_chain, mc (g/mol), byproducts, the force field during and after the run.

methodDocument.relax(self, ftol: 'float' = 0.5, method: 'str' = 'lbfgs', max_iterations: 'int' = 5000, density: 'float' = 0.0, pushoff: 'bool' = True, box: 'bool' = False, pressure: 'float' = 1.0, cutoff: 'float' = 10.0, coulomb: 'bool' = True, threads: 'int' = 0, restraints: 'Optional[list]' = None, box_axes: 'str' = '', pushoff_md_ps: 'float' = 0.0, pushoff_cap: 'float' = 0.0, pushoff_temperature: 'float' = 0.0, etol: 'float' = 0.0, pressure_tol: 'float' = 0.0) -> 'int'

Minimises the current frame with the Field assignment (else the built-in GAFF): 0 converged, 1 not quite. restraints: [(i, j, r0), …] or [(i, j, r0, k), …] — k (r − r0)² between atoms i and j (indices from 0, Å, k kcal/mol/Ų, default 10); dihedral ones as {"i", "j", "k", "l", "phi0", "kphi"} dicts; they stay set for later relaxations until relax(restraints=[]) clears them. box_axes: "z", "xy" … relaxes the box axis by axis (only those axes move); "" keeps the isotropic box relaxation (box=True). pushoff_md_ps > 0: the push-off by NVT MD first (Auhl et al. 2003), the LJ force cap raised to pushoff_cap (default 500 kcal/mol/Å) over that time.

methodDocument.render(self, path: 'str', width: 'int' = 1280, height: 'int' = 800, background: 'str' = 'white', style: 'str' = 'ball_and_stick', colour: 'str' = 'molecule', yaw: 'float' = 0.55, pitch: 'float' = 0.4, zoom: 'float' = 1.0, engine: 'str' = 'raster', bits: 'int' = 8, samples: 'int' = 64, shadows: 'bool' = True, occlusion: 'bool' = True, depth_of_field: 'float' = 0.0, outlines: 'bool' = False, dpi: 'float' = 0) -> 'None'

A PNG of the current frame. engine="raytrace" traces it (CAPS's ray tracer: ambient occlusion, soft shadows, depth of field as a fraction of the view's half-width, e.g. 0.012 soft, 0.035 strong); bits=16 keeps 16 bits.

methodDocument.resolution(self, per_bead: 'int' = 5) -> 'dict'

Sites, hydrogens and mass all-atom, united-atom and coarse-grained (mass conserved).

methodDocument.rigid(self, molecules: 'str' = '') -> 'int'

Molecules ("1-3,7"; "" none) that move as rigid bodies in the LAMMPS inputs written by export_engines (fix rigid/nvt/small; pairs inside a body excluded). CAPS's own runs have no rigid bodies. Returns how many.

methodDocument.save(self, path: 'str') -> 'None'

Writes the current frame: .data (LAMMPS, with force-field sections when assigned), .pdb, .xyz, .mol2, .gro, .cif (P 1), .vasp or a file named POSCAR/CONTCAR (VASP 5, Direct; held atoms as Selective dynamics) …

methodDocument.save_gromacs(self, stem: 'str') -> 'str'

Writes stem.top, stem.gro and stem.mdp for GROMACS with the same force field (a single-point run; energies and forces checked against GROMACS by bench/ff/check_gromacs.py). Returns the .mdp non-bonded settings, with "; note:" lines where GROMACS computes differently (DSF becomes PME; no cell).

methodDocument.save_trajectory(self, path: 'str') -> 'None'

methodDocument.scene(self, hydrogens: 'bool' = True, max_atoms: 'int' = 60000) -> 'dict'

The current frame for a viewer: z, xyz (flat), bonds (flat pairs), colours, radii, cell.

methodDocument.select(self, mode: 'str', pattern: 'str' = '', op: 'str' = 'replace', **kw) -> 'int'

methodDocument.selection(self) -> 'list'

methodDocument.series(self, molecule: 'int' = 0, dt_fs: 'float' = 1.0, log: 'str' = '') -> 'dict'

methodDocument.set_frame(self, k: 'int') -> 'None'

methodDocument.show(self, atoms=None) -> 'int'

Show these atoms again (None: every hidden and ghosted atom).

methodDocument.sorption(self, sorbate: 'str' = 'O=C=O', pressures_kpa=(), temperature: 'float' = 300.0, insertions: 'int' = 100000, steps: 'int' = 200000, mixture=None, map_grid: 'int' = 0, cutoff: 'float' = 12.0, coulomb: 'bool' = True, seed: 'int' = 1) -> 'dict'

Sorption of a gas in the frame held fixed: Widom insertion (excess chemical potential, Henry constant, solubility) and GCMC at each pressure (kPa). mixture: [(smiles, mole fraction), …] for an ideal gas mixture (each species at y_i p; per-species loadings, isosteric heats and selectivities over the first). Returns {widom_w, mu_ex, henry_mol_kg_kpa, solubility, species: [...], isotherm: [{pressure_kpa, loading, species_loading, selectivity, heat, ...}], notes}.

methodDocument.summary(self) -> 'dict'

methodDocument.tacticity(self) -> 'dict'

methodDocument.tag(self, name: 'str', atoms=None, colour: 'str' = '', op: 'str' = 'set') -> 'int'

Tag these atoms (0-based; None: the selection) with NAME. op: "set" (exactly these), "add", "remove", "delete" (the tag itself), "rename" (atoms is then the new name) or "colour". Returns the tag's index.

propertyDocument.tags

The structure's tags: name -> list of atom indices (0-based). Tags are saved beside the structure (NAME.tags.json) and written as groups in LAMMPS inputs and GROMACS index files; analyze(group="tag:NAME") analyses one.

methodDocument.torsion_scan(self, atoms, step: 'float' = 15, relax: 'bool' = False, forcefield: 'str' = 'auto') -> 'dict'

methodDocument.undo(self, redo: 'bool' = False) -> 'None'

methodDocument.view(self, style: 'str' = 'ball-and-stick', width: 'int' = 640, height: 'int' = 400, hydrogens: 'bool' = True, max_atoms: 'int' = 60000, cell: 'Optional[bool]' = None)

An interactive view for notebooks (drag to rotate, wheel to zoom, double-click to reset); a PNG where HTML is not shown. style: ball-and-stick, space-filling, sticks, no-hydrogens, lines; cell: draw the periodic cell (default: when there is more than one molecule).

caps.build

The builders: each returns a new Document.

builderbuild.beads(text: 'str', forcefield: 'Optional[str]' = None, seed: 'int' = 1) -> 'Document'

A coarse-grained molecule: a bead template of the force field (MARTINI's DPPC, W, NA+ …; bead_templates()) or bead SMILES ("[Q0+1][Qa-1][Na]…"); one site per bead named by its type, bonds at the force field's lengths.

builderbuild.cg_from_polymer(unit: 'str', name: 'str' = 'unit', scheme: 'str' = 'unit', chains: 'int' = 10, dp: 'int' = 20, density: 'float' = 1.0, temperature: 'float' = 300.0, per_bead: 'int' = 3, seed: 'int' = 1, ibi: 'Optional[dict]' = None) -> 'Document'

A polymer (repeat-unit SMILES with two *) coarse-grained from an all-atom reference melt: chains × dp grown, compressed to density (g/cm³) and mapped (see Document.cg_map). .report holds the inverted parameters. ibi: {"iterations": 6, "run_ps": 20} refines the non-bonded pair by iterative Boltzmann inversion (a table).

builderbuild.cg_polymer(beads_per_unit: 'dict', composition: 'Optional[dict]' = None, sequence: 'str' = 'bernoulli', dp: 'int' = 100, chains: 'int' = 10, density: 'float' = 1.2, lengths: 'Optional[dict]' = None, bonded: 'str' = 'bonded', maps=None, masses: 'Optional[dict]' = None, seed: 'int' = 1, out: 'str' = 'melt', markov: 'Optional[dict]' = None, blocks: 'Optional[list]' = None, pattern: 'str' = '', types: 'str' = '', reference=None) -> 'dict'

A coarse-grained melt (caps cgbuild): beads_per_unit={"BS": ["B", "S"], "BA": ["B", "A"]}, composition={"BS": 0.8, "BA": 0.2}, sequence bernoulli | markov (markov={"BA": {"BA": 0.9, "BT": 0.1}, …}) | block (blocks=[20, 10]) | gradient | alternating | pattern, lengths={"distribution": "schulz-zimm", "pdi": 2} (dp is the number average), bonded: the folder of caps cgfit bonded, maps: map.json file(s) for the bead masses (or masses={"B": 88.1, …}), reference: [map.json, frames …] of the all-atom chains to compare ⟨R²(n)⟩/n with. Writes OUT/melt.cg.data, melt.map.json, melt.internal.csv and in.cg_equil; returns the report (box, ree_rms, text …).

builderbuild.crystal(space_group: 'str', a: 'float', b: 'float', c: 'float', sites, alpha: 'float' = 90, beta: 'float' = 90, gamma: 'float' = 90, supercell=(1, 1, 1), primitive: 'bool' = False) -> 'Document'

sites: [("Ti1", "Ti", x, y, z), …] (fractional).

builderbuild.kremer_grest(chains: 'int' = 50, beads: 'int' = 100, density: 'float' = 0.85, k_theta: 'float' = 0.0, seed: 'int' = 1, sigma: 'float' = 0.0, temperature: 'float' = 0.0, bead_mass: 'float' = 0.0) -> 'Document'

A Kremer–Grest bead-spring melt with its own force field (FENE + WCA; cosine bending k_theta). Reduced units, or mapped to a polymer when sigma (Å), temperature (K, ε = k_B T) and bead_mass (g/mol) are all given; export_engines writes its LAMMPS deck (push-off, then the run).

builderbuild.martini_melt(repeat: 'str', repeats: 'int' = 20, chains: 'int' = 20, density: 'float' = 1.0, forcefield: 'str' = 'martini-moltemplate', seed: 'int' = 1, tolerance: 'float' = 3.0) -> 'Document'

A MARTINI polymer melt: chains of `repeats` × the repeat unit's bead SMILES ("[C1]", "[SN0]" …) packed, the MARTINI force field (a library id or a path) assigned and the cell compressed to the density (g/cm³) with it.

builderbuild.nano(**options) -> 'Document'

kind="tube" (n, m, length, periodic) | "sheet" (lx, ly, layers) | "particle" (crystal CIF, shape, radius; a metal particle takes thiolate="C6" | "C12" | "C18" | "MPA" | "MUA" | "MHA" | "*S…" SMILES and thiolate_fraction).

builderbuild.peptide(sequence: 'str', structure: 'str' = '', n_term: 'str' = 'NH3+', c_term: 'str' = 'COO-', ph: 'float' = 7.0, cleanup: 'bool' = True) -> 'Document'

builderbuild.smiles(smiles: 'str', forcefield: 'str' = 'uff', conformers: 'int' = 1, seed: 'int' = 1, rotor_search: 'bool' = False, hydrogens: 'str' = 'add') -> 'Document'

A 3D molecule from SMILES (one frame per conformer, lowest first). rotor_search: each minimised conformer's rotatable bonds tried at their staggered positions with the force field; hydrogens: "add" (from valences) or "as_written" (heavy atoms only, united-atom models).

builderbuild.solvate(solute: 'Optional[Document]' = None, **options) -> 'Document'

Solvent and ions around a solute (or a box of solvent): shape, edge, padding, solvent, water_model, ion_mode, salt, concentration …

caps.pipeline

The @step decorator and the data a step sees (particles, bonds, cell, attributes, tables).

classpipeline.Data(raw)

classpipeline.Particles(columns, count)

Per-particle columns by name: "Particle Identifier", "Molecule Identifier", "Particle Type", "Element", "Charge", "Selection", "Backbone" (1 on backbone atoms), "Position" (N × 3) and any property earlier steps made.

functionpipeline.step(name=None)

Marks the function CAPS calls: modify(frame, data).

caps.sweep

caps.sweep: the Studio's Sweep from Python — one polymer over tacticities, chain lengths and seeds, each cell grown, relaxed (optionally run NPT) and analysed, saved with its provenance; and the results of a finished sweep. runs = caps.sweep.run("*CC(*)c1ccccc1", tacticities=("isotactic", "syndiotactic", "atactic"), dps=(20,), seeds=(1, 2, 3)) cells = [caps.sweep.result(t) for t in ("isotactic", "syndiotactic", "atactic")] caps.table(cells, ["density", "rg", "c_inf"]) A sweep folder holds TACTICITY_dpN_seedS.data (+ .provenance.json) for every run and results.json with the values; the Studio writes the same layout under ~/CAPS/sweeps.

classsweep.Result(label: 'str', runs: 'list', folder: 'Path')

The runs of one condition: r["density"] → (mean, standard deviation, n) over the runs that have it.

classsweep.Sweep(folder)

A sweep folder: its runs and the result of each condition.

functionsweep.latest(root: 'Optional[str]' = None) -> 'Path'

The newest sweep folder (under ~/CAPS/sweeps).

functionsweep.result(condition: 'str', folder: 'Optional[str]' = None, dp: 'Optional[int]' = None) -> "'Result'"

One condition (a tacticity) of a sweep, its seeds pooled: the newest sweep unless folder is given.

functionsweep.run(polymer, tacticities: 'Iterable[str]' = ('isotactic', 'syndiotactic', 'atactic'), dps: 'Iterable[int]' = (20,), seeds: 'Iterable[int]' = (1, 2, 3), chains: 'int' = 10, density: 'float' = 0.5, forcefield: 'str' = 'default', relax: 'bool' = True, npt_ps: 'float' = 0.0, temperature: 'float' = 300.0, properties: 'Iterable[str]' = ('density', 'rg', 'cn'), tg: 'Optional[dict]' = None, folder: 'Optional[str]' = None, progress=None) -> "'Sweep'"

Runs every (tacticity, DP, seed); failures are recorded, not raised. tg={"t_start": 500, "t_end": 200, "t_step": 25, "ps_per_step": 20} adds a stepwise-cooling Tg to each run (slow). progress(run, status) is told as runs finish.

caps.geometry

Small geometry helpers for steps (numpy, imported when used).

functiongeometry.angles(pos)

Bond angles (degrees) along a chain of points.

functiongeometry.dihedrals(pos)

Dihedral angles (degrees) along a chain of points: one per four consecutive points.