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()
Elements and Periodic-Table Metadata¶
Element is a stable integer enum whose values are atomic numbers. Zero is
the dummy atom, and 1 through 118 represent H through Og. Symbol lookup is
case-sensitive and metadata comes from the same source-aligned periodic table
used by the chemistry core:
from cosmolkit import Element, element_from_symbol, get_element_info
assert Element.C == 6
assert element_from_symbol("Cl") == Element.CL
chlorine = get_element_info(Element.CL)
assert chlorine.symbol() == "Cl"
assert chlorine.atomic_number() == 17
ELEMENT_MAP maps canonical symbols, the dummy symbol "*", and retained
source aliases to Element values. Molecule atoms expose the same identity
through atom.element().
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. The versioned archive preserves graph, coordinate, property,
and materialized derived chemistry state so supported operations retain the
same behavior after restoration; legacy archive readers remain available for
older payloads:
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.
Potential Stereo And Stereoisomer Enumeration¶
analyze_potential_stereo() returns an isolated molecule state and ordered,
typed potential-stereo records without mutating the source. Each record reports
its stereo type, specified state, atom or bond center, descriptor, permutation,
and ordered controlling atoms.
stereoisomers() returns a lazy iterator. The default options enumerate only
unassigned atom and double-bond stereo, include enhanced stereo-group flippers,
deduplicate by canonical isomeric SMILES, and yield at most 1,024 outputs. A
molecule with no selected center yields one isolated molecule value.
from cosmolkit import Molecule, StereoisomerOptions
source = Molecule.from_smiles("CC(F)C(Cl)Br")
analysis = source.analyze_potential_stereo()
print([(item.center_kind, item.center_index) for item in analysis.stereo_info])
print(source.stereoisomer_count())
options = StereoisomerOptions(max_isomers=4, rand=0xF00D)
for isomer in source.stereoisomers(options):
print(isomer.to_smiles())
assert source.to_smiles() == "CC(F)C(Cl)Br"
max_isomers bounds successful outputs. When it is smaller than the
configuration space, rand follows Python random.Random semantics; a
random.Random instance or subclass can supply getrandbits() lazily.
try_embedding=True applies the source-defined one-conformer geometry
filter, and embedding failures do not consume the successful-output limit.
only_unassigned=False also enumerates assigned centers,
only_stereo_groups=True restricts selection to enhanced stereo groups, and
unique=False retains source configurations that canonicalize to the same
isomeric SMILES.
Iterator errors are raised when the corresponding output is requested. The
source molecule remains unchanged, including when candidate discovery,
custom random generation, finalization, or embedding fails. This API matches
the pinned RDKit Python EnumerateStereoisomers boundary; the separate newer
C++ enumerator and atropisomer enumeration are not part of this surface.
Modern CIP Labels¶
with_cip_labels() assigns molecular-context CIP descriptors and returns a
new molecule. assign_cip_labels_() is the explicit in-place form. After
assignment, atom and bond records expose cip_descriptor() and
cip_neighbor_order(); cip_computed() reports the molecule-level
completion state.
from cosmolkit import Molecule
mol = Molecule.from_smiles("C[C@H](F)Cl")
labeled = mol.with_cip_labels()
print(labeled.atoms()[1].cip_descriptor())
alkene = Molecule.from_smiles("F/C=C/F")
alkene.assign_cip_labels_()
print(alkene.bonds()[1].cip_descriptor())
Pass atoms=[...] or bonds=[...] for selected assignment. Priorities
remain molecular-context dependent; selection controls which configurations
are labeled, not how their priorities are calculated. The source-backed
boundary includes tetrahedral R/S/r/s, double-bond E/Z, and
atropisomeric M/P/m/p descriptors constructed by the pinned RDKit
findConfigs dispatcher. The maintained ChEMBL 37 phase compares full,
selected-atom, selected-bond, and empty-selection state across 2,854,362
eligible molecules with exact agreement.
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())
Molecular Alignment And RMSD¶
MolAlign measurement methods do not change either molecule. Use
alignment_transform_to() for an explicit or automatic first-match
transform, best_alignment_to() or best_rmsd_to() for symmetry-aware
best-map alignment, and coordinate_rmsd_to() to measure coordinates in
their existing frames without alignment.
import numpy as np
from cosmolkit import AlignmentAtomMap, AlignmentParameters, Molecule
reference_coords = np.array(
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 2.0, 0.0]]
)
probe_coords = reference_coords + np.array([3.0, -2.0, 1.0])
reference = Molecule.from_smiles("CCC").with_only_3d_conformer(reference_coords)
probe = Molecule.from_smiles("CCC").with_only_3d_conformer(probe_coords)
params = AlignmentParameters(
atom_map=[AlignmentAtomMap(index, index) for index in range(3)]
)
measured = probe.alignment_transform_to(reference, params)
aligned, applied = probe.with_alignment_to(reference, params)
assert np.array_equal(probe.coordinates_3d(), probe_coords)
assert measured.rmsd() < 1.0e-8
assert applied.rmsd() < 1.0e-8
assert np.allclose(aligned.coordinates_3d(), reference_coords)
with_alignment_to() returns a new molecule and result. align_to_() is
the explicit in-place form. all_conformer_best_rmsds() is read-only and
returns RDKit’s triangular pair order; with_aligned_conformers() and
align_conformers_() expose value-style and in-place conformer-set
alignment. Its AllConformerRmsdParameters exposes exactly the source call’s
map, weight, match-limit, terminal-symmetry, hydrogen, and threading controls;
reflection and iteration count are fixed by that RDKit path. Weighted and
reflected pair alignment, stored conformer IDs, match limits, terminal-group
symmetry, and iteration limits are available through the other typed parameter
classes. O3A and MMFF/Crippen alignment scoring are separate capabilities
outside this ordinary MolAlign boundary.
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 ordinary molecules or query-bearing
Molecule values compiled by the canonical SMARTS parser:
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() returns an ordinary Molecule carrying the compiled query
graph. The same value can be passed directly to the substructure functions.
smarts = cosmolkit.parse_smarts("[#6]-O")
print(smarts.num_atoms())
print(smarts.num_bonds())
print(smarts.to_smarts())
print(cosmolkit.has_substruct_match(mol, smarts))
Query-bearing molecules can be written as ordinary SMARTS or CXSMARTS without converting them to a separate query type:
labeled = cosmolkit.parse_smarts("[#6] |$site$|")
print(labeled.to_smarts())
print(labeled.to_cx_smarts())