1 /**
2  * @cond doxygenLibsbmlInternal
3  *
4  * @file    OverDeterminedCheck.cpp
5  * @brief   Checks for over determined models.
6  * @author  Sarah Keating
7  *
8  * <!--------------------------------------------------------------------------
9  * This file is part of libSBML.  Please visit http://sbml.org for more
10  * information about SBML, and the latest version of libSBML.
11  *
12  * Copyright (C) 2020 jointly by the following organizations:
13  *     1. California Institute of Technology, Pasadena, CA, USA
14  *     2. University of Heidelberg, Heidelberg, Germany
15  *     3. University College London, London, UK
16  *
17  * Copyright (C) 2019 jointly by the following organizations:
18  *     1. California Institute of Technology, Pasadena, CA, USA
19  *     2. University of Heidelberg, Heidelberg, Germany
20  *
21  * Copyright (C) 2013-2018 jointly by the following organizations:
22  *     1. California Institute of Technology, Pasadena, CA, USA
23  *     2. EMBL European Bioinformatics Institute (EMBL-EBI), Hinxton, UK
24  *     3. University of Heidelberg, Heidelberg, Germany
25  *
26  * Copyright (C) 2009-2013 jointly by the following organizations:
27  *     1. California Institute of Technology, Pasadena, CA, USA
28  *     2. EMBL European Bioinformatics Institute (EMBL-EBI), Hinxton, UK
29  *
30  * Copyright (C) 2006-2008 by the California Institute of Technology,
31  *     Pasadena, CA, USA
32  *
33  * Copyright (C) 2002-2005 jointly by the following organizations:
34  *     1. California Institute of Technology, Pasadena, CA, USA
35  *     2. Japan Science and Technology Agency, Japan
36  *
37  * This library is free software; you can redistribute it and/or modify it
38  * under the terms of the GNU Lesser General Public License as published by
39  * the Free Software Foundation.  A copy of the license agreement is provided
40  * in the file named "LICENSE.txt" included with this software distribution
41  * and also available online as http://sbml.org/software/libsbml/license.html
42  * ---------------------------------------------------------------------- -->*/
43 
44 #include <sbml/Model.h>
45 #include <sbml/Compartment.h>
46 #include <sbml/util/List.h>
47 
48 #include <sbml/util/IdList.h>
49 #include "OverDeterminedCheck.h"
50 
51 #include <iostream>
52 #include <cstring>
53 
54 /** @cond doxygenIgnored */
55 using namespace std;
56 /** @endcond */
57 
58 LIBSBML_CPP_NAMESPACE_BEGIN
59 
60 #define SET_NAME(name,prefix,count)\
61 {\
62   std::stringstream str; str << prefix << count;\
63   name = str.str();\
64 }
65 
66 /*
67  * Creates a new Constraint with the given @p id.
68  */
OverDeterminedCheck(unsigned int id,Validator & v)69 OverDeterminedCheck::OverDeterminedCheck ( unsigned int id,
70                                                      Validator& v ) :
71   TConstraint<Model>(id, v)
72 {
73 }
74 
75 
76 /*
77  * Destroys this Constraint.
78  */
~OverDeterminedCheck()79 OverDeterminedCheck::~OverDeterminedCheck ()
80 {
81   //mEquations.clear(); // list of equation vertexes
82   //mVariables.clear(); // list of variable vertexes
83   //mGraph.clear();
84 
85   ///* these are to enable the bipartite matching without passing variables */
86   //mMatching.clear();
87   //mVarNeighInPrev.clear();
88   //mEqnNeighInPrev.clear();
89 
90   //revisited.clear();
91   //visited.clear();
92 
93 }
94 
95 
96 /*
97  * Checks that a model is not over determined
98  */
99 void
check_(const Model & m,const Model &)100 OverDeterminedCheck::check_ (const Model& m, const Model&)
101 {
102   unsigned int n;
103   unsigned int NumAlgRules = 0;
104   IdList unmatchedEqns;
105 
106   for (n = 0; n < m.getNumRules(); n++)
107   {
108     if (m.getRule(n)->isAlgebraic() && m.getRule(n)->isSetMath())
109     {
110       NumAlgRules++;
111     }
112   }
113 
114   if (NumAlgRules > 0)
115   {
116     EquationMatching* eqn = new EquationMatching();
117 
118     eqn->createGraph(m);
119 
120     /* short check - if number equations exceeds number of variables
121      * then maximal matching MUST leave one or more equations unconnected
122      */
123     if (eqn->getNumEquations() > eqn->getNumVariables())
124     {
125        logOverDetermined(m);
126     }
127     else
128     {
129       unmatchedEqns = eqn->findMatching();
130 
131       if (unmatchedEqns.size() > 0)
132       {
133         logOverDetermined(m);
134       }
135     }
136 
137     delete eqn;
138   }
139 }
140 
141 /*
142  * Logs a message about overdetermined model.
143  * As yet this only reports the problem - it doesnt really give
144  * any additional information
145  */
146 void
logOverDetermined(const Model & m)147 OverDeterminedCheck::logOverDetermined (const Model& m)
148 {
149   //msg =
150   //  "The system of equations created from an SBML model must not be "
151   //  "overdetermined. (References: L2V2 Section 4.11.5.)";
152 
153   logFailure(m);
154 }
155 
EquationMatching()156 EquationMatching::EquationMatching()
157 {
158 }
159 
~EquationMatching()160 EquationMatching::~EquationMatching ()
161 {
162   mEquations.clear(); // list of equation vertexes
163   mVariables.clear(); // list of variable vertexes
164   mGraph.clear();
165 
166   /* these are to enable the bipartite matching without passing variables */
167   mMatching.clear();
168   mVarNeighInPrev.clear();
169   mEqnNeighInPrev.clear();
170 
171   revisited.clear();
172   visited.clear();
173 
174 }
175 
176 
177 
178 /*
179  * creates equation vertexes according to the L2V2 spec 4.11.5 for every
180  * 1. a Species that has the boundaryCondition field set to false
181  * and constant field set to false and which is referenced by one or more
182  * reactant or product lists of a Reaction containing a KineticLaw
183  * 2. a Rule
184  * 3. a KineticLaw
185  */
186 void
writeEquationVertexes(const Model & m)187 EquationMatching::writeEquationVertexes(const Model& m)
188 {
189   const Reaction *r;
190   const Species* s;
191   string rule;
192   string react;
193 
194   unsigned int n, sr;
195 
196   /* a Species that has the boundaryCondition field set to false
197    * and constant field set to false and which is referenced by one or
198    * more reactant or product lists of a Reaction containing
199    * a KineticLaw
200    */
201   for (n = 0; n < m.getNumReactions(); n++)
202   {
203     if (m.getReaction(n)->isSetKineticLaw())
204     {
205       r = m.getReaction(n);
206       for (sr = 0; sr < r->getNumReactants(); sr++)
207       {
208         s = m.getSpecies(r->getReactant(sr)->getSpecies());
209         if (!s->getBoundaryCondition() && !s->getConstant())
210         {
211           if (!mEquations.contains(s->getId()))
212             mEquations.append(s->getId());
213         }
214       }
215 
216       for (sr = 0; sr < r->getNumProducts(); sr++)
217       {
218         s = m.getSpecies(r->getProduct(sr)->getSpecies());
219         if (!s->getBoundaryCondition() && !s->getConstant())
220         {
221           if (!mEquations.contains(s->getId()))
222             mEquations.append(s->getId());
223         }
224       }
225     }
226   }
227 
228   /* a Rule structure */
229   for (n = 0; n < m.getNumRules(); n++)
230   {
231     SET_NAME(rule, "rule_", n);
232     mEquations.append(rule);
233   }
234 
235   /* a Kinetic Law structure */
236   for (n = 0; n < m.getNumReactions(); n++)
237   {
238     if (m.getReaction(n)->isSetKineticLaw())
239     {
240         SET_NAME(react, "KL_", n);
241         mEquations.append(react);
242     }
243   }
244 }
245 
246 
247 /*
248  * creates variable vertexes according to the L2V2 spec 4.11.5 for
249  * (a) every Species, Compartment and Parameter structure which has the
250  * Constant field set to false; and
251  * (b) for every Reaction structure.
252  */
253 void
writeVariableVertexes(const Model & m)254 EquationMatching::writeVariableVertexes(const Model& m)
255 {
256   unsigned int n, k;
257 
258   for (n = 0; n < m.getNumCompartments(); n++)
259   {
260     if (!m.getCompartment(n)->getConstant())
261     {
262       mVariables.append(m.getCompartment(n)->getId());
263     }
264     else if (m.getLevel() == 1)
265     {
266       mVariables.append(m.getCompartment(n)->getId());
267     }
268   }
269 
270   for (n = 0; n < m.getNumSpecies(); n++)
271   {
272     if (!m.getSpecies(n)->getConstant())
273     {
274       mVariables.append(m.getSpecies(n)->getId());
275     }
276     else if (m.getLevel() == 1)
277     {
278       mVariables.append(m.getSpecies(n)->getId());
279     }
280   }
281 
282   for (n = 0; n < m.getNumParameters(); n++)
283   {
284     if (!m.getParameter(n)->getConstant())
285     {
286       mVariables.append(m.getParameter(n)->getId());
287     }
288     else if (m.getLevel() == 1)
289     {
290       mVariables.append(m.getParameter(n)->getId());
291     }
292   }
293 
294   for (n = 0; n < m.getNumReactions(); n++)
295   {
296     if (m.getReaction(n)->isSetKineticLaw())
297     {
298       mVariables.append(m.getReaction(n)->getId());
299     }
300     if (m.getLevel() > 2)
301     {
302       /* in L3 stoichiometry could be altered by rules
303        * so these need to be included
304        * where a speciesReference is marked as false
305        */
306       for (k = 0; k < m.getReaction(n)->getNumReactants(); k++)
307       {
308         if (m.getReaction(n)->getReactant(k)->getConstant() == false )
309         {
310           mVariables.append(m.getReaction(n)->getReactant(k)->getId());
311         }
312       }
313       for (k = 0; k < m.getReaction(n)->getNumProducts(); k++)
314       {
315         if (m.getReaction(n)->getProduct(k)->getConstant() == false )
316         {
317           mVariables.append(m.getReaction(n)->getProduct(k)->getId());
318         }
319       }
320     }
321   }
322 }
323 
324 unsigned int
getNumEquations()325 EquationMatching::getNumEquations()
326 {
327   return mEquations.size();
328 }
329 unsigned int
getNumVariables()330 EquationMatching::getNumVariables()
331 {
332   return mVariables.size();
333 }
334 /*
335  * creates a bipartite graph according to the L2V2 spec 4.11.5
336  * creates edges between the equation vertexes and the variable vertexes
337  * graph produced is an id representimg the equation and an IdList
338  * listing the edges the equation vertex is connected to
339  */
340 void
createGraph(const Model & m)341 EquationMatching::createGraph(const Model& m)
342 {
343   IdList joined;
344   IdList speciesAdded;
345   unsigned int n, sr;
346   const Reaction *r;
347   const Species *s;
348   const char * sId;
349   const Rule *rule;
350   const ASTNode *math;
351   const KineticLaw * kl;
352   List * names;
353   ASTNode * node;
354   string name;
355 
356   /* create a list of ids relating to
357    * 1. species
358    * 2. rules
359    * 3. kinetic laws
360    */
361   writeEquationVertexes(m);
362 
363   /* create a list relating to variables
364    * 1. compartments
365    * 2. species
366    * 3. parameters
367    * 4. reactions
368    */
369   writeVariableVertexes(m);
370 
371   /* create the edges for the graph */
372 
373   /*
374    * a Species structure that has the boundaryCondition field set to false
375    * and constant field set to false and which is referenced by the reactant
376    * or product lists of a Reaction structure containing a KineticLaw structure.
377    * The edge connects the vertex representing the species
378    *    to the vertex representing the species' equation
379    */
380 
381   speciesAdded.clear();
382   unsigned int eqnCount = 0;
383   for (n = 0; n < m.getNumReactions(); n++)
384   {
385     if (m.getReaction(n)->isSetKineticLaw())
386     {
387       r = m.getReaction(n);
388       for (sr = 0; sr < r->getNumReactants(); sr++)
389       {
390         s = m.getSpecies(r->getReactant(sr)->getSpecies());
391         sId = s->getId().c_str();
392 
393         if (mEquations.contains(sId)
394           && mVariables.contains(sId)
395           && !speciesAdded.contains(sId))
396         {
397           joined.append(sId);
398           speciesAdded.append(sId);
399         }
400         if (joined.size() > 0)
401         {
402           mGraph[mEquations.at((int)eqnCount)] = joined;
403           joined.clear();
404           eqnCount++;
405         }
406       }
407 
408       for (sr = 0; sr < r->getNumProducts(); sr++)
409       {
410         s = m.getSpecies(r->getProduct(sr)->getSpecies());
411         sId = s->getId().c_str();
412 
413         if (mEquations.contains(sId)
414           && mVariables.contains(sId)
415           && !speciesAdded.contains(sId))
416         {
417           joined.append(sId);
418           speciesAdded.append(sId);
419         }
420         if (joined.size() > 0)
421         {
422           mGraph[mEquations.at((int)eqnCount)] = joined;
423           joined.clear();
424           eqnCount++;
425         }
426       }
427     }
428   }
429 
430   /* rules */
431   for (n = 0; n < m.getNumRules(); n++)
432   {
433     rule = m.getRule(n);
434 
435     /*
436      * an AssignmentRule or RateRule.
437      * The edge connects the vertex representing the Rule to the vertex
438      * representing the variable referenced by the variable field of the rule.
439      */
440     if (rule->isAssignment() || rule->isRate())
441     {
442       if (mVariables.contains(rule->getVariable()))
443       {
444         joined.append(rule->getVariable());
445       }
446     }
447 
448     /*
449      * the occurrence of a MathML ci symbol referencing a variable within an
450      * AssignmentRule or AlgebraicRule.
451      * The ci element must either reference: (a) a Species, compartment or
452      * parameter structure which has the constant field set to false; or
453      * (b) reference a Reaction structure
454      * The edge connects the vertex representing the rule to the vertex
455      * representing the variable.
456      */
457     if (rule->isSetMath())
458     {
459       math = rule->getMath();
460       names = math->getListOfNodes( ASTNode_isName );
461 
462       for (sr = 0; sr < names->getSize(); sr++)
463       {
464         node = static_cast<ASTNode*>( names->get(sr) );
465         name = node->getName() ? node->getName() : "";
466         if (mVariables.contains(name))
467         {
468           joined.append(name);
469         }
470       }
471 
472       delete names;
473 
474     }
475 
476     mGraph[mEquations.at((int)eqnCount)] = joined;
477     joined.clear();
478     eqnCount++;
479   }
480 
481   /* kineticlaws */
482   for (n = 0; n < m.getNumReactions(); n++)
483   {
484     if (m.getReaction(n)->isSetKineticLaw())
485     {
486       /*
487        * a KineticLaw.
488        * The edge connects the vertex representing the KineticLaw equation
489        * to the variable vertex representing the Reaction containing the
490        * KineticLaw.
491        */
492       if (mVariables.contains(m.getReaction(n)->getId()))
493       {
494         joined.append(m.getReaction(n)->getId());
495       }
496 
497       /*
498        * the occurrence of a MathML ci symbol referencing a variable within
499        * an KineticLaw.
500        * The ci element must either reference a Species, compartment or
501        * parameter structure which has the constant field set to false.
502        * In this context a ci element cannot refer to a Reaction structure.
503        * The edge connects the vertex representing the kinetic law to the
504        * vertex representing the variable.
505        */
506       kl = m.getReaction(n)->getKineticLaw();
507 
508       if (kl->isSetMath())
509       {
510         math = kl->getMath();
511         names = math->getListOfNodes( ASTNode_isName );
512 
513         for (sr = 0; sr < names->getSize(); sr++)
514         {
515           node = static_cast<ASTNode*>( names->get(sr) );
516           name = node->getName() ? node->getName() : "";
517           if (mVariables.contains(name))
518           {
519             joined.append(name);
520           }
521         }
522 
523   delete names;
524 
525       }
526       mGraph[mEquations.at((int)eqnCount)] = joined;
527       joined.clear();
528       eqnCount++;
529     }
530   }
531 }
532 
533 /*
534  * finds a maximal matching of the bipartite graph
535  * adapted from the only implementation I could find:
536  * # Hopcroft-Karp bipartite max-cardinality mMatching and max independent set
537  * # David Eppstein, UC Irvine, 27 Apr 2002 - Python Cookbook
538  *
539  * returns an IdList of any equation vertexes that are unconnected
540  * in the maximal matching
541  */
542 IdList
findMatching()543 EquationMatching::findMatching()
544 {
545   IdList unmatchedEquations;
546 
547 
548   unsigned int n, p;
549   IdList temp;
550   IdList tempVarsInMatching;
551   IdList unmatch;
552   IdList layer;
553   IdList unmatchFlag;
554   unmatchFlag.append("unmatched");
555   graph newLayer;
556 
557   /* create greedy mMatching */
558   for (n = 0; n < mEquations.size(); n++)
559   {
560     for (p = 0; p < mGraph[mEquations.at((int)n)].size(); p++)
561     {
562       if (mMatching.count(mGraph[mEquations.at((int)n)].at((int)p)) == 0)
563       {
564         temp.append(mEquations.at((int)n));
565         mMatching[mGraph[mEquations.at((int)n)].at((int)p)] = temp;
566         temp.clear();
567         break;
568       }
569     }
570   }
571 
572   unsigned int maximal = 1;
573   while (maximal == 1)
574   {
575     unmatch.clear();
576     mVarNeighInPrev.clear();
577 
578     /* create mEqnNeighInPrev - graph giving neighbour in previous layer */
579 
580     /* list of variables in mMatching */
581     tempVarsInMatching.clear();
582     for (graph::iterator iter = mMatching.begin(); iter != mMatching.end(); iter++)
583     {
584       tempVarsInMatching.append((*iter).second.at(0));
585     }
586 
587 
588     for (n = 0; n < mEquations.size(); n++)
589     {
590       if (!tempVarsInMatching.contains(mEquations.at((int)n)))
591       {
592         mEqnNeighInPrev[mEquations.at((int)n)] = unmatchFlag;
593         layer.append(mEquations.at((int)n));
594       }
595     }
596 
597     /* extend layering structure */
598     while (layer.size() > 0 && unmatch.size() == 0)
599     {
600       graph::iterator iter;
601 
602       newLayer.clear();
603 
604       temp.clear();
605       for (iter = mVarNeighInPrev.begin();
606                         iter != mVarNeighInPrev.end(); iter++)
607       {
608         temp.append((*iter).first);
609       }
610       for (n = 0; n < layer.size(); n++)
611       {
612         for (p = 0; p < mGraph[layer.at((int)n)].size(); p++)
613         {
614           if (!temp.contains(mGraph[layer.at((int)n)].at((int)p)))
615           {
616             newLayer[mGraph[layer.at((int)n)].at((int)p)].append(layer.at((int)n));
617           }
618         }
619       }
620 
621       layer.clear();
622       temp.clear();
623       for (iter = newLayer.begin();
624                         iter != newLayer.end(); iter++)
625       {
626         mVarNeighInPrev[(*iter).first] = (*iter).second;
627         if (tempVarsInMatching.contains((*iter).first))
628         {
629           layer.append(mMatching[(*iter).first].at(0));
630           temp.append((*iter).first);
631           mEqnNeighInPrev[mMatching[(*iter).first].at(0)] = temp;
632         }
633         else
634         {
635           unmatch.append((*iter).first);
636         }
637       }
638     } // end of extending layers while statement
639 
640     /* finished without needing alternative paths */
641     if (unmatch.size() == 0)
642     {
643       /* list any equations that are not matched */
644       temp.clear();
645       for (graph::iterator iter = mMatching.begin();
646                         iter != mMatching.end(); iter++)
647       {
648         temp.append(mMatching[(*iter).first].at(0));
649       }
650 
651       for (n = 0; n < mEquations.size(); n++)
652       {
653         if (!temp.contains(mEquations.at((int)n)))
654         {
655           unmatchedEquations.append(mEquations.at((int)n));
656         }
657       }
658       maximal = 0;
659     }
660     else
661     {
662       for (n = 0; n < unmatch.size(); n++)
663       {
664         maximal = Recurse(unmatch.at((int)n));
665         if (maximal == 2) break;
666       }
667     }
668   }
669 
670   if (maximal == 2)
671   {
672     // we have a flip flopping set of matches
673     unmatchedEquations.append(mMatching[unmatch.at((int)n)].at(0));
674   }
675   return unmatchedEquations;
676 
677 }
678 
679 
680 /*
681  * function that looks for alternative paths and adds these to the matching
682  * where necessary
683  */
684 unsigned int
Recurse(std::string v)685 EquationMatching::Recurse(std::string v)
686 {
687   //static graph revisited;
688   //static IdList visited;
689   unsigned int rec = 0;
690   unsigned int n;
691   IdList tempVarNeigh;
692   IdList tempEqnNeigh;
693   IdList L;
694   IdList pu;
695   IdList prev;
696 
697   graph::iterator iter;
698 
699   tempVarNeigh.clear();
700   for (iter = mVarNeighInPrev.begin();
701                     iter != mVarNeighInPrev.end(); iter++)
702   {
703     tempVarNeigh.append((*iter).first);
704   }
705 
706   tempEqnNeigh.clear();
707   for (iter = mEqnNeighInPrev.begin();
708                     iter != mEqnNeighInPrev.end(); iter++)
709   {
710     tempEqnNeigh.append((*iter).first);
711   }
712 
713   if (tempVarNeigh.contains(v))
714   {
715     L = mVarNeighInPrev[v];
716     mVarNeighInPrev.erase(v);
717 
718     for (n = 0; n < L.size(); n++)
719     {
720       if (tempEqnNeigh.contains(L.at((int)n)))
721       {
722         pu = mEqnNeighInPrev[L.at((int)n)];
723         mEqnNeighInPrev.erase(L.at((int)n));
724 
725         if (pu.size() == 0)
726           break;
727 
728         if (pu.size() == 1 && !strcmp(pu.at(0).c_str(), "unmatched"))
729         {
730           /* look to see if variable has been here before
731            * in order t catch variables that are flip flopping
732            * between equations
733            */
734           bool repeat = false;
735           if (!visited.contains(v))
736           {
737             visited.append(v);
738             revisited[v] = L;
739           }
740           else
741           {
742             prev = revisited[v];
743             unsigned int i;
744             for (i = 0; i < L.size(); i++)
745             {
746               if (prev.contains(L.at((int)i)))
747               {
748                 repeat = true;
749               }
750               else
751               {
752                 prev.append(L.at((int)i));
753               }
754               if (repeat) break;
755             }
756           }
757           if (!repeat)
758           {
759             mMatching[v] = L;
760             rec = 1;
761           }
762           else
763           {
764             rec = 2;
765             return rec;
766           }
767         }
768         else if (Recurse(pu.at(0)))
769         {
770           mMatching[v] = L;
771           rec = 1;
772         }
773       }
774     }
775   }
776   return rec;
777 }
778 
779 
780 /* return true if the pair are matched
781  */
782 bool
match_dependency(const std::string & var,const std::string & eq)783 EquationMatching::match_dependency(const std::string& var, const std::string& eq)
784 {
785   bool match = false;
786   IdList matches = mMatching[var];
787 
788   if (matches.size() == 1 && matches.at(0) == eq)
789     match = true;
790 
791 
792   return match;
793 }
794 LIBSBML_CPP_NAMESPACE_END
795 /** @endcond */
796