Skip to content
ReShiki / guide

Structure-to-name harness

src/naming/s2n_harness.rs (test-only) measures the structure-to-name namer by round trip through the in-process OPSIN Rust port. The namer sits behind one function, namer, which calls the openclatura port (naming::openclatura::name_smiles, re-exported from the external nazuchem-s2n dependency). The app’s naming worker calls the same engine’s style-aware entry point in its generate operation, and shows a name only after the OPSIN port reads it back to the drawn structure.

Terminal window
# Fast subset (normal test)
cargo test --locked -p reshiki --no-default-features --lib naming::s2n_harness::s2n_fast_subset -- --nocapture
# Metamorphic relations (normal tests)
cargo test --locked -p reshiki --no-default-features --lib naming::s2n_harness::metamorphic -- --nocapture
# Full corpus (ignored by default; nightly in .github/workflows/s2n.yml)
S2N_REPORT=s2n-report.tsv cargo test --locked -p reshiki --no-default-features --lib naming::s2n_harness::s2n_full_corpus -- --ignored --nocapture
# Regenerate baseline.tsv and status-snapshot.tsv, then review both diffs
S2N_UPDATE_BASELINE=1 cargo test --locked -p reshiki --no-default-features --lib naming::s2n_harness::s2n_fast_subset -- --nocapture

S2N_EXTRA=<file> with the ignored s2n_extra_file test round-trips any extra SMILES (first tab-separated column) and prints the wrong and unparseable rows; S2N_EXTRA_OUT=<file> writes every row, and S2N_EXTRA_GIVEN=1 round-trips the name in the second column instead of calling the namer (e.g. upstream’s names). S2N_DUMP=<dir> writes every classified row per check (class, category, SMILES, name, openclatura text for parity, decline reason) for analysis, and {fast,full}-status.tsv with the PIN status of every named row: check, SMILES, status (preferred, not_preferred or unverified) and reason. The ignored parity_file test in nazuchem-s2n/src/lib.rs names the SMILES in OPENCLATURA_PARITY and adds the same status and reason columns to each different row it writes. Run that engine test in a nazuchem checkout at ReShiki’s pinned revision.

The fast test fails on any Wrong name and when a correct count in tests/fixtures/s2n/baseline.tsv drops; it prints a reminder when one rises. The full test fails on any Wrong name. The workflow runs nightly, on manual dispatch and on pushes to feat/s2n, and uploads the full report.

tests/fixtures/s2n/status-snapshot.tsv holds the name, PIN status and reason of every fixture and generated row of the fast subset, once per SMILES. Declined rows have status declined and the decline message as reason. The fast test fails on any difference and prints each changed row’s old and new name, status and reason. Every PR that changes names or statuses must regenerate the snapshot with S2N_UPDATE_BASELINE=1, review each changed row, and list the changes in its PR body. A new Preferred label, or a change one PR causes in another’s results, is then visible in review.

tests/fixtures/s2n/worker_snapshot.jsonl freezes the complete worker responses for every distinct SMILES in the full bakeoff, openclatura and rules corpora. Requests follow first occurrence in that source order, with IUPAC generation for every input and Boughton generation for inputs containing an isotope label. Each successful generation is followed by a Verify request for its name, a strict Parse request for the same name, a Verify request for each alternative name and, when present, a Verify request for its radical-ion neutral-parent name. A few inputs from the unit tests (listed in worker_snapshot.rs) keep every optional response field populated; the test fails if one disappears. Requests use the app’s builders and current worker protocol.

Each JSONL row contains the request object and the original UTF-8 response string, or an {"error":"..."} object for a worker error. The replay compares response bytes, including JSON formatting and field order, and reports the total differences and the first ten expected/actual responses. It also checks that the frozen requests still cover the full corpora in the expected order.

