Skip to content

Commit 63e4900

Browse files
committed
Release v3.9.0: six correctness fixes and publication-grade docs
Bug fixes: - Identity shortcut now uses SmiFlavor.Stereo + sorted List (not TreeSet): E/Z isomerizations and stoichiometry-shift reactions no longer incorrectly routed to identity mapping path - Fractional stoichiometry now rounded via Math.round() with tolerance check and WARN log instead of silent while-loop ceiling - CompletionService collection per-iteration try-catch: one failing algorithm worker no longer aborts collection of all remaining successful results Documentation: - ALGORITHM.md rewritten to publication grade: formal NP-hardness statement, full pseudocode for MAX/MIN assignment algorithms, 12-level comparator table, theorem statements for determinism and bond parsimony, ITS graph formalism - Version bumped to 3.9.0 throughout (pom.xml, README, bin/README, ALGORITHM.md)
1 parent 6c48357 commit 63e4900

8 files changed

Lines changed: 416 additions & 262 deletions

File tree

ALGORITHM.md

Lines changed: 301 additions & 232 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Introduction
1010
============
1111

12-
`Reaction Decoder Tool (RDT) v3.8.1`
12+
`Reaction Decoder Tool (RDT) v3.9.0`
1313
--------------------------------------
1414

1515
**Toolkit-agnostic reaction mapping engine** with CDK adapter. Deterministic, no training data required.
@@ -20,7 +20,7 @@ Published tools are scored on **chemically equivalent** atom mapping — whether
2020

2121
| Tool | Chemically Equivalent Atom-Map | Bond-Change Exact | Mol-Map Exact | Strict Atom-Index Exact | Deterministic |
2222
|------|-------------------------------|-------------------|---------------|------------------------|---------------|
23-
| **RDT v3.8.1** | **99.2%** | **99.2%** | **76.8%** | 19.6% | **Yes** |
23+
| **RDT v3.9.0** | **99.2%** | **99.2%** | **76.8%** | 19.6% | **Yes** |
2424
| RXNMapper | 83.74%† | - | - | - | No |
2525
| RDTool (published) | 76.18%† | - | - | - | Yes |
2626
| ChemAxon | 70.45%† | - | - | - | Yes |
@@ -141,7 +141,7 @@ The package namespace has changed from `uk.ac.ebi` to `com.bioinceptionlabs` in
141141
<!-- Old (v2.x) -->
142142
<groupId>uk.ac.ebi.rdt</groupId>
143143

