@@ -220,6 +220,8 @@ public void firePropertyChange(String propertyName, Object oldValue, Object newV
220220public static final String BondTransitionCounter = "bondTransitionCounter" ;
221221public static final String CspReactantBond = "cspReactantBond" ;
222222
223+ public static final String NoStateSpecified = "NoStateSpecified" ; // used when we have a binding transition but no state specified for the transitioning site
224+
223225// --------------------------------------------------------------------------------------------------------
224226public void analizeReaction (Map <String , Object > analysisResults ) {
225227 List <ReactantPattern > rpList = reactionRule .getReactantPatterns ();
@@ -347,7 +349,7 @@ public void analizeReaction(Map<String, Object> analysisResults) {
347349
348350 String stateReactant ;
349351 if (cspReactant == null ) {
350- stateReactant = "ERROR" ;
352+ stateReactant = NoStateSpecified ;
351353 } else if (cspReactant .isAny ()) {
352354 stateReactant = ANY_STATE_STRING ;
353355 } else {
@@ -424,14 +426,10 @@ public TransitionCondition getTransitionCondition(Map<String, Object> analysisRe
424426 for (MolecularComponentPattern mcpReactant : mtpReactant .getComponentPatternList ()) {
425427 ComponentStatePattern cspReactant = mcpReactant .getComponentStatePattern ();
426428 BondType bondTypeReactant = mcpReactant .getBondType ();
429+
427430 if (cspReactant == null ) {
428- return null ; // all sites must have at least one state
429- }
430- // if(!cspReactant.isAny() && BondType.Possible != bondTypeReactant) {
431- // // this is the transition site, bond must be "Possible"
432- // return null;
433- // }
434- if (!cspReactant .isAny ()) {
431+ numAnyStates ++; // no state for this site, so counts as any (no transition)
432+ } else if (!cspReactant .isAny ()) {
435433 ;
436434 } else {
437435 numAnyStates ++;
@@ -491,9 +489,6 @@ public enum TransitionCondition { // everywhere internally in vcell we use RBM b
491489 }
492490 for (MolecularComponentPattern mcpReactant : mtpReactant .getComponentPatternList ()) {
493491 ComponentStatePattern cspReactant = mcpReactant .getComponentStatePattern ();
494- if (cspReactant == null ) {
495- return null ; // all sites must have at least one state
496- }
497492 BondType bondTypeReactant = mcpReactant .getBondType ();
498493 if (BondType .Specified == bondTypeReactant ) { // we must have exactly one Specified bond, all the others must be Possible
499494 numSpecifiedBonds [i ]++; // the condition binding site has bond type "Exists"
@@ -502,7 +497,7 @@ public enum TransitionCondition { // everywhere internally in vcell we use RBM b
502497 return null ; // all other bonds in the reactant must be of type "Possible"
503498 }
504499
505- if (!cspReactant .isAny ()) { // we also need to reack explicit states
500+ if (cspReactant != null && !cspReactant .isAny ()) { // we also need to reack explicit states
506501 numExplicitStates [i ]++;
507502 totalExplicitStates ++;
508503 mcpExplicitStates [i ] = mcpReactant ;
@@ -603,7 +598,7 @@ private boolean isAllostericReaction(Map<String, Object> analysisResults) {
603598 }
604599 for (MolecularComponentPattern mcp : mcpReactantList ) {
605600 if (mcp .getComponentStatePattern () == null ) {
606- return false ; // all sites must have at least one state
601+ continue ; // if no state defined for this site, counts as any state, so we skip it
607602 }
608603 if (!mcp .getComponentStatePattern ().isAny ()) {
609604 explicitReactantStatesSet .add (mcp .getComponentStatePattern ().getComponentStateDefinition ());
@@ -613,7 +608,7 @@ private boolean isAllostericReaction(Map<String, Object> analysisResults) {
613608 int matchedStates = 0 ;
614609 int unmatchedStates = 0 ;
615610 for (MolecularComponentPattern mcp : mcpProductList ) {
616- if (!mcp .getComponentStatePattern ().isAny ()) {
611+ if (mcp . getComponentStatePattern () != null && !mcp .getComponentStatePattern ().isAny ()) {
617612 ComponentStateDefinition csdProductExplicit = mcp .getComponentStatePattern ().getComponentStateDefinition ();
618613 if (explicitReactantStatesSet .contains (csdProductExplicit )) {
619614 matchedStates ++;
@@ -669,8 +664,8 @@ private boolean isBindingReaction(Map<String, Object> analysisResults) {
669664 for (MolecularComponentPattern mcp : mtp .getComponentPatternList ()) {
670665 BondType bt = mcp .getBondType ();
671666 ComponentStatePattern csp = mcp .getComponentStatePattern ();
672- if (csp == null || ( !csp .isAny () && BondType .Possible == bt ) ) {
673- // all the sites not binding must be in Any state
667+ if (( csp != null && !csp .isAny ()) && BondType .Possible == bt ) {
668+ // all the sites not binding must be in Any state or must have no state defined
674669 return false ;
675670 }
676671 }
@@ -754,7 +749,9 @@ private void writeTransitionData(StringBuilder sb, Subtype subtype, Map<String,
754749 for (MolecularComponentPattern mcpCandidate : mtpConditionReactant .getComponentPatternList ()) {
755750 if (BondType .Specified == mcpCandidate .getBondType ()) {
756751 mcpConditionReactant = mcpCandidate ; // found the bond condition site, it's the one with bond type "Specified"
757- if (!mcpConditionReactant .getComponentStatePattern ().isAny ()) {
752+ if (mcpConditionReactant .getComponentStatePattern () == null ) {
753+ stateConditionReactant = SiteAttributesSpec .StateZero ;
754+ } else if (!mcpConditionReactant .getComponentStatePattern ().isAny ()) {
758755 stateConditionReactant = mcpConditionReactant .getComponentStatePattern ().getComponentStateDefinition ().getName ();
759756 }
760757 break ;
@@ -799,7 +796,7 @@ private void writeAllostericData(StringBuilder sb, Subtype subtype, Map<String,
799796 if (mcpTransitionReactant == mcp ) {
800797 continue ; // found the allosteric site index
801798 }
802- if (mcp .getComponentStatePattern ().isAny ()) {
799+ if (mcp .getComponentStatePattern () == null || mcp . getComponentStatePattern () .isAny ()) {
803800 continue ; // the allosteric state must be explicit
804801 }
805802 mcpAllostericReactant = mcp ;
@@ -835,7 +832,13 @@ private void writeBindingData(StringBuilder sb, Subtype subtype, Map<String, Obj
835832 MolecularComponentPattern mcpReactantOne = (MolecularComponentPattern )analysisResults .get (McpReactantBond + "1" );
836833 MolecularComponentPattern mcpReactantTwo = (MolecularComponentPattern )analysisResults .get (McpReactantBond + "2" );
837834 String stateReactantOne = (String )analysisResults .get (CspReactantBond + "1" );
835+ if (stateReactantOne .equals (NoStateSpecified )) {
836+ stateReactantOne = SiteAttributesSpec .StateZero ;
837+ }
838838 String stateReactantTwo = (String )analysisResults .get (CspReactantBond + "2" );
839+ if (stateReactantTwo .equals (NoStateSpecified )) {
840+ stateReactantTwo = SiteAttributesSpec .StateZero ;
841+ }
839842 if (mtpReactantOne == null || mtpReactantTwo == null || mcpReactantOne == null || mcpReactantTwo == null ) {
840843 throw new RuntimeException ("writeBindingData() error: something is wrong" );
841844 }
@@ -1215,6 +1218,13 @@ public void gatherIssues(IssueContext issueContext, List<Issue> issueList, React
12151218 siteAttributesMapTwo = scs .getSiteAttributesMap ();
12161219 }
12171220 }
1221+ if (siteAttributesMapOne == null ) {
1222+ // this may happen if the reaction uses a Molecule for which no Species was yet defined
1223+ String msg = "Could not match reactant '" + mtOursOne .getName () + "' to any Species in the model." ;
1224+ String tip = "Verify that a Species has been defined for the molecule type '" + mtOursOne .getName () + "'." ;
1225+ issueList .add (new Issue (r , issueContext , IssueCategory .Identifiers , msg , tip , Issue .Severity .ERROR ));
1226+ return ;
1227+ }
12181228 SiteAttributesSpec sasOne = null ;
12191229 SiteAttributesSpec sasTwo = null ;
12201230 for (Map .Entry <MolecularComponentPattern , SiteAttributesSpec > entry : siteAttributesMapOne .entrySet ()) {
@@ -1234,8 +1244,14 @@ public void gatherIssues(IssueContext issueContext, List<Issue> issueList, React
12341244 // happened when the reaction was between 2 molecules of the same type A + A -> A.A
12351245 // the temp fixwas: we check for non-null siteAttributesMapTwo
12361246 // as from feb 24, 2025 this was fixed and siteAttributesMapTwo should never be null
1237- if (siteAttributesMapTwo == null ) {
1238- throw new RuntimeException ("Unexpected null value for siteAttributesMapTwo" );
1247+ // feb 18 2026 - it actually may still be null if no Species is defined for the molecule type,
1248+ // // so we keep the check and fire an issue if it's nevertheless null
1249+ if (siteAttributesMapTwo == null ) {
1250+ // this may happen if the reaction uses a Molecule for which no Species was yet defined
1251+ String msg = "Could not match reactant '" + mtOursTwo .getName () + "' to any Species in the model." ;
1252+ String tip = "Verify that a Species has been defined for the molecule type '" + mtOursTwo .getName () + "'." ;
1253+ issueList .add (new Issue (r , issueContext , IssueCategory .Identifiers , msg , tip , Issue .Severity .ERROR ));
1254+ return ;
12391255 }
12401256 if (sasOne != null && sasTwo != null ) {
12411257 for (Map .Entry <MolecularComponentPattern , SiteAttributesSpec > entry : siteAttributesMapTwo .entrySet ()) {
0 commit comments