Terminal window
# Replay the frozen worker responses in the normal cargo test profile
cargo test --locked --lib -p reshiki naming::s2n_harness::worker_snapshot::s2n_worker_snapshot -- --exact --nocapture
# Explicitly regenerate all requests and responses, then review the fixture diff
RESHIKI_UPDATE_S2N_SNAPSHOT=1 cargo test --locked --lib -p reshiki naming::s2n_harness::worker_snapshot::s2n_worker_snapshot -- --exact --nocapture

Run regeneration twice and compare the complete fixture before accepting an update. Do not normalize response JSON, sort its fields or remove variable data: any nondeterminism must be investigated. The fixture is test data and is excluded from formatting by bunfig.toml.

The IR worker uses protocol 7. The pre-migration fixture used protocol 6 and contained 4,933 requests in 3,243,897 bytes. Parse and Verify now return nazuchem_ir::Molecule instead of semantic CXSMILES, locant arrays and axis side channels. Generate retains its SMILES input and existing result semantics.

The one-off migration gates ran at commit 4ab8fd7b on branch feat/naming-ir-protocol. Their harness, worker_snapshot_migration.rs, was removed after the migration. To rerun these historical gates, preserve the fixture from main before that commit and create an isolated worktree at 4ab8fd7b:

Terminal window
# 4ab8fd7b's parent is main before the IR migration.
git show 4ab8fd7b^:tests/fixtures/s2n/worker_snapshot.jsonl > /tmp/reshiki-10b-old-worker_snapshot.jsonl
git worktree add --detach /tmp/reshiki-ir-migration-4ab8fd7b 4ab8fd7b
cd /tmp/reshiki-ir-migration-4ab8fd7b
RESHIKI_OLD_S2N_SNAPSHOT=/tmp/reshiki-10b-old-worker_snapshot.jsonl cargo test --locked --lib -p reshiki naming::s2n_harness::worker_snapshot::migration::s2n_worker_ir_equivalence -- --ignored --exact --nocapture

The ignored migration test at that commit replays every preserved request through the protocol-7 worker. It normalizes the protocol field and requires Generate response bytes to match. For Parse and Verify, it checks status, message, warnings, provenance, options and input name, then compares canonical chemical identity, first parent locants, semantic CX layers and explicit biaryl axes. The pinned IR keeps the old writer’s atom identities as a prefix; native hydrogen removal projects the prepared graph back to those identities. The gate validates this map against per-atom chemical facts and the complete edge set before comparing the literal parent-only first-locant vectors. Cases whose canonical identity is unsupported on both paths are counted explicitly; their mapped semantic groups and underlying specified stereochemistry must still match. Set RESHIKI_OLD_S2N_SNAPSHOT to the fixture from main before 4ab8fd7b, using another path if needed. This semantic comparison is specific to the migration; ordinary replay still compares the current wire response byte for byte.

The full migration-equivalence run on 2026-11-09 passed with zero differences and no skipped rows across all 4,933 requests:

Check Result
Generate 1,654 responses byte-identical after changing only protocol 6 to 7
Parse/Verify envelope 3,279 identical status, message, warnings, provenance, options and input-name results
Parse/Verify without a structure 83 identical absent-structure results
Canonical chemical identity 3,195 equal identities
Canonical rejection with relative stereo groups 1 matching rejection, with equal mapped membership and equal group-cleared specified stereo
Biaryl axes 2 equal mapped atom/reference/winding annotations

The companion size gate measures the snapshot requests and Parse/Verify requests for distinct names from the pinned OPSIN oracle fixtures. It reports the largest request and response for each operation against the unchanged 32,768-byte request and 65,536-byte response limits, including explicit worker-error counts:

Run this gate from the same 4ab8fd7b worktree, with a nazuchem checkout at its pinned revision, 6fcd7c4f9ef7b892ae97aa042b5b4e13ee1aebe3:

Terminal window
RESHIKI_OLD_S2N_SNAPSHOT=/tmp/reshiki-10b-old-worker_snapshot.jsonl RESHIKI_NAZUCHEM_CHECKOUT=/path/to/pinned/nazuchem cargo test --locked --lib -p reshiki naming::s2n_harness::worker_snapshot::migration::s2n_worker_ir_wire_sizes -- --ignored --exact --nocapture