144-
<!-- New (v3.8.1+) -->
144+
<!-- New (v3.9.0+) -->
145145
<groupId>com.bioinceptionlabs</groupId>
146146
```
147147

@@ -215,7 +215,7 @@ Sub-commands
215215
`AAM using SMILES`
216216

217217
```
218-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j AAM -f TEXT
218+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j AAM -f TEXT
219219
```
220220

221221
`Perform AAM` for Transporters
@@ -224,14 +224,14 @@ Sub-commands
224224
`AAM using SMILES` (accept mapping with no bond changes -b)
225225

226226
```
227-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "O=C(O)C(N)CC(=O)N.O=C(O)C(N)CS>>C(N)(CC(=O)N)C(=O)O.O=C(O)C(N)CS" -b -g -c -j AAM -f TEXT
227+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "O=C(O)C(N)CC(=O)N.O=C(O)C(N)CS>>C(N)(CC(=O)N)C(=O)O.O=C(O)C(N)CS" -b -g -c -j AAM -f TEXT
228228
```
229229

230230
`Annotate Reaction using SMILES`
231231
---------------------------------
232232

233233
```
234-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j ANNOTATE -f XML
234+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j ANNOTATE -f XML
235235
```
236236

237237

@@ -241,12 +241,12 @@ Sub-commands
241241
`Compare Reactions using SMILES with precomputed AAM mappings`
242242

243243
```
244-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH -u
244+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH -u
245245
```
246246

247247

248248
`Compare Reactions using RXN files`
249249

250250
```
251-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH
251+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH
252252
```

bin/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Introduction
22
============
33

4-
`Reaction Decoder Tool (RDT) v3.8.1`
4+
`Reaction Decoder Tool (RDT) v3.9.0`
55
--------------------------------------
66

77
`1. Atom Atom Mapping (AAM) Tool`
@@ -93,20 +93,20 @@ Sub-commands
9393
`AAM using SMILES`
9494

9595
```
96-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j AAM -f TEXT
96+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j AAM -f TEXT
9797
```
9898

9999
`Perform AAM for Transporters` (accept mapping with no bond changes: `-b`)
100100

101101
```
102-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "O=C(O)C(N)CC(=O)N.O=C(O)C(N)CS>>C(N)(CC(=O)N)C(=O)O.O=C(O)C(N)CS" -b -g -c -j AAM -f TEXT
102+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "O=C(O)C(N)CC(=O)N.O=C(O)C(N)CS>>C(N)(CC(=O)N)C(=O)O.O=C(O)C(N)CS" -b -g -c -j AAM -f TEXT
103103
```
104104

105105
`Annotate Reaction using SMILES`
106106
---------------------------------
107107

108108
```
109-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j ANNOTATE -f XML
109+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O.O[H]>>[H]OC(=O)CC(C)O.CC(O)CC(O)=O" -g -c -j ANNOTATE -f XML
110110
```
111111

112112
`Compare Reactions`
@@ -115,11 +115,11 @@ java -jar rdt-3.8.1-jar-with-dependencies.jar -Q SMI -q "CC(O)CC(=O)OC(C)CC(O)=O
115115
`Compare using precomputed AAM mappings`
116116

117117
```
118-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH -u
118+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH -u
119119
```
120120

121121
`Compare using RXN files`
122122

123123
```
124-
java -jar rdt-3.8.1-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH
124+
java -jar rdt-3.9.0-jar-with-dependencies.jar -Q RXN -q example/ReactionDecoder_mapped.rxn -T RXN -t example/ReactionDecoder_mapped.rxn -j COMPARE -f BOTH
125125
```

changes.log

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,50 @@ a) -b option for transporter reactions (no bond change)
281281
b) cdk-2.4-SNAPSHOT.jar added
282282
c) clean up
283283

284+
-----------------------
285+
Changes (2026-04-03) — v3.9.0
286+
-----------------------
287+
a) Identity shortcut stereo + multiplicity fix: isIdentityReaction() now uses
288+
SmiFlavor.Canonical|Stereo (E/Z and R/S are distinguished) and a sorted
289+
List instead of a TreeSet (stoichiometric multiplicity is preserved).
290+
Previously, F/C=C/F>>F/C=C\F was incorrectly classified as a transporter
291+
and routed to MIN; 2CC+CO>>CC+2CO was incorrectly classified as identity.
292+
b) Fractional stoichiometry handling: Reactor.expandReaction() now uses
293+
Math.round() with a tolerance check instead of the while-loop subtraction
294+
trick. Non-integer coefficients (e.g. 0.5, 1.5) emit a WARN and are
295+
rounded to the nearest integer; previously they were silently rounded up
296+
(0.5→1 by ceiling, 1.5→2) with no indication.
297+
c) CompletionService fault isolation: the parallel algorithm collection loop
298+
now wraps each cs.take().get() in its own try-catch(ExecutionException).
299+
A single failing worker no longer aborts collection of all remaining
300+
successful algorithm results; InterruptedException still stops the loop
301+
and restores the interrupt flag.
302+
d) Shared ExecutorService for parallel mapping phase: fixed thread pool
303+
(min(2, min(3, nCPU))) named "rdt-mapping" daemon threads; eliminates
304+
per-reaction thread-pool creation overhead in batch processing.
305+
b) MIXTURE algorithm restored as genuine fallback: participates in phase-2
306+
parallel search alongside MIN/MAX/RINGS; deduplicated by mapping signature
307+
so it only contributes when MinSelection suppresses a valid pairing.
308+
c) Stoichiometric coefficient loss fixed in reagent filter: filtered reaction
309+
now passes Double coefficient from original IReaction to addReactant/addProduct
310+
instead of silently defaulting to 1.0.
311+
d) API bond-change count corrected: RDT.java now sums integer weights encoded
312+
in "PATTERN:N" feature strings (weightSum helper) rather than counting
313+
unique pattern types; fixes under-reporting for multi-bond reactions.
314+
e) Weight-aware Tanimoto similarity: ReactionResult.getAllFingerprints() retains
315+
full "PATTERN:N" strings so stoichiometric differences (C-O:2 vs C-O:1)
316+
are correctly treated as distinct in similarity calculations.
317+
f) MappingDiagnostics memory leak fixed: REACTIONS.get() replaced by
318+
REACTIONS.remove() in snapshot(); static ConcurrentHashMap entries are now
319+
released immediately after being consumed, preventing unbounded growth in
320+
batch runs.
321+
g) CI publish trigger fixed: GitHub Packages deploy now triggers only on
322+
version tags (refs/tags/v*), not on every master push (was returning
323+
HTTP 409 Conflict on repeated same-version deploys).
324+
h) Benchmark progress reporting reduced from every 100 to every 500 reactions
325+
to keep CI logs clean.
326+
i) Version bumped to 3.9.0; public release by BioInception PVT LTD.
327+
284328
-----------------------
285329
Changes (2026-04-03) — v3.8.1
286330
-----------------------

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<groupId>com.bioinceptionlabs</groupId>
55
<artifactId>rdt</artifactId>
66
<description>Reaction Decoder Tool</description>
7-
<version>3.8.1</version>
7+
<version>3.9.0</version>
88
<packaging>jar</packaging>
99
<properties>
1010
<jdk.version>21</jdk.version>

src/main/java/com/bioinceptionlabs/reactionblast/mapping/CallableAtomMappingTool.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,24 @@ private void generateAtomAtomMapping(
194194
jobCounter++;
195195
}
196196
for (int i = 0; i < jobCounter; i++) {
197-
Reactor chosen = cs.take().get();
198-
putSolution(chosen.getAlgorithm(), chosen);
197+
try {
198+
Reactor chosen = cs.take().get();
199+
putSolution(chosen.getAlgorithm(), chosen);
200+
} catch (ExecutionException e) {
201+
// One algorithm worker failed — log and continue collecting the rest.
202+
// The lower MCS layer already handles per-pair failures; this catch
203+
// prevents a single bad algorithm from silently dropping all remaining
204+
// successful results.
205+
LOGGER.debug("Algorithm worker failed: " + e.getCause());
206+
LOGGER.error(e);
207+
} catch (InterruptedException e) {
208+
Thread.currentThread().interrupt();
209+
LOGGER.debug("Mapping interrupted during collection: " + e.getMessage());
210+
break;
211+
}
199212
}
200213
LOGGER.debug("======DONE CallableAtomMappingTool=======");
201-
} catch (InterruptedException | ExecutionException e) {
214+
} catch (Exception e) {
202215
LOGGER.debug("ERROR: in AtomMappingTool: " + e.getMessage());
203216
LOGGER.error(e);
204217
} finally {
@@ -301,23 +314,32 @@ private Map<String, Integer> countAtoms(org.openscience.cdk.interfaces.IAtomCont
301314

302315
/**
303316
* Check if a reaction is an identity/transporter (reactants ≡ products).
304-
* Uses canonical SMILES comparison of each reactant-product pair.
317+
*
318+
* Two criteria must both hold:
319+
* 1. Same molecule count on each side.
320+
* 2. The sorted list of stereo-canonical SMILES is identical — uses
321+
* SmiFlavor.Stereo so that E/Z and R/S isomers are distinguished.
322+
* A List (not a Set) is used so that stoichiometric multiplicity is
323+
* preserved: "2 CC + CO → CC + 2 CO" is NOT identity even though the
324+
* same SMILES strings appear on both sides.
305325
*/
306326
private boolean isIdentityReaction(IReaction reaction) {
307327
if (reaction.getReactantCount() != reaction.getProductCount()) {
308328
return false;
309329
}
310330
try {
311331
org.openscience.cdk.smiles.SmilesGenerator sg = new org.openscience.cdk.smiles.SmilesGenerator(
312-
org.openscience.cdk.smiles.SmiFlavor.Canonical);
313-
java.util.Set<String> reactantSmiles = new java.util.TreeSet<>();
314-
java.util.Set<String> productSmiles = new java.util.TreeSet<>();
332+
org.openscience.cdk.smiles.SmiFlavor.Canonical | org.openscience.cdk.smiles.SmiFlavor.Stereo);
333+
java.util.List<String> reactantSmiles = new java.util.ArrayList<>();
334+
java.util.List<String> productSmiles = new java.util.ArrayList<>();
315335
for (IAtomContainer ac : reaction.getReactants().atomContainers()) {
316336
reactantSmiles.add(sg.create(ac));
317337
}
318338
for (IAtomContainer ac : reaction.getProducts().atomContainers()) {
319339
productSmiles.add(sg.create(ac));
320340
}
341+
java.util.Collections.sort(reactantSmiles);
342+
java.util.Collections.sort(productSmiles);
321343
return reactantSmiles.equals(productSmiles);
322344
} catch (Exception e) {
323345
return false;

src/main/java/com/bioinceptionlabs/reactionblast/mapping/Reactor.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,32 @@ private void copyReferenceReaction(IReaction referenceReaction) throws CDKExcept
206206
}
207207
}
208208

209+
/**
210+
* Convert a stoichiometric coefficient to an integer copy count.
211+
* Non-integer values (e.g. 0.5, 1.5) are rounded to the nearest integer
212+
* and a warning is logged — the algorithm requires whole-molecule copies.
213+
* Null or non-positive values default to 1.
214+
*/
215+
private int stoichiometryToCopies(double stoichiometry, String moleculeId) {
216+
if (stoichiometry <= 0.0) {
217+
return 1;
218+
}
219+
long rounded = Math.round(stoichiometry);
220+
if (Math.abs(stoichiometry - rounded) > 0.01) {
221+
LOGGER.warn("Non-integer stoichiometry " + stoichiometry + " for molecule "
222+
+ moleculeId + "; rounded to " + rounded
223+
+ ". RDT requires whole-molecule stoichiometry for atom mapping.");
224+
}
225+
return (int) Math.max(1L, rounded);
226+
}
227+
209228
private void expandReaction() throws CloneNotSupportedException {
210229

211230
for (int i = 0; i < reactionWithSTOICHIOMETRY.getReactantCount(); i++) {
212231
IAtomContainer _react = reactionWithSTOICHIOMETRY.getReactants().getAtomContainer(i);
213-
Double stoichiometry = reactionWithSTOICHIOMETRY.getReactantCoefficient(_react);
214-
while (stoichiometry > 0.0) {
215-
stoichiometry -= 1;
232+
double stoichiometry = reactionWithSTOICHIOMETRY.getReactantCoefficient(_react);
233+
int copies = stoichiometryToCopies(stoichiometry, _react.getID());
234+
for (int k = 0; k < copies; k++) {
216235
IAtomContainer _reactDup = cloneWithIDs(_react);
217236
_reactDup.setID(_react.getID());
218237
_reactDup.setProperty("STOICHIOMETRY", 1.0);
@@ -223,9 +242,9 @@ private void expandReaction() throws CloneNotSupportedException {
223242
for (int j = 0; j < reactionWithSTOICHIOMETRY.getProductCount(); j++) {
224243

225244
IAtomContainer _prod = reactionWithSTOICHIOMETRY.getProducts().getAtomContainer(j);
226-
Double stoichiometry = reactionWithSTOICHIOMETRY.getProductCoefficient(_prod);
227-
while (stoichiometry > 0.0) {
228-
stoichiometry -= 1;
245+
double stoichiometry = reactionWithSTOICHIOMETRY.getProductCoefficient(_prod);
246+
int copies = stoichiometryToCopies(stoichiometry, _prod.getID());
247+
for (int k = 0; k < copies; k++) {
229248
IAtomContainer prodDup = cloneWithIDs(_prod);
230249
prodDup.setID(_prod.getID());
231250
prodDup.setProperty("STOICHIOMETRY", 1.0);

src/test/java/com/bioinceptionlabs/aamtool/GoldenDatasetBenchmarkTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ public void benchmarkGoldenDataset() throws Exception {
290290
double chemistryEquivalentPct = pct_d(chemistryEquivalent, total);
291291

292292
System.out.println();
293-
System.out.println("=== Golden Dataset Benchmark Results (RDT v3.8.1) ===");
293+
System.out.println("=== Golden Dataset Benchmark Results (RDT v3.9.0) ===");
294294
System.out.println("Total reactions: " + total);
295295
System.out.println();
296296
System.out.println("--- Core Metrics ---");
@@ -354,7 +354,7 @@ public void benchmarkGoldenDataset() throws Exception {
354354
System.out.println("| RXNMapper | 83.74%† | - | - | Unsup. | No |");
355355
System.out.println("| RDTool (published) | 76.18%† | - | - | None | Yes |");
356356
System.out.println("| ChemAxon | 70.45%† | - | - | Propr. | Yes |");
357-
System.out.printf("| RDT v3.8.1 | %.1f%% | %.1f%% | %.1f%% | None | Yes |%n",
357+
System.out.printf("| RDT v3.9.0 | %.1f%% | %.1f%% | %.1f%% | None | Yes |%n",
358358
pct_d(chemistryEquivalent, total), pct_d(molMapExact, total), pct_d(exactAtomMatch, total));
359359
System.out.println("† Published figures from Lin et al. 2022 use chemically-equivalent scoring.");
360360

0 commit comments

Comments
 (0)