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:interactiveSmilesOutputcallsnts.parseChemicalName(name, cfg)and writesresult.getSmiles(nothing at all when it is null) followed by a newline.generateOpsinConfigObjectFromCmdsets all five config flags explicitly fromcmd.hasOption(...), so with no flags every one isfalseand with-ronlyallowRadicalsistrue. We set all five explicitly rather than trustingNameToStructureConfig’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.LabelCipexposes onlymain(String)(verified withjavap); there is no programmatic API to call, and reimplementing its logic would risk changing labels. So we invokeLabelCip.mainwith the identical argv and the identical temp file, capturingSystem.outinto aByteArrayOutputStream. Verified withjavap -c:LabelCipcontains zero ``System.exit`` calls, so callingmainin-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
orthonymstarts 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_jarhelpers, which glob the vendored jars at PROJECT_ROOT (OPSIN 2.9.0 — the versioneval/goals.jsonrecords 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, andeval/harness.pyusesmp.Pool(processes). A child that inherited a parent’s JVM would seeisJVMStarted == Truewhile 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 calledstartJVMand 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.
-Xmxdefaults to 512 MB (verified sufficient for OPSIN + CDK), overridable viaORTHONYM_JVM_XMX. This matters because the worker model is processes: the reference snippet’s-Xmx4096Mtimes 12 workers would reserve 48 GB.ORTHONYM_DISABLE_JPYPE=1forces 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 extendedsmiwrites 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. Mirrorsopsin_stdoutbut 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.argvis exactly what would followjava -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.outis a JVM-global, so the redirect is held under_LOCKand restored in afinallyon every path.