The wire-size gate also passed on 2026-11-09. It measured the 4,933 snapshot requests and 3,576 Parse/Verify requests for 1,788 distinct OPSIN oracle names:

Measurement Snapshot maximum Oracle maximum Limit
Request, any operation 349 bytes 349 bytes 32,768 bytes
Parse response 8,059 bytes 18,358 bytes 65,536 bytes
Verify response 8,072 bytes 18,371 bytes 65,536 bytes

undecadictane produced both oracle response maxima. The largest response used 28.03% of the existing cap; the largest request used 1.07%. Two explicit request rejections were the Parse and Verify requests for one oracle name containing an embedded tab. No request or response was rejected by a wire cap, so both limits remain unchanged.

The current protocol-7 snapshot was regenerated twice on 2026-11-09. Both runs produced 4,933 rows and 8,020,627 bytes, byte for byte identical, with SHA-256 2188b9309fb252de6a2b29d777ace8c38413c0f2e8220e01b0ffb7a81cd72262. The ordinary byte-for-byte snapshot replay also passed.

src/naming/s2n_harness/metamorphic.rs names every fast-run structure, plus curated isotope, salt, charge, meso, E/Z and biaryl-axis cases, under four relations that must not change the name or status:

  • atom_order: three seeded random atom and component orders of the SMILES.
  • mirror: the mirror image (@ and @@ and the axis markers swapped) is named with every R/S, M/P and Ra/Sa swapped and r/s and E/Z kept. An achiral or meso input is its own mirror image. When only the R-before-S rule (P-31.1.4.3.4) decides the numbering, the enantiomer may be numbered the other way; its name must then differ in descriptors only and, mirrored back, name the input.
  • atom_maps: adding atom maps 1..n (inputs with axis markers are skipped).
  • isotope_swap: moving an isotope label to an equivalent atom.

tests/fixtures/s2n/metamorphic_known_failures.tsv lists the current failures (relation, canonical SMILES, reason). Each entry is an open bug: the test fails on a failure that isn’t listed and on an entry that no longer fails, so remove the entry with the fix.

Stereo-locant omission intentionally differs from the frozen 0.4.3 spelling at quinine’s methanol centre and a random-corpus hydrazone. Each descriptor is the only stereounit of its kind in its parent word, and that word cites no locants; locants inside separately enclosed substituents belong to those substituents. The descriptors therefore drop their locants (P-91.3): (1R) and (1Z) become (R) and (Z). The text-parity baseline records both; structure checks still require the same complete graphs. A word without locants still retains its stereodescriptor locant when inequivalent positions could carry it, as at the junctions of tetradecahydrophenanthrene.

The same decorated-parent proof also removes redundant constitutional locants when their possible placements are equivalent and the word cites no other locants (P-14.3.3 and P-14.3.4.6). This changes frozen spellings such as 2-phenylacetic acid, 2-phenylacetonitrile and 2-cyclopentylacetic acid. Amido prefixes retain locants when the real nitrogen supplies another substitution site, including prefixes assembled through a temporary acid. These locant changes cost six fast text-parity matches; the structure round-trip ratchet is 424/434 and parity is 421/431 after excluding annotated bad goldens, with zero wrong structures. The corpus retains its upstream source attribution, with later corrections and local expectation overrides documented below and in the fixture history; it is not a pristine upstream snapshot. Changed port expectations, including aminoacetic acid in rules_cases.tsv, are checked by OPSIN round trip.

Check What it does
structure SMILES → namer → OPSIN → structure; compare canonical isomeric SMILES (stereo, charge, isotopes).
name PubChem name → OPSIN → compare with the fixture’s canonical isomeric SMILES, then namer → OPSIN → compare again. A mismatch is wrong unless listed in name_known_failures.tsv; a PubChem name OPSIN rejects is input unparseable. Both exclusions stay out of the rate.
parity Namer text equals openclatura 0.4.3’s text exactly. Bakeoff goldens are eligible only when openclatura_status is correct; other statuses count as missing goldens. This is the Phase 2 port target.
generated Structure round trip on a seeded, deduplicated set of molecules built from fragments: chains with branches and unsaturation, monocycles and heteroaromatics, ring assemblies, spiro, bicyclo/tricyclo, fused aromatics, functional groups, E/Z and R/S, and ring + linker + group combinations. Every generated SMILES must parse.

