Language Bindings Cross-Reference
Rust crates, Python (PyO3), and WASM (wasm-bindgen) share the Rust core but have different API and error shapes. This page names those differences.
Atom provenance and output order
The atom-tracking APIs expose only mappings with a documented atom order. The checked reaction additions below are included since v1.0.32; the published v1.0.38 83-row gate is 80 exact graph/origin/map rows and three inputs invalid in both engines.
| Capability | Rust | Python | WASM / Node |
|---|---|---|---|
| SMILES atom output order | write_with_atom_order, canonical_smiles_with_atom_order |
Mol.smiles_with_atom_order() |
Not exposed |
| Connected-component source atom indices | Molecule::fragments_with_source_atoms() |
Mol.connected_components_with_atom_indices() |
Not exposed |
| Reaction product atom provenance and template maps | PreparedReaction::apply_match_traced, run_reactants_traced_with_rejection_diagnostics |
run_smirks_checked() returns product_atom_sources and product_template_maps |
run_reactants_checked() returns the same JSON arrays |
| Opt-in product rejection diagnostics | run_reactants_traced_rdkit_2026_03_6_detailed returns typed reasons and bounded atom context |
run_smirks_checked(..., rdkit_compat=True) returns rejection_diagnostics |
run_reactants_checked(..., true) returns the same JSON field |
| Label-stable R-group columns | rgroup_decompose_labeled() returns aligned optional rows |
rgroup_decompose_columns() |
rgroup_decompose_columns_json() |
order[k] is the input atom index for the k-th atom written in a SMILES
string; it is also that atom's index after parsing the returned string.
Reaction tracing returns None for an atom newly created by the product
template. Python arrays follow the returned Mol atom indices; WASM/Node
arrays follow the returned canonical SMILES parse order. Template-map labels
may be None even when an atom has a reactant origin. These are additive APIs:
ordinary SMILES writing, connected components, and reaction application retain
their existing return values. The published rerun
records the current bounded reaction result; it is not general SMIRKS parity.
R-group column output is input-aligned and uses null/None for unmatched
or absent rows. It covers the documented mapped terminal-wildcard subset; it
does not imply RDKit's multi-core alignment, enumeration, or GA scoring.
See also: format-capabilities.md for the
per-format read/write/streaming/limits matrix this page's examples are
drawn from.
RDKit-compatible Python import surface
The Python package includes an explicit, lightweight compatibility namespace at
chematic.rdkit_compat. The common Morgan entry points below resolve to the
same chematic-backed fingerprint implementation, so migration does not depend
on one particular RDKit import style:
from chematic import rdkit_compat as Chem
from chematic.rdkit_compat import AllChem, rdFingerprintGenerator
mol = Chem.MolFromSmiles("CCO")
legacy = AllChem.GetMorganFingerprintAsBitVect(mol, 2)
stable = rdFingerprintGenerator.GetMorganGenerator(
radius=2, fpSize=2048
).GetFingerprint(mol)
assert legacy.GetOnBits() == stable.GetOnBits()
Chem.CanonSmiles is also available. Unsupported RDKit options such as
count-simulation Morgan fingerprints raise NotImplementedError; they are not
silently substituted with a different algorithm. This is a Python API
compatibility surface, not a claim of bit parity with RDKit's C++ implementation.
RDKit-compatible WASM / Node surface
MolHandle exposes the separately named rdkit_* writer, scaffold, stereo,
hash, Morgan, alignment, seeded-embedding, and bounds-matrix operations. The
free functions rdkit_from_pdb_block, rdkit_from_xyz_block, and
rdkit_from_mol2_block return a RdkitReadResult with molecule(),
coords_json(), and sanitized smiles where applicable.
All atom indices and coordinate rows are zero-based and follow the returned
MolHandle order. Alignment search is capped at 1,000,000 matches, stereo
enumeration at 4,096 isomers, and ETKDG/bounds output at 512 atoms. These are
the RDKit 2026.03.1 compatibility paths; the existing native browser APIs are
unchanged.
The three surfaces, in one sentence each
- Rust (
chematic-*crates): the source of truth. Typed errors (thiserror-style enums), zero-copy where the type system allows it,BufRead-based streaming where a reader type exists. - Python (
chematic-py, PyO3): free functions mirroring the Rust free functions where practical (parse_mmcif,parse_pqr, ...), plusMoland a handful of dedicated pyclasses (VolumetricGrid,LammpsDumpFrame,PipelineV2Config). Typed Rust errors map to PythonValueError. NumPy arrays are used for large numeric payloads (fingerprints, grid values) — always fresh copies, never views. - WASM (
chematic-wasm, wasm-bindgen): mostly free functions returning JSON strings (Result<String, JsValue>), with selected functions also returningjs_sys::Float64Array/Uint32Arrayfor large numeric grid/row payloads — also always fresh copies, never views. Typed Rust errors map to a thrown JS error /JsValue.
Python NumPy arrays and WASM typed arrays are copies, not Rust-memory views.
Parsers are format-specific; there is no parse_any(text) auto-detection.
Protein structure projection follows the same rule: Rust exposes typed
ProteinProjection/ProteinChain/ProteinResidue/ProteinAtom values through
project_protein_from_pdb and project_protein_from_mmcif; Python and WASM
expose the same bounded hierarchy as JSON through protein_from_pdb_json and
protein_from_mmcif_json. It is an amino-acid-only view, not a full PDB/mmCIF
biological-assembly model.
Copy-vs-view semantics
| Boundary | Owns a view of Rust memory? | Notes |
|---|---|---|
Python NumPy arrays (ecfp4_numpy(), the other *_fp_numpy() fingerprint methods, VolumetricGrid.values, VolumetricGrid.values_3d, ...) |
No — fresh copy | Fingerprints are one-dimensional uint8 arrays in the same LSB-first order as their packed-byte methods. values_3d reshapes the flat values copy to (nx, ny, nz), third axis (k) fastest, matching chematic_mol::VolumetricGrid::checked_index. |
WASM js_sys::Float64Array/Uint32Array (cube_values_f64, opendx_values_f64, lammps_dump_rows_f64, lammps_dump_cartesian_positions_f64, cube_shape_u32, opendx_shape_u32) |
No — fresh copy | Additive alongside the JSON-string functions; neither replaces the other. |
WASM JSON strings (cube_grid_json, opendx_grid_json, mmcif_to_json, ...) |
No — serialized copy | The oldest/default binding shape in chematic-wasm; large numeric arrays here round-trip through a JSON number array, a disclosed perf trade-off versus the typed-array functions above. |
Unit-conversion ownership: who converts what
| Concept | Owned by | Where |
|---|---|---|
| Gaussian Cube Bohr/Ångström tag | Rust core | GridUnits::{Bohr, Angstrom} — read and preserved as-is, never silently converted, in Rust, Python, and WASM alike. |
| OpenDX Ångström assumption | Rust core | No in-file unit tag exists; every parsed grid is tagged Angstrom by convention in chematic_mol::opendx. This is a core-crate assumption, not a binding-layer one — Python and WASM inherit it unchanged. |
| QCSchema geometry Bohr → Ångström | Binding layer, not core | chematic_mol::QcMolecule.geometry stays in Bohr (a0), matching the QCSchema spec. Python and WASM bindings convert to Ångström for convenience when exposing coordinates — this conversion does not happen in chematic-mol itself. |
| mmCIF/PQR/ORCA coordinate units | Rust core | Always Ångström as documented per-format; no binding-layer conversion involved since there is nothing to convert. |
None / null / Err divergence points
These are real, disclosed differences in how "no value" is represented across the three languages for the same underlying Rust computation — not accidents.
LAMMPS dump Cartesian positions — the clearest case
chematic_mol::LammpsDumpFrame::cartesian_positions() returns
Option<Vec<[f64; 3]>> — None when the frame has no x y z or xs ys zs
columns (it deliberately does not fall back to xu yu zu; see
format-capabilities.md).
| Language | Function | "no value" representation |
|---|---|---|
| Rust | LammpsDumpFrame::cartesian_positions() |
None |
| Python | LammpsDumpFrame.cartesian_positions() |
(Python-native None, same Option mapping) |
| WASM (JSON) | lammps_dump_cartesian_positions_json |
JSON null |
| WASM (typed array) | lammps_dump_cartesian_positions_f64 |
Err, not null — a Float64Array has no null representation, so the unresolvable case becomes a thrown error with a message naming the columns it looked for. |
The JSON and typed-array siblings deliberately differ; see their definitions
in crates/chematic-wasm/src/format_io.rs.
Typed-error mapping
| Rust | Python | WASM |
|---|---|---|
A typed error enum (e.g. LammpsDataError::UnsupportedAtomStyle, OpenDxError::NonAngstromUnits, MmcifError::...) |
ValueError with the Rust error's Display text as the message |
Thrown JS error / JsValue::from_str(...) with the same Display text |
Both bindings flatten the Rust error variant to message text; they do not expose its structured fields.
QCSchema unknown fields
chematic_mol::qcschema's unknown_fields: JsonObject bag (kept distinct
from the spec's own open extensibility bags — extras/keywords/
protocols/native_files/properties) round-trips losslessly in all
three language bindings — Rust, Python (routed through Python's own
json module as the dict↔text boundary, not a hand-written field mapper),
and WASM. This is one of the few places where all three surfaces behave
identically rather than diverging; noted here for contrast with the
divergent cases above.
Worked examples: same concept, three languages
Gaussian Cube parse + grid values
| Language | Parse | Access grid values |
|---|---|---|
| Rust | chematic_mol::parse_cube(text) / parse_cube_with_limits(text, limits) → VolumetricGrid |
grid.values: Vec<f64> (flat, row-major, third-axis-fastest — see checked_index) |
| Python | chematic.VolumetricGrid.from_cube(text) (staticmethod) |
grid.values (flat NumPy copy) or grid.values_3d (reshaped (nx, ny, nz) NumPy copy, k fastest) |
| WASM | cube_grid_json(text) → JSON string |
cube_values_f64(text) → Float64Array (flat, same ordering) or parse the JSON string's values field |
OpenDX strict vs. lossy write
| Language | Strict (fails closed on non-Ångström units) | Explicit lossy (Bohr→Ångström, opt-in) |
|---|---|---|
| Rust | write_opendx(&grid) → Err(OpenDxError::NonAngstromUnits) for a Bohr-tagged grid |
write_opendx_lossy(&grid) — rescales origin/axes only, never values |
| Python | grid.to_opendx() → raises ValueError for Bohr |
grid.to_opendx_lossy() |
| WASM | write_opendx_json(grid_json) → Err/thrown error |
write_opendx_lossy_json(grid_json) |
No language collapses these into a single lossy-by-default function — the strict/lossy split from the Rust core is preserved identically in every binding.
Streaming vs. materialization, by language
| Format | Rust | Python | WASM |
|---|---|---|---|
| MOL/SDF | SdfFileReader<R: BufRead> streams records |
iter_sdf / iter_sdf_batched stream from files with seen/emitted/rejected accounting |
sdf_records_batch_json is bounded and resumable over an in-memory string, not file-backed streaming |
| XYZ/Extended XYZ | File-backed readers are fail-stop on ambiguous frame boundaries | Batch readers use one-line look-ahead recovery and count rejected frames | xyz_frames_batch_json / extxyz_frames_batch_json resume over an in-memory string; the convenience path recovers only at unambiguous count lines |
| LAMMPS dump/trajectory | LammpsDumpReader<R: BufRead> — true streaming Iterator |
parse_lammps_dump_all materializes the whole trajectory as a list (disclosed scope choice, not a silently dropped capability) |
lammps_trajectory_to_json materializes (same disclosed choice) |
| Gaussian Cube | CubeFileReader<R: BufRead> streams the input reading only — the returned VolumetricGrid.values is still one fully-materialized Vec<f64> (single-dataset format, nothing to iterate across) |
via VolumetricGrid.from_cube(), materializes |
materializes |
| Other documented formats | no BufRead-backed streaming reader type exists |
materializes | materializes |
LAMMPS bindings deliberately materialize even though Rust has a streaming reader.
Fail-closed behavior (binding-independent)
These fail-closed checks live in the Rust core and are inherited unchanged by both bindings — no binding loosens or bypasses them:
write_opendxon aBohr-tagged grid → typed error in all 3 languages (see OpenDX table above).- Both OpenDX writers on a grid with any atoms →
OpenDxError::AtomsNotSupportedin all 3 languages (no lossy-atom-dropping path exists at all). - Gaussian Cube multi-dataset input →
CubeError::MultiDatasetUnsupportedin all 3 languages (typed rejection, not silent truncation). - LAMMPS data with an unrecognized
atom_style→LammpsDataError::UnsupportedAtomStylein all 3 languages (no best-effort guess). - LAMMPS "Type Labels" sections → rejected in all 3 languages.
See error-and-limits.md for the full typed-error
taxonomy and parse-limit reference.