Molecule Values¶
Molecule objects behave as value-style molecule values. Transformation
methods return new molecule objects and leave the original object unchanged.
Internally COSMolKit uses copy-on-write (COW) storage to share unchanged data
efficiently. This is intentionally different from common RDKit Python
workflows, where code often mutates an existing molecule or RWMol directly.
from cosmolkit import Molecule
mol = Molecule.from_smiles("CCO")
mol_h = mol.with_hydrogens()
assert mol is not mol_h
print(mol.to_smiles())
print(mol_h.to_smiles())
Do not write code that assumes mol.with_hydrogens() changes mol. Keep
the returned value and pass that value to later operations.
Common transformations include:
with_hydrogens()without_hydrogens()with_kekulized_bonds()with_2d_coordinates()with_chiral_tags_from_structure()
Read Finalization¶
Molfile and SDF molecule readers use the RDKit-source-backed finalization path
for modeled parser behavior. With the default sanitize=True and
remove_hs=True, readers parse the CTAB, process molfile/SDF properties,
assign modeled stereochemistry, remove hydrogens through the RDKit-aligned
hydrogen-removal path, sanitize, and assign final stereochemistry.
Passing sanitize=False preserves the parsed molecule state for a later
value-style sanitize operation:
raw = Molecule.read_mol("input.mol", sanitize=False)
sanitized = raw.sanitize()
assert raw is not sanitized
Passing remove_hs=False preserves explicit hydrogens for later value-style
hydrogen removal:
with_h = Molecule.read_sdf("input.sdf", remove_hs=False)
heavy = with_h.without_hydrogens()
assert with_h is not heavy
The same delayed-operation pattern applies to read_mol_from_str() and
read_sdf_from_str().
In-Place Operations¶
Performance-sensitive code can opt into explicit in-place mutation. Every
public Molecule in-place method ends with _; the trailing underscore has
no other Molecule API meaning.
mol = Molecule.from_smiles("CCO")
mol.add_hydrogens_()
mol.compute_2d_coordinates_()
Common in-place operations include:
add_hydrogens_()remove_hydrogens_()kekulize_()sanitize_()compute_2d_coordinates_()assign_chiral_tags_from_structure_()
If an in-place method returns an error, the molecule is not guaranteed to equal its pre-call value. Use the value-style method when failure-preserving behavior is required.
SMILES Output¶
to_smiles() returns a SMILES string:
mol = Molecule.from_smiles("F[C@H](Cl)Br")
print(mol.to_smiles())
print(mol.to_smiles(isomeric_smiles=False))
SMILES writer options are available on both single molecules and batches:
benzene = Molecule.from_smiles("c1ccccc1")
ethanol = Molecule.from_smiles("CCO")
print(benzene.to_smiles(kekule=True))
print(ethanol.to_smiles(all_bonds_explicit=True))
print(ethanol.to_smiles(canonical=False, rooted_at_atom=2))
Explicit Editing¶
Use Molecule.edit() when you want to stage changes and commit them as one
new molecule:
editor = mol.edit()
cl = editor.add_atom("Cl")
editor.add_bond(0, cl, order="single")
edited = editor.commit()
Serialization¶
Molecule supports Python pickle for in-process persistence and
inter-process transfer. The pickle state carries a COSMolKit pickle schema and
the versioned core molecule archive payload, so future incompatible payload
changes can be rejected explicitly instead of being decoded as the wrong
structure.
import pickle
mol = Molecule.from_smiles("F[C@H](Cl)[13CH3:7]").with_2d_coordinates()
restored = pickle.loads(pickle.dumps(mol, protocol=pickle.HIGHEST_PROTOCOL))
print(restored.to_smiles(canonical=False))
Advanced callers can use mol_to_binary() and mol_from_binary() to
inspect or persist the COSMolKit molecule archive directly. Python
applications should prefer pickle unless they specifically need the raw
archive payload:
payload = mol.mol_to_binary()
restored = Molecule.mol_from_binary(payload)
assert restored.to_smiles(canonical=False) == mol.to_smiles(canonical=False)
Depictions¶
Molecules with 2D coordinates can be exported as SVG or PNG:
mol = Molecule.from_smiles("c1ccccc1O").with_2d_coordinates()
svg = mol.to_svg(width=400, height=300)
mol.write_svg("python/examples/output/phenol.svg", width=400, height=300)
mol.write_png("python/examples/output/phenol.png", width=400, height=300)
Stereo¶
COSMolKit keeps the atom-level CW/CCW chiral tag path available. This is the closest representation to the explicit chiral information carried by SMILES or RDKit atoms:
from cosmolkit import ChiralTag, Molecule
mol = Molecule.from_smiles("F[C@H](Cl)Br")
for atom in mol.atoms():
if atom.chiral_tag() != ChiralTag.CHI_UNSPECIFIED:
print(atom.idx(), atom.chiral_tag().name)
print(mol.find_chiral_centers(include_unassigned=False))
Atom and bond enum-valued fields return Python IntEnum members, so callers
can compare or match against ChiralTag, BondOrder, BondDirection,
and BondStereo instead of spelling chemistry states as strings. Read-only
maps such as BOND_ORDER_MAP and CHIRAL_TAG_MAP are available when a
string name from an external source needs to be converted to the enum member.
When code needs COSMolKit’s ordered-ligand tetrahedral representation, use
tetrahedral_stereo(). The returned ligand order is the stereochemical
value, not just the atom adjacency order. This makes it useful both for
finding the four ligands around a center and for comparing whether two records
represent the same tetrahedral configuration. Equivalent even permutations are
canonicalized to one numeric representative. The precise contract is in
dev/tetrahedral_stereo.md.
mol = Molecule.from_smiles("F[C@H](Cl)Br")
print(mol.tetrahedral_stereo())
print(Molecule.from_smiles("F[C@@H](Cl)Br").tetrahedral_stereo())
print(mol.with_hydrogens().tetrahedral_stereo())
print(Molecule.from_smiles("F[C@](Cl)(Br)I").tetrahedral_stereo())
print(Molecule.from_smiles("F[C@@](Cl)(Br)I").tetrahedral_stereo())
None in the ligand list represents an implicit hydrogen ligand. It does
not mean the ligand slot is empty. If hydrogens are materialized with
with_hydrogens(), that hydrogen ligand is returned as an atom index.
Conformer Generation And Force-Field Optimization¶
Conformer generation APIs create native 3D conformers through the source-ported distance-geometry path. The default value-style operation uses ETKDGv3 and returns a new molecule value.
from cosmolkit import EmbedParameters, Molecule
mol = Molecule.from_smiles("CC(=O)NC").with_hydrogens()
params = EmbedParameters.etkdg_v3()
params.random_seed = 0xF00D
params.num_threads = 1
params.track_failures = True
embedded = mol.with_3d_conformer(params)
print(embedded.num_conformers())
print(embedded.coordinates_3d())
print(params.failures)
Manual Coordinate Assignment¶
Coordinate setters take a complete coordinate block and validate it before the molecule value is updated. This avoids exposing partially edited conformer state through Python.
import numpy as np
from cosmolkit import Molecule
mol = Molecule.from_smiles("CCO")
coords_2d = np.array(
[
[0.0, 0.0],
[1.5, 0.0],
[2.1, 1.2],
]
)
drawn = mol.with_2d_coordinates(coords_2d)
coords_3d = np.array(
[
[0.0, 0.0, 0.0],
[1.5, 0.0, 0.0],
[2.1, 1.2, 0.4],
]
)
placed = mol.with_added_3d_conformer(coords_3d)
shifted = placed.with_3d_coordinates(coords_3d + [0.0, 0.0, 1.0])
single = placed.with_only_3d_conformer(coords_3d + [0.0, 0.0, 2.0])
cleared = placed.with_cleared_3d_conformers()
print(drawn.coordinates_2d())
print(placed.num_conformers())
print(shifted.coordinates_3d())
print(single.num_conformers())
print(cleared.num_conformers())
The in-place forms follow COSMolKit’s trailing-underscore convention:
mol.set_2d_coordinates_(coords_2d)
conf_id = mol.add_3d_conformer_(coords_3d)
mol.set_3d_coordinates_(coords_3d + [0.0, 0.0, 1.0], conformer_index=conf_id)
mol.clear_3d_conformers_()
conf_id = mol.set_only_3d_conformer_(coords_3d)
2D assignment accepts shape (num_atoms, 2) or (num_atoms, 3). For
three-column input, z_policy controls whether the z column is ignored,
required to be zero, or rejected:
coords_2d_with_zero_z = np.column_stack([coords_2d, np.zeros(mol.num_atoms())])
mol.with_2d_coordinates(coords_2d_with_zero_z, z_policy="require_zero")
3D assignment accepts only shape (num_atoms, 3). All coordinate values must
be finite, and row counts must match mol.num_atoms().
with_only_3d_conformer(coords) is the direct value-style equivalent of
RDKit RemoveAllConformers(); AddConformer(conf, assignId=True) for manual
coordinate assignment; the in-place form is set_only_3d_conformer_(coords)
and returns conformer id 0.
For multi-conformer generation, explicit seeds are deterministic. RMS pruning, sequential seed expansion, and terminal-group symmetrization for pruning follow the source-ported RDKit path.
params = EmbedParameters.etkdg()
params.random_seed = 123
params.num_threads = 1
params.prune_rms_thresh = 0.5
params.enable_sequential_random_seeds = True
pruned = mol.with_3d_conformers(5, params)
print(pruned.num_conformers())
UFF and MMFF optimization APIs operate on existing or generated 3D conformers and return new molecule values through result objects. They do not mutate the source molecule.
from cosmolkit import Molecule
mol = Molecule.from_smiles("CCO").with_hydrogens().with_3d_conformer()
if mol.has_uff_params():
result = mol.with_uff_optimized(max_iters=200)
optimized = result.molecule()
print(not result.needs_more())
print(result.status_code())
print(result.energy())
print(optimized.coordinates_3d())
if mol.has_mmff_params():
result = mol.with_mmff_optimized(mmff_variant="MMFF94", max_iters=200)
optimized = result.molecule()
print(not result.needs_more())
print(result.status_code())
Substructure And SMARTS¶
Substructure matching functions accept molecule queries. This surface is unfinished until strict RDKit molecule-query parity tests pass:
import cosmolkit
mol = Molecule.from_smiles("CCO")
query = Molecule.from_smiles("CO")
print(cosmolkit.has_substruct_match(mol, query))
print(cosmolkit.get_substruct_match(mol, query).atom_mapping())
parse_smarts() exposes the Rust SMARTS parser as parse metadata. It returns
a SmartsMolecule query-tree value. Direct SMARTS query matching is not yet
a Python API; Python substructure functions currently accept molecule queries.
smarts = cosmolkit.parse_smarts("[#6]-O")
print(smarts.num_atoms())
print(smarts.num_bonds())