Every molecule gets one class: correct (OPSIN rebuilds the same structure), wrong (OPSIN rebuilds a different structure; always a failure), declined (namer refused, or the input is outside naming’s identity rules), or unparseable (namer produced text that the OPSIN port rejects). The report lists every wrong and unparseable case and the top decline reasons.

The report also lists known PubChem-name/fixture mismatches with their expected and actual SMILES. Its per-category and total known_failure column counts these separately, like input_unparseable, outside total and correct. name_known_failures.tsv keys each entry by fixture SMILES and PubChem name, with an issue #N: diagnosis. Both runs check every entry, including random rows the fast run does not sample: it must exist in the bakeoff corpus, its name must still parse, and the parse must still differ from the fixture; a listed name that now matches fails with a request to remove the entry.

File Rows Source
bakeoff_corpus.tsv 703 Phase 0 bake-off: 500 random PubChem compounds and 203 curated ones in 14 categories, with PubChem IUPAC names and openclatura 0.4.3 names.
openclatura_tests.tsv 894 SMILES mined from openclatura’s tests (commit 90dc5bcb), named by openclatura 0.4.3. asserted is 1 or 0 where the test asserts a name that does or doesn’t match the output. Category is the test directory.
rules_cases.tsv 94 Cases of the removed rules namer, its name (empty: declined) and the port’s name; checked by s2n_rules_cases.
baseline.tsv 107 Fast-subset correct counts per check and category; parity_text rows hold exact text matches.
status-snapshot.tsv 909 Name, status and reason of every fast-subset fixture and generated row; see Status snapshot.
name_known_failures.tsv 0 PubChem-name/fixture identity mismatches, keyed by SMILES and PubChem name, with an issue-prefixed diagnosis; excluded from the name check’s rate.
metamorphic_known_failures.tsv 9 Open metamorphic-relation failures; see Metamorphic tests.

openclatura-derived data is MIT; see licenses/openclatura/.

The fast subset is all 203 curated rows, every 10th random PubChem row, every 5th openclatura row per category, and the first 500 generated molecules (934 structures). The full run uses all fixture rows and 3,000 generated molecules.

