4141import org .openscience .cdk .interfaces .IMapping ;
4242import org .openscience .cdk .interfaces .IReaction ;
4343import org .openscience .cdk .smiles .SmilesGenerator ;
44- import static org .openscience .cdk .smiles .SmilesGenerator .generic ;
4544import org .openscience .cdk .tools .ILoggingTool ;
4645import static org .openscience .cdk .tools .LoggingToolFactory .createLoggingTool ;
4746import static org .openscience .cdk .tools .manipulator .AtomContainerSetManipulator .getAllAtomContainers ;
@@ -270,84 +269,59 @@ && getAtomCount(reaction.getReactants())
270269 generate2D ,
271270 generate3D );
272271 LOGGER .debug ("is solution: " + algorithm + " selected: " + selected );
272+
273+ // Early exit: if this solution has minimal bond changes (≤2)
274+ // and zero fragment changes, it's likely optimal — skip remaining algorithms
275+ if (selected && this .selectedMapping != null
276+ && this .selectedMapping .getTotalBondChanges () <= 2
277+ && this .selectedMapping .getTotalFragmentChanges () == 0 ) {
278+ LOGGER .debug ("Early exit: optimal mapping found by " + algorithm );
279+ break ;
280+ }
273281 }
274282 } catch (Exception e ) {
275283 LOGGER .error (SEVERE , "Bond change calculation error" , e );
276284 throw new Exception (NEW_LINE + "ERROR: Unable to calculate bond changes: " + e .getMessage (), e );
277285 }
286+ if (this .selectedMapping != null ) {
287+ LOGGER .info ("Selected algorithm: " + this .selectedMapping .getAlgorithmID ().description ()
288+ + " (bonds=" + this .selectedMapping .getTotalBondChanges ()
289+ + ", energy=" + this .selectedMapping .getBondEnergySum ()
290+ + ", fragments=" + this .selectedMapping .getTotalFragmentChanges () + ")" );
291+ }
278292 LOGGER .debug ("=====DONE REACTION MECH TOOL=====" );
279293 }
280294 }
281295
296+ /**
297+ * Check if a reaction is balanced (same heavy atom counts on both sides).
298+ * Hydrogens are excluded since they are often implicit.
299+ */
282300 private boolean isBalanced (IReaction r ) {
283-
284- Map <String , Integer > atomUniqueCounter1 = new TreeMap <>();
285- Map <String , Integer > atomUniqueCounter2 = new TreeMap <>();
286-
287- int leftHandAtomCount = 0 ;
288- for (IAtomContainer q : r .getReactants ().atomContainers ()) {
289- for (IAtom a : q .atoms ()) {
290- if (a .getSymbol ().equals ("H" )) {
291- continue ;
292- }
293- if (!atomUniqueCounter1 .containsKey (a .getSymbol ())) {
294- atomUniqueCounter1 .put (a .getSymbol (), 1 );
295- } else {
296- int counter = atomUniqueCounter1 .get (a .getSymbol ()) + 1 ;
297- atomUniqueCounter1 .put (a .getSymbol (), counter );
298- }
299- leftHandAtomCount ++;
300- }
301- if (DEBUG ) {
302- try {
303- LOGGER .debug ("Q=mol " + generic ().create (q ));
304- } catch (CDKException ex ) {
305- LOGGER .error (SEVERE , null , ex );
306- }
307- }
301+ Map <String , Integer > reactantAtoms = countHeavyAtoms (r .getReactants ());
302+ Map <String , Integer > productAtoms = countHeavyAtoms (r .getProducts ());
303+
304+ if (!reactantAtoms .equals (productAtoms )) {
305+ LOGGER .warn ("Number of atom(s) on the Left side "
306+ + reactantAtoms .values ().stream ().mapToInt (Integer ::intValue ).sum ()
307+ + " =/= Number of atom(s) on the Right side "
308+ + productAtoms .values ().stream ().mapToInt (Integer ::intValue ).sum ());
309+ LOGGER .warn (reactantAtoms + " =/= " + productAtoms );
310+ return false ;
308311 }
312+ return true ;
313+ }
309314
310- int rightHandAtomCount = 0 ;
311- for (IAtomContainer t : r .getProducts ().atomContainers ()) {
312- for (IAtom b : t .atoms ()) {
313- if (b .getSymbol ().equals ("H" )) {
314- continue ;
315- }
316- if (!atomUniqueCounter2 .containsKey (b .getSymbol ())) {
317- atomUniqueCounter2 .put (b .getSymbol (), 1 );
318- } else {
319- int counter = atomUniqueCounter2 .get (b .getSymbol ()) + 1 ;
320- atomUniqueCounter2 .put (b .getSymbol (), counter );
321- }
322- rightHandAtomCount ++;
323- }
324- if (DEBUG ) {
325- try {
326- LOGGER .debug ("T=mol " + generic ().create (t ));
327- } catch (CDKException ex ) {
328- LOGGER .error (SEVERE , null , ex );
315+ private Map <String , Integer > countHeavyAtoms (IAtomContainerSet containers ) {
316+ Map <String , Integer > counts = new TreeMap <>();
317+ for (IAtomContainer mol : containers .atomContainers ()) {
318+ for (IAtom a : mol .atoms ()) {
319+ if (!"H" .equals (a .getSymbol ())) {
320+ counts .merge (a .getSymbol (), 1 , Integer ::sum );
329321 }
330322 }
331323 }
332-
333- LOGGER .debug ("atomUniqueCounter1 " + leftHandAtomCount );
334- LOGGER .debug ("atomUniqueCounter2 " + rightHandAtomCount );
335-
336- if (leftHandAtomCount != rightHandAtomCount ) {
337- LOGGER .warn ("Number of atom(s) on the Left side " + leftHandAtomCount
338- + " =/= Number of atom(s) on the Right side " + rightHandAtomCount );
339- LOGGER .warn (atomUniqueCounter1 + " =/= " + atomUniqueCounter2 );
340- return false ;
341- } else if (!atomUniqueCounter1 .keySet ().equals (atomUniqueCounter2 .keySet ())) {
342- LOGGER .warn ("Number of atom(s) on the Left side " + leftHandAtomCount
343- + " =/= Number of atom(s) on the Right side " + rightHandAtomCount );
344- LOGGER .warn (atomUniqueCounter1 + " =/= " + atomUniqueCounter2 );
345- return false ;
346- }
347-
348- LOGGER .debug ("atomUniqueCounter1 " + atomUniqueCounter1 );
349- LOGGER .debug ("atomUniqueCounter2 " + atomUniqueCounter2 );
350- return atomUniqueCounter1 .keySet ().equals (atomUniqueCounter2 .keySet ());
324+ return counts ;
351325 }
352326
353327 private boolean isMappingSolutionAcceptable (Reactor reactor ,
@@ -356,14 +330,9 @@ private boolean isMappingSolutionAcceptable(Reactor reactor,
356330 boolean generate2D ,
357331 boolean generate3D
358332 ) throws Exception {
359- if (reactor != null && reactor .getMappingCount () > 500 && reactor .getMappingCount () < 1000 ) {
360- LOGGER .warn ("wolla...are after something big?...so many atoms to compute bond changes!" );
361- LOGGER .warn ("...Let me try..hold on your horses!" );
362- }
363- if (reactor != null && reactor .getMappingCount () > 1000 ) {
364- LOGGER .warn ("...wolla...are after something big?...!" );
365- LOGGER .warn ("...This might drive me bit crazy ... have to compute so many atoms for bond changes...!" );
366- LOGGER .warn ("...Let me try..hold on your horses!" );
333+ if (reactor != null && reactor .getMappingCount () > 500 ) {
334+ LOGGER .warn ("Large mapping: " + reactor .getMappingCount ()
335+ + " atoms — bond change computation may be slow" );
367336 }
368337 boolean chosen = false ;
369338 try {
@@ -460,9 +429,17 @@ private boolean isMappingSolutionAcceptable(Reactor reactor,
460429 return chosen ;
461430 }
462431
463- /*
464- * if bond changes are lesser than stored bond changes then update the flag or if stereo changes are lesser than
465- * stores stereo changes
432+ /**
433+ * Determines if a new mapping solution should replace the current best.
434+ *
435+ * Chemical rationale (Occam's razor for reaction mapping):
436+ * 1. Prefer fewer total bond changes (simplest mechanism)
437+ * 2. Among equal bond changes, prefer fewer fragment changes (less molecular rearrangement)
438+ * 3. Among equal fragments, prefer lower bond energy change (thermodynamic favorability)
439+ * 4. Use stereo changes and carbon bond changes as tiebreakers
440+ *
441+ * Special cases: transporters (no bond changes), identity reactions,
442+ * and reactions with only stereo changes are handled first.
466443 */
467444 private boolean isChangeFeasible (MappingSolution ms ) {
468445
0 commit comments