orthonym.jvm_bridge#

Note

Internal API. Names and behaviour may change between releases.

ONE lazily-started in-process JVM (via JPype) shared by the OPSIN and centres bridges.

WHY#

Both JVM bridges shelled out java -jar... per call. Measured 2026-07-30 by counting subprocess invocations whose argv contains java over 15 molecules spanning all naming classes: 65 spawns (4.33/molecule) before any fix, 37 (2.47/molecule) after the _java_available probe was cached in ``. Of those 37, 30 came from ``centres_label_batch`` — a function named “batch” that production calls twice per molecule with ONE molecule each, so its batching never amortised anything. Each spawn is ~130 ms of pure process launch.

JPype starts one JVM inside this process and hands back live Java objects, so parseChemicalName becomes a JNI call rather than a process launch. Measured on the real single-molecule production pattern (60 calls): 216 ms/call -> 0.8 ms/call.

BYTE-IDENTITY IS THE CONTRACT#

This is a pure performance change: it must not alter one character of one name. So this module does not reimplement either tool — it drives the same code the CLI drives, and it reproduces the CLI’s own input/output framing exactly:

  • OPSIN — opsin/opsin-cli/.../Cli.java:interactiveSmilesOutput calls nts.parseChemicalName(name, cfg) and writes result.getSmiles (nothing at all when it is null) followed by a newline. generateOpsinConfigObjectFromCmd sets all five config flags explicitly from cmd.hasOption(...), so with no flags every one is false and with -r only allowRadicals is true. We set all five explicitly rather than trusting NameToStructureConfig’s defaults, because the CLI does. Two CLI framing details are reproduced deliberately:

    • the CLI splits each input line at the first TAB and parses only the part before it (line.indexOf('\t')), so we truncate identically;

    • the CLI reads line by line, so a name containing a newline is two inputs to it and one to us. Those are NOT equivalent, so such names are refused here and the caller’s subprocess path handles them.

  • centres — com.simolecule.centres.LabelCip exposes only main(String) (verified with javap); there is no programmatic API to call, and reimplementing its logic would risk changing labels. So we invoke LabelCip.main with the identical argv and the identical temp file, capturing System.out into a ByteArrayOutputStream. Verified with javap -c: LabelCip contains zero ``System.exit`` calls, so calling main in-process cannot terminate the interpreter.

Rather than re-parse anything, the OPSIN entry point returns the exact bytes the CLI would have written to stdout, so each caller keeps its own existing output handling unchanged and merely receives it from a cheaper source.

Validation (a temp dir harnesses, denominators asserted): 217 distinct real names x both shipped configs = 434 pairs, 0 mismatches vs the real CLI; centres 508 SMILES as one batch plus 60 single calls, 0 mismatches.

SAFETY#

  • Lazy. Importing orthonym starts no JVM and touches no network. The JVM boots on first actual use. Nothing is ever downloaded: jars are resolved by the existing _find_opsin_jar / _find_centres_jar helpers, which glob the vendored jars at PROJECT_ROOT (OPSIN 2.9.0 — the version eval/goals.json records in its baseline provenance — and centres 1.2.1).

  • Fallback, never failure. Every entry point returns a sentinel meaning “I could not do this; use your subprocess path” when jpype is absent, the jars are missing, or the JVM will not start. A missing JVM must never hard-fail a name (centres_bridge).

  • fork-safe. A JVM does not survive fork, and eval/harness.py uses mp.Pool (processes). A child that inherited a parent’s JVM would see isJVMStarted == True while the JVM’s threads no longer exist — and a JNI call into that is liable to crash the worker outright rather than raise something catchable. So we record the pid that actually called startJVM and refuse to touch a JVM started by any other pid. In the normal flow this never triggers (laziness means the parent starts no JVM and each worker starts its own); it exists so that ordering cannot become a segfault.

  • Heap. -Xmx defaults to 512 MB (verified sufficient for OPSIN + CDK), overridable via ORTHONYM_JVM_XMX. This matters because the worker model is processes: the reference snippet’s -Xmx4096M times 12 workers would reserve 48 GB.

  • ORTHONYM_DISABLE_JPYPE=1 forces every caller back onto the subprocess path — the escape hatch for A/B measurement and for reproducing a subprocess-only result.

orthonym.jvm_bridge.opsin_available()#

True if the in-process OPSIN path can serve calls.

orthonym.jvm_bridge.centres_available()#

True if the in-process centres path can serve calls.

orthonym.jvm_bridge.opsin_stdout(name, allow_radicals, jar_path=None)#

Memoising front of:func:_opsin_stdout_uncached (Lever I, 2026-09-12): within one naming scope the same (name, allow_radicals, jar_path) is parsed once; a served result is a pure function of the name, so the memo is exact. Unserved results are never cached, and nothing is cached outside a scope or in ORTHONYM_MEMO=verify mode.

orthonym.jvm_bridge.opsin_extended_smiles(name, jar_path=None)#

Exactly what java -jar opsin -o extendedsmi writes for ONE name.

Returns (extended_smiles, True) on success — the "<smiles> |$_AV:...$|" line with per-atom locant annotations — (REJECTED, True) when OPSIN parsed the request and definitively rejected the name (the CLI would print an empty line, so there is nothing a subprocess could add), or (None, False) only when the in-process path cannot serve this call (jpype/jar absent, wrong jar version, embedded newline, or a Java-side error), so the caller MUST fall back to its subprocess path. Never raises. Mirrors opsin_stdout but for the extended-SMILES output mode; used by the stereo-locant re-anchor (validation.opsin_roundtrip.opsin_atom_locant_map).

orthonym.jvm_bridge.centres_stdout(argv)#

Run LabelCip.main(argv) in-process; return its captured stdout.

argv is exactly what would follow java -jar centres-cli.jar (e.g. ["-i", "smi", "/tmp/xxx.smi"]), so this drives the identical code path with the identical arguments — the output is byte-identical by construction.

Returns None when the in-process path is unavailable or Java raised, meaning the caller must fall back to its subprocess path. Never raises.

System.out is a JVM-global, so the redirect is held under _LOCK and restored in a finally on every path.