1919package com .bioinceptionlabs .reactionblast .tools ;
2020
2121import static java .lang .System .currentTimeMillis ;
22+ import java .util .ArrayList ;
23+ import java .util .Arrays ;
24+ import java .util .HashSet ;
2225import java .util .LinkedHashMap ;
26+ import java .util .List ;
2327import java .util .Map ;
28+ import java .util .Set ;
2429import java .util .logging .Level ;
2530
31+ import org .openscience .cdk .fingerprint .CircularFingerprinter ;
2632import org .openscience .cdk .interfaces .IAtom ;
2733import org .openscience .cdk .interfaces .IAtomContainer ;
34+ import org .openscience .cdk .interfaces .IAtomContainerSet ;
2835import org .openscience .cdk .interfaces .IReaction ;
36+ import org .openscience .cdk .similarity .Tanimoto ;
2937import org .openscience .cdk .tools .ILoggingTool ;
3038import static org .openscience .cdk .tools .LoggingToolFactory .createLoggingTool ;
3139import com .bioinceptionlabs .reactionblast .mapping .ReactionContainer .CDKReactionBuilder ;
@@ -58,6 +66,9 @@ public IReaction standardize(IReaction reaction) throws Exception {
5866 reaction .setID (reactionID );
5967 }
6068
69+ // Filter reagents/solvents before mapping (improves accuracy for multi-component reactions)
70+ reaction = filterReagents (reaction );
71+
6172 // Validate atom balance (warn but don't fail — some reactions are intentionally unbalanced)
6273 checkAtomBalance (reaction );
6374
@@ -81,7 +92,141 @@ private void checkAtomBalance(IReaction reaction) {
8192 }
8293 }
8394
84- private Map <String , Integer > countAtoms (org .openscience .cdk .interfaces .IAtomContainerSet molSet ) {
95+ /**
96+ * Filter out reagents and solvents from reactants that don't participate
97+ * in the actual bond-changing reaction. Uses Tanimoto fingerprint similarity
98+ * to identify reactant molecules that have no corresponding product.
99+ *
100+ * A reactant is classified as a reagent/solvent if:
101+ * 1. Its max Tanimoto similarity to any product is < 0.3 (no product resembles it)
102+ * 2. AND it has no atoms that appear in the product atom balance
103+ *
104+ * This is conservative — it only removes molecules that clearly don't
105+ * participate. If in doubt, the molecule is kept as a reactant.
106+ *
107+ * @param reaction the reaction to filter
108+ * @return filtered reaction with reagents moved to agents
109+ */
110+ public IReaction filterReagents (IReaction reaction ) {
111+ if (reaction .getReactantCount () <= 1 ) {
112+ return reaction ; // nothing to filter
113+ }
114+
115+ try {
116+ CircularFingerprinter fp = new CircularFingerprinter ();
117+ IAtomContainerSet products = reaction .getProducts ();
118+
119+ // Pre-compute product fingerprints
120+ List <org .openscience .cdk .fingerprint .IBitFingerprint > productFPs = new ArrayList <>();
121+ for (IAtomContainer prod : products .atomContainers ()) {
122+ try {
123+ productFPs .add (fp .getBitFingerprint (prod ));
124+ } catch (Exception e ) {
125+ productFPs .add (null );
126+ }
127+ }
128+
129+ // Collect product atom types for balance check
130+ Map <String , Integer > productAtomCounts = countAtoms (products );
131+
132+ List <IAtomContainer > keptReactants = new ArrayList <>();
133+ List <IAtomContainer > reagents = new ArrayList <>();
134+
135+ for (IAtomContainer reactant : reaction .getReactants ().atomContainers ()) {
136+ boolean isReagent = false ;
137+ try {
138+ org .openscience .cdk .fingerprint .IBitFingerprint reactantFP = fp .getBitFingerprint (reactant );
139+
140+ // Find max similarity to any product
141+ double maxSim = 0.0 ;
142+ for (org .openscience .cdk .fingerprint .IBitFingerprint prodFP : productFPs ) {
143+ if (prodFP != null ) {
144+ double sim = Tanimoto .calculate (reactantFP , prodFP );
145+ maxSim = Math .max (maxSim , sim );
146+ }
147+ }
148+
149+ // If no product resembles this reactant, it's likely a reagent
150+ if (maxSim < 0.3 && reactant .getAtomCount () > 0 ) {
151+ // Double-check: does any atom type in this molecule appear
152+ // exclusively in the reactant (i.e., removed in products)?
153+ // If so, it might still be a real reactant (leaving group)
154+ boolean hasUniqueContribution = false ;
155+ Map <String , Integer > reactantAtomCounts = new LinkedHashMap <>();
156+ for (IAtom atom : reactant .atoms ()) {
157+ reactantAtomCounts .merge (atom .getSymbol (), 1 , Integer ::sum );
158+ }
159+ // Check if this reactant contributes atoms not in products
160+ for (Map .Entry <String , Integer > entry : reactantAtomCounts .entrySet ()) {
161+ if (!productAtomCounts .containsKey (entry .getKey ())) {
162+ hasUniqueContribution = true ;
163+ break ;
164+ }
165+ }
166+
167+ // Only filter if: low similarity AND no unique atom contribution
168+ // AND molecule is small (≤ 10 heavy atoms) — large molecules
169+ // are more likely to be real reactants
170+ int heavyAtomCount = 0 ;
171+ for (IAtom atom : reactant .atoms ()) {
172+ if (!"H" .equals (atom .getSymbol ())) heavyAtomCount ++;
173+ }
174+ if (!hasUniqueContribution && heavyAtomCount <= 10 ) {
175+ isReagent = true ;
176+ LOGGER .debug ("Filtered reagent/solvent: " + reactant .getID ()
177+ + " (Tanimoto=" + String .format ("%.2f" , maxSim )
178+ + ", atoms=" + heavyAtomCount + ")" );
179+ }
180+ }
181+ } catch (Exception e ) {
182+ // If fingerprinting fails, keep the molecule as reactant
183+ LOGGER .debug ("Fingerprint failed for " + reactant .getID () + ": " + e .getMessage ());
184+ }
185+
186+ if (isReagent ) {
187+ reagents .add (reactant );
188+ } else {
189+ keptReactants .add (reactant );
190+ }
191+ }
192+
193+ // Only filter if we'd keep at least 1 reactant
194+ if (keptReactants .isEmpty () || reagents .isEmpty ()) {
195+ return reaction ; // nothing filtered or would remove all
196+ }
197+
198+ // Build filtered reaction
199+ IReaction filtered = reaction .getBuilder ().newInstance (IReaction .class );
200+ filtered .setID (reaction .getID ());
201+ filtered .setDirection (reaction .getDirection ());
202+ for (IAtomContainer r : keptReactants ) {
203+ filtered .addReactant (r );
204+ }
205+ for (IAtomContainer p : products .atomContainers ()) {
206+ filtered .addProduct (p );
207+ }
208+ for (IAtomContainer agent : reagents ) {
209+ filtered .addAgent (agent );
210+ }
211+ // Copy existing agents
212+ if (reaction .getAgents () != null ) {
213+ for (IAtomContainer agent : reaction .getAgents ().atomContainers ()) {
214+ filtered .addAgent (agent );
215+ }
216+ }
217+
218+ LOGGER .debug ("Filtered " + reagents .size () + " reagent(s) from "
219+ + reaction .getReactantCount () + " reactants → "
220+ + keptReactants .size () + " reactants" );
221+ return filtered ;
222+
223+ } catch (Exception e ) {
224+ LOGGER .debug ("Reagent filtering failed: " + e .getMessage ());
225+ return reaction ; // return unfiltered on error
226+ }
227+ }
228+
229+ private Map <String , Integer > countAtoms (IAtomContainerSet molSet ) {
85230 Map <String , Integer > counts = new LinkedHashMap <>();
86231 for (IAtomContainer mol : molSet .atomContainers ()) {
87232 for (IAtom atom : mol .atoms ()) {
0 commit comments