After the ground-truth checks, the fast name baseline is 247/247, with 6 input-unparseable rows excluded. Exact parity is 380/431; the baseline still has 107 rows and the unchanged status snapshot has 909 rows. Five bakeoff rows whose PubChem names omit stereo the fixture SMILES defines (#961) carry stereo-complete names instead: cis-1,4-dimethylcyclohexane, cis-4-tert-butylcyclohexan-1-ol, trans- and cis-1,2,3,4,4a,5,6,7,8,8a-decahydronaphthalene, and the imine row’s (1E,2E)-but-2-enylidene.

Final numbers (openclatura port, 2026-10-31)

Section titled “Final numbers (openclatura port, 2026-10-31)”

Phase 1 is the removed rules namer at 3a0c668f. Full corpus, correct of total; no check has a wrong name.

Check Phase 1 rules namer openclatura port
structure 145 / 1597 1536 / 1597
name 31 / 685 683 / 685
parity (exact text) 105 / 1592 1569 / 1592
generated 1717 / 3000 3000 / 3000

Baseline (openclatura port, stage 4, 2026-10-31)

Section titled “Baseline (openclatura port, stage 4, 2026-10-31)”

namer calls naming::openclatura::name_smiles, the Rust port of openclatura 0.4.3. It names one connected component: chains, monocycles, ring assemblies, spiro systems, von Baeyer systems and fused ring systems, with heteroatom replacement, unsaturation, suffixes, charges and stereo. Fused parents come from upstream’s retained templates (naphthalene, 1H-indole, quinoline, purine, 9H-carbazole) or its systematic fusion planner (pyrazolo[3,4-d]pyrimidine, [1,2,4]triazolo[4,3-a]pyridine, dibenzo...), with hydro prefixes, indicated and added hydrogen, bridged fused parents (4,7-methano-1H-indene, 1,4-epoxynaphthalene), skeletal replacement on a fusion parent, and spiro unions with fused sides. More than one component still declines. No wrong names in any check. Unparseable rows are openclatura names the OPSIN port cannot read (mostly chalcogen peroxides and thioperoxy anhydrides); they are not counted as correct. Fast subset about 17 s; full corpus about 90 s.

Stage 1 (at most one ring; 659 corpus rows): 615 correct, 0 wrong, 41 unparseable, 3 declined; text equals openclatura’s for 654 of 658 rows with a golden name. Stage 2 (several rings, no shared bond; 324 corpus rows): all 324 correct, and the text equals openclatura’s for 320. The other 4 differ only by input atom order: the harness names its canonical SMILES, and upstream’s tie-breaking depends on atom order (Python itself returns the port’s name for another atom order). Named from their original SMILES, the stage-2 corpus and generated molecules (619) and held-out generated sets of about 16,000 ring assemblies and spiro systems match upstream exactly except for 121 declines and one E/Z descriptor. In 103 of those declines and the E/Z row, upstream’s own name round-trips to a different structure; the other 18 come from conservative checks on stereo the OPSIN reading can invert: stereocentres ranked through a mancude-ring duplicate atom (RDKit’s CIP labels and OPSIN can disagree there), nitrogen centres and ring centres with a lone pair (P, As, S+). Lines marked Upstream divergence in the nazuchem-s2n sources list every such decline. The remaining mr_assembly and mr_spiro rows need fused parents (naphthalene, spiro[indene-1,1'-indene]). Previous rules-namer baseline: structure 145, name 31, parity 105, generated 1717 (full).

Stage 3 (von Baeyer systems; 604 corpus rows had a fused or bridged block): 35 are now correct, including all 14 mr_bridged rows; 34 equal openclatura’s text and one (a corrin skeleton) gets a von Baeyer name where upstream uses a retained name. The other 569 are fused or fusion-eligible (555) or spiro junctions on a polycyclic block (14). Named from their original SMILES, the in-scope corpus and generated molecules match upstream for 212 of 214 (both others are spiro junctions), and held-out generated sets of about 6,000 von Baeyer molecules match for 83–92%; the rest decline. Most of those declines are stereo that upstream’s own name loses or that the OPSIN reading cannot place: unresolved ring stereo, nitrogen and lone-pair centres, pseudoasymmetric (r/s) centres on a von Baeyer parent, and secondary bridges numbered in the direction OPSIN does not read. The generated tricyclo declines (61) are fusion names upstream (decahydrocyclopenta[cd]pentalene, octahydro-2,5-methanopentalene).

Stage 4 (fused ring systems; 569 corpus rows still declined after stage 3): the full structure round trip has 20 declines left: 8 salts and mixtures (more than one component), 4 inputs outside naming’s identity rules, 4 fused parents whose localized nonaromatic pi state is not ported, 2 unresolved multicyclic stereo, one Cr compound and one secondary bridge OPSIN numbers differently. pubchem_random is 490 of 500 correct (Python openclatura: 495 in the bake-off). Named from their original SMILES, held-out sets of 2,023 random PubChem compounds with a fused or bridged block and 2,000 generated fused systems match upstream’s text for 1,992 and 1,723 rows; 1,988 of the 2,000 PubChem rows whose Python name round-trips are exact. About 23,000 rows of held-out and adversarial molecules round-trip with no wrong name. Upstream’s own name is wrong for some inputs (protonated fused nitrogen, quinoid and localized pi states of retained parents, bridges without a valence proof, re-primed dispiro locants); the port declines those classes, and each such place is marked Upstream divergence.

Intentional PIN corrections count against exact text parity with openclatura 0.4.3. Under P-14.3.4.2(c), the suffix locant 1 is omitted only on a monosubstituted homogeneous monocycle: 2-methylcyclohexan-1-ol replaces upstream’s 2-methylcyclohexanol. Substitution on a suffix heteroatom alone still permits omission, as in N-methylcyclohexanamine; retained names such as 2-methylphenol and 2-methylbenzoic acid are unchanged. Four openclatura_tests.tsv expectations have corrected suffix locants but retain upstream’s cis/trans wording; these already differ from the generated r/s names and do not affect exact parity. This costs three fast text matches (drug 18→16, natural_product 17→16) and six full text matches, three of them in pubchem_random. Structure, name and generated round-trip counts do not change. Ring-assembly parents (P-28), such as [1,1′-biphenyl]-4-ol for upstream’s 4-phenylphenol, cost eight more (mr_assembly 10→3, roundtrip one). With all three changes the fast text ratchet is 386/434.

Check Fast Full
structure 420 / 434 1536 / 1597 (96.2%)
name 245 / 246 683 / 685 (99.7%), 18 input unparseable
parity (exact text) 425 / 434 1569 / 1592 (98.6%)
generated 500 / 500 3000 / 3000 (100.0%)
unparseable (structure) 10 41
Category Fast correct / total Full correct / total
alcohol_amine 13 / 13 13 / 13 (100.0%)
alkenyl_benzyl 10 / 10 10 / 10 (100.0%)
chain_ring_prefix 12 / 12 12 / 12 (100.0%)
data 13 / 13 63 / 63 (100.0%)
drug 20 / 20 20 / 20 (100.0%)
exploratory 1 / 1 4 / 4 (100.0%)
fixtures 22 / 22 106 / 106 (100.0%)
heterocycle 25 / 25 25 / 25 (100.0%)
integration 16 / 16 75 / 80 (93.8%)
mr_assembly 10 / 10 10 / 10 (100.0%)
mr_bridged 14 / 14 14 / 14 (100.0%)
mr_fused 18 / 20 18 / 20 (90.0%)
mr_macro_other 6 / 7 6 / 7 (85.7%)
mr_spiro 7 / 7 7 / 7 (100.0%)
natural_product 19 / 19 19 / 19 (100.0%)
pubchem_random 49 / 50 490 / 500 (98.0%)
ring_ON_ester_amide 10 / 10 10 / 10 (100.0%)
roundtrip 77 / 77 381 / 383 (99.5%)
stereo 16 / 16 16 / 16 (100.0%)
suffix_on_ring 20 / 20 20 / 20 (100.0%)
unit 42 / 52 217 / 258 (84.1%)
total 420 / 434 1536 / 1597 (96.2%)
Category Fast correct / total Full correct / total
alcohol_amine 13 / 13 13 / 13 (100.0%)
alkenyl_benzyl 10 / 10 10 / 10 (100.0%)
chain_ring_prefix 12 / 12 12 / 12 (100.0%)
drug 19 / 19 19 / 19 (100.0%)
heterocycle 25 / 25 25 / 25 (100.0%)
mr_assembly 10 / 10 10 / 10 (100.0%)
mr_bridged 14 / 14 14 / 14 (100.0%)
mr_fused 20 / 20 20 / 20 (100.0%)
mr_macro_other 5 / 6 5 / 6 (83.3%)
mr_spiro 7 / 7 7 / 7 (100.0%)
natural_product 17 / 17 17 / 17 (100.0%)
pubchem_random 49 / 49 487 / 488 (99.8%)
ring_ON_ester_amide 10 / 10 10 / 10 (100.0%)
stereo 14 / 14 14 / 14 (100.0%)
suffix_on_ring 20 / 20 20 / 20 (100.0%)
total 245 / 246 683 / 685 (99.7%)
Category Fast exact / total Full exact / total
alcohol_amine 13 / 13 13 / 13 (100.0%)
alkenyl_benzyl 10 / 10 10 / 10 (100.0%)
chain_ring_prefix 12 / 12 12 / 12 (100.0%)
data 13 / 13 63 / 63 (100.0%)
drug 20 / 20 20 / 20 (100.0%)
exploratory 1 / 1 4 / 4 (100.0%)
fixtures 22 / 22 106 / 106 (100.0%)
heterocycle 25 / 25 25 / 25 (100.0%)
integration 16 / 16 76 / 80 (95.0%)
mr_assembly 10 / 10 10 / 10 (100.0%)
mr_bridged 14 / 14 14 / 14 (100.0%)
mr_fused 18 / 20 18 / 20 (90.0%)
mr_macro_other 5 / 7 5 / 7 (71.4%)
mr_spiro 7 / 7 7 / 7 (100.0%)
natural_product 19 / 19 19 / 19 (100.0%)
pubchem_random 46 / 50 485 / 495 (98.0%)
ring_ON_ester_amide 10 / 10 10 / 10 (100.0%)
roundtrip 77 / 77 380 / 383 (99.2%)
stereo 16 / 16 16 / 16 (100.0%)
suffix_on_ring 20 / 20 20 / 20 (100.0%)
unit 51 / 52 256 / 258 (99.2%)
total 425 / 434 1569 / 1592 (98.6%)
Category Fast correct / total Full correct / total
acid 21 / 21 125 / 125 (100.0%)
alcohol 21 / 21 125 / 125 (100.0%)
aldehyde 21 / 21 125 / 125 (100.0%)
alkene 21 / 21 125 / 125 (100.0%)
alkene-stereo 20 / 20 125 / 125 (100.0%)
alkyl 21 / 21 125 / 125 (100.0%)
alkyne 21 / 21 125 / 125 (100.0%)
amide 21 / 21 125 / 125 (100.0%)
amine 21 / 21 125 / 125 (100.0%)
bicyclo 21 / 21 125 / 125 (100.0%)
ester 21 / 21 125 / 125 (100.0%)
ether 20 / 20 125 / 125 (100.0%)
fused-aromatic 21 / 21 125 / 125 (100.0%)
halide 21 / 21 125 / 125 (100.0%)
heteroaromatic 21 / 21 125 / 125 (100.0%)
ketone 21 / 21 125 / 125 (100.0%)
monocycle 21 / 21 125 / 125 (100.0%)
n-substituted-amide 21 / 21 125 / 125 (100.0%)
nitrile 21 / 21 125 / 125 (100.0%)
ring-assembly 21 / 21 125 / 125 (100.0%)
ring-with-group 20 / 20 125 / 125 (100.0%)
spiro 21 / 21 125 / 125 (100.0%)
tetrahedral-stereo 20 / 20 125 / 125 (100.0%)
tricyclo 21 / 21 125 / 125 (100.0%)
total 500 / 500 3000 / 3000 (100.0%)

Outside its optional self-audit (audit/, which the OPSIN round trip replaces) and its own OPSIN check, openclatura uses RDKit only to read the structure: SMILES parsing, kekulization, ring info (FastFindRings), legacy stereo perception (AssignStereochemistry + FindMolChiralCenters, which decides which atoms are stereocentres) and the modern rdCIPLabeler (which decides their labels, including pseudo-asymmetric r/s). crates/chemistry has ports of all of these: legacy perception in stereo/perception.rs, the new CIP labeler in stereo/cip/ (R/S, r/s, E/Z, M/P), kekulize.rs and rings.rs. Points to settle in Phase 2:

  • rings::fast is a depth-first cycle basis like RDKit’s FastFindRings, not a smallest or symmetric set; rings::perceive gives the symmetric SSSR. Match each openclatura ring query to the right one.
  • Canonical kekulization needs caller-supplied canonical ranks; backtracking is bounded.
  • The potential-stereocentre predicate in perception is crate-private.
  • The naming input boundary (naming::canonical_smiles) rejects allene and coordination stereo, and the port keeps that check.