1 /**
2  * @cond doxygenLibsbmlInternal
3  *
4  * @file    PowerUnitsCheck.cpp
5  * @brief   Ensures math units are consistent.
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/Species.h>
47 #include <sbml/Parameter.h>
48 #include <sbml/UnitDefinition.h>
49 #include <sbml/Event.h>
50 #include <sbml/Reaction.h>
51 #include <sbml/EventAssignment.h>
52 #include <sbml/SpeciesReference.h>
53 #include <sbml/Rule.h>
54 #include <sbml/SBMLTransforms.h>
55 #include <sbml/math/FormulaFormatter.h>
56 #include <sbml/util/IdList.h>
57 
58 #include <sbml/units/UnitFormulaFormatter.h>
59 
60 #include <sbml/util/List.h>
61 #include <sbml/util/util.h>
62 
63 #include "PowerUnitsCheck.h"
64 
65 
66 /** @cond doxygenIgnored */
67 using namespace std;
68 /** @endcond */
69 
70 LIBSBML_CPP_NAMESPACE_BEGIN
71 
72 static const char* PREAMBLE =
73   "A math expression using power with non-integer units may result in incorrect units.";
74 
75 
76 /*
77  * Creates a new Constraint with the given @p id.
78  */
PowerUnitsCheck(unsigned int id,Validator & v)79 PowerUnitsCheck::PowerUnitsCheck (unsigned int id, Validator& v) : UnitsBase(id, v)
80 {
81 }
82 
83 
84 /*
85  * Destroys this Constraint.
86  */
~PowerUnitsCheck()87 PowerUnitsCheck::~PowerUnitsCheck ()
88 {
89 }
90 
91 /*
92  * @return the preamble to use when logging constraint violations.
93  */
94 const char*
getPreamble()95 PowerUnitsCheck::getPreamble ()
96 {
97   return PREAMBLE;
98 }
99 
100 
101 
102 
103 /*
104  * Checks that the units of the result of the assignment rule
105  * are consistent with variable being assigned
106  *
107  * If an inconsistent variable is found, an error message is logged.
108  */
109 void
checkUnits(const Model & m,const ASTNode & node,const SBase & sb,bool inKL,int reactNo)110 PowerUnitsCheck::checkUnits (const Model& m, const ASTNode& node, const SBase & sb,
111                                  bool inKL, int reactNo)
112 {
113   ASTNodeType_t type = node.getType();
114 
115   switch (type)
116   {
117     //case AST_DIVIDE:
118     //  checkForPowersBeingDivided(m, node, sb);
119     //  break;
120     case AST_POWER:
121     case AST_FUNCTION_POWER:
122 
123       checkUnitsFromPower(m, node, sb, inKL, reactNo);
124       break;
125 
126     case AST_FUNCTION:
127 
128       checkFunction(m, node, sb, inKL, reactNo);
129       break;
130 
131     default:
132 
133       checkChildren(m, node, sb, inKL, reactNo);
134       break;
135 
136   }
137 }
138 
139 
140 /*
141  * Checks that the units of the power function are consistent
142  *
143  * If inconsistent units are found, an error message is logged.
144  *
145  * The two arguments to power, which are of the form power(a, b)
146  * with the meaning a^b, should be as follows:
147  * (1) if the second argument is an integer,
148  *     then the first argument can have any units;
149  * (2) if the second argument b is a rational number n/m,
150  * it must be possible to derive the m-th root of (a{unit})n,
151  * where {unit} signifies the units associated with a;
152  * otherwise, (3) the units of the first argument must be 'dimensionless'.
153  * The second argument (b) should always have units of 'dimensionless'.
154  *
155  */
156 void
checkUnitsFromPower(const Model & m,const ASTNode & node,const SBase & sb,bool inKL,int reactNo)157 PowerUnitsCheck::checkUnitsFromPower (const Model& m,
158                                         const ASTNode& node,
159                                         const SBase & sb, bool inKL, int reactNo)
160 {
161 
162   /* check that node has 2 children */
163   if (node.getNumChildren() != 2)
164   {
165     return;
166   }
167 
168   double value;
169   UnitDefinition dim(m.getSBMLNamespaces());
170   Unit unit(m.getSBMLNamespaces());
171   unit.setKind(UNIT_KIND_DIMENSIONLESS);
172   unit.initDefaults();
173   dim.addUnit(&unit);
174 
175   UnitFormulaFormatter *unitFormat = new UnitFormulaFormatter(&m);
176 
177   UnitDefinition *tempUD = NULL;
178   UnitDefinition *unitsArg1, *unitsArgPower;
179   unitsArg1 = unitFormat->getUnitDefinition(node.getLeftChild(), inKL, reactNo);
180   unsigned int undeclaredUnits =
181     unitFormat->getContainsUndeclaredUnits();
182 
183   ASTNode *child = node.getRightChild();
184   unitFormat->resetFlags();
185   unitsArgPower = unitFormat->getUnitDefinition(child, inKL, reactNo);
186 
187   unsigned int undeclaredUnitsPower =
188     unitFormat->getContainsUndeclaredUnits();
189 
190   // The second argument (b) should always have units of 'dimensionless'.
191   // or it has undeclared units that we assume are correct
192 
193   if (undeclaredUnitsPower == 0 && !UnitDefinition::areEquivalent(&dim, unitsArgPower))
194   {
195     logNonDimensionlessPowerConflict(node, sb);
196   }
197 
198   // The first argument is dimensionless then it doesnt matter
199   // what the power is
200 
201   if (undeclaredUnits == 0 && !UnitDefinition::areEquivalent(&dim, unitsArg1))
202   {
203     // if not argument needs to be an integer or a rational
204     unsigned int isInteger = 0;
205     unsigned int isRational = 0;
206     unsigned int isExpression = 0;
207     /* power must be an integer
208      * but need to check that it is not a real
209      * number that is integral
210      * i.e. mathml <cn> 2 </cn> will record a "real"
211      */
212     if (child->isRational())
213     {
214       isRational = 1;
215     }
216     else if (child->isInteger())
217     {
218       isInteger = 1;
219     }
220     else if (child->isReal())
221     {
222       if (ceil(child->getReal()) == child->getReal())
223       {
224         isInteger = 1;
225       }
226     }
227     else if (child->getNumChildren() > 0)
228 
229     {
230       // power might itself be an expression
231       tempUD = unitFormat->getUnitDefinition(child, inKL, reactNo);
232       UnitDefinition::simplify(tempUD);
233 
234       if (tempUD->isVariantOfDimensionless())
235       {
236         SBMLTransforms::mapComponentValues(&m);
237         double value1 = SBMLTransforms::evaluateASTNode(child);
238         SBMLTransforms::clearComponentValues();
239         if (!util_isNaN(value1))
240         {
241           if (floor(value1) != value1)
242             isExpression = 1;
243           else
244             isInteger = 1;
245         }
246         else
247         {
248           isExpression = 1;
249         }
250       }
251       else
252       {
253         /* here the child is an expression with units
254         * flag the expression as not checked
255         */
256         isExpression = 1;
257       }
258     }
259     else
260     {
261       // power could be a parameter or a speciesReference in l3
262       const Parameter *param = NULL;
263       const SpeciesReference *sr = NULL;
264 
265       if (child->isName())
266       {
267         /* Parameters may be declared in two places (the model and the
268         * kinetic law's local parameter list), so we have to check both.
269         */
270 
271         if (sb.getTypeCode() == SBML_KINETIC_LAW)
272         {
273           const KineticLaw* kl = dynamic_cast<const KineticLaw*>(&sb);
274 
275           /* First try local parameters and if null is returned, try
276           * the global parameters */
277           if (kl != NULL)
278           {
279             param = kl->getParameter(child->getName());
280           }
281         }
282 
283         if (param == NULL)
284         {
285           param = m.getParameter(child->getName());
286         }
287 
288         if (param == NULL && m.getLevel() > 2)
289         {
290           // could be a species reference
291           sr = m.getSpeciesReference(child->getName());
292         }
293 
294       }
295 
296       if (param != NULL)
297       {
298         /* We found a parameter with this identifier. */
299 
300         if (UnitDefinition::areEquivalent(&dim, unitsArgPower) || undeclaredUnitsPower)
301         {
302           value = param->getValue();
303           if (value != 0)
304           {
305             if (ceil(value) == value)
306             {
307               isInteger = 1;
308             }
309           }
310 
311         }
312         else
313         {
314     /* No parameter definition found for child->getName() */
315           logUnitConflict(node, sb);
316         }
317       }
318       else if (sr != NULL)
319       {
320         // technically here there is an issue
321         // stoichiometry is dimensionless
322         SBMLTransforms::mapComponentValues(&m);
323         double value1 = SBMLTransforms::evaluateASTNode(child, &m);
324         SBMLTransforms::clearComponentValues();
325         // but it may not be an integer
326         if (util_isNaN(value1))
327           // we cant check
328         {
329           isExpression = 1;
330         }
331         else
332         {
333           if (ceil(value1) == value1)
334           {
335             isInteger = 1;
336           }
337         }
338       }
339     }
340 
341     if (isRational == 1)
342     {
343       //FIX-ME will need sorting for double exponents
344 
345       //* (2) if the second argument b is a rational number n/m,
346       //* it must be possible to derive the m-th root of (a{unit})n,
347       //* where {unit} signifies the units associated with a;
348       unsigned int impossible = 0;
349       for (unsigned int n = 0; impossible == 0 && n < unitsArg1->getNumUnits(); n++)
350       {
351         if ((int)(unitsArg1->getUnit(n)->getExponent()) * child->getInteger() %
352           child->getDenominator() != 0)
353           impossible = 1;
354       }
355 
356       if (impossible)
357         logRationalPowerConflict(node, sb);
358 
359     }
360     else if (isExpression == 1)
361     {
362       logExpressionPowerConflict(node, sb);
363     }
364     else if (isInteger == 0)
365     {
366       // in level 3 this would be allowed
367       if (m.getLevel() < 3)
368       {
369         logNonIntegerPowerConflict(node, sb);
370       }
371     }
372 
373   }
374 
375 
376 
377 
378  // if (!areEquivalent(dim, unitsPower))
379  // {
380  //   /* 'v' does not have units of dimensionless. */
381 
382  //   /* If the power 'n' is a parameter, check if its units are either
383  //    * undeclared or declared as dimensionless.  If either is the case,
384  //    * the value of 'n' must be an integer.
385  //    */
386 
387  //   const Parameter *param = NULL;
388 
389  //   if (child->isName())
390  //   {
391  //     /* Parameters may be declared in two places (the model and the
392  //      * kinetic law's local parameter list), so we have to check both.
393  //      */
394 
395  //     if (sb.getTypeCode() == SBML_KINETIC_LAW)
396  //     {
397   //      const KineticLaw* kl = dynamic_cast<const KineticLaw*>(&sb);
398 
399   //      /* First try local parameters and if null is returned, try
400   //      * the global parameters */
401   //      if (kl != NULL)
402   //      {
403   //        param = kl->getParameter(child->getName());
404   //      }
405  //     }
406 
407   //    if (param == NULL)
408   //    {
409   //      param = m.getParameter(child->getName());
410   //    }
411  //
412  //   }
413 
414  //   if (param != NULL)
415  //   {
416  //     /* We found a parameter with this identifier. */
417 
418  //     if (areEquivalent(dim, unitsArgPower) || unitFormat->hasUndeclaredUnits(child))
419  //     {
420  //       value = param->getValue();
421  //       if (value != 0)
422  //       {
423  //         if (ceil(value) != value)
424  //         {
425  //           logUnitConflict(node, sb);
426  //         }
427  //       }
428 
429  //     }
430  //     else
431  //     {
432   ///* No parameter definition found for child->getName() */
433  //       logUnitConflict(node, sb);
434  //     }
435  //   }
436  //   else if (child->isFunction() || child->isOperator())
437  //   {
438  //     /* cannot test whether the value will be appropriate */
439  //     if (!areEquivalent(dim, unitsArgPower))
440  //     {
441  //       logUnitConflict(node, sb);
442  //     }
443  //   }
444  //   /* power must be an integer
445  //    * but need to check that it is not a real
446  //    * number that is integral
447  //    * i.e. mathml <cn> 2 </cn> will record a "real"
448  //    */
449  //   else if (!child->isInteger())
450  //   {
451  //     if (!child->isReal())
452  //     {
453  //       logUnitConflict(node, sb);
454  //     }
455  //     else if (ceil(child->getReal()) != child->getReal())
456  //     {
457  //       logUnitConflict(node, sb);
458  //     }
459  //   }
460  // }
461  // else if (!areEquivalent(dim, unitsArgPower))
462  // {
463  //   /* power (3, k) */
464  //   logUnitConflict(node, sb);
465  // }
466 
467   checkUnits(m, *node.getLeftChild(), sb, inKL, reactNo);
468 
469   if (tempUD != NULL) delete tempUD;
470   delete unitFormat;
471   delete unitsArg1;
472   delete unitsArgPower;
473 }
474 
475 
476 /*
477  * @return the error message to use when logging constraint violations.
478  * This method is called by logFailure.
479  *
480  * Returns a message that the given @p id and its corresponding object are
481  * in  conflict with an object previously defined.
482  */
483 const string
getMessage(const ASTNode & node,const SBase & object)484 PowerUnitsCheck::getMessage (const ASTNode& node, const SBase& object)
485 {
486 
487   ostringstream oss_msg;
488 
489   //oss_msg << getPreamble();
490 
491   char * formula = SBML_formulaToString(&node);
492   oss_msg << "The formula '" << formula;
493   oss_msg << "' in the " << getFieldname() << " element of the <" << object.getElementName();
494   oss_msg << "> ";
495   switch(object.getTypeCode()) {
496   case SBML_INITIAL_ASSIGNMENT:
497   case SBML_EVENT_ASSIGNMENT:
498   case SBML_ASSIGNMENT_RULE:
499   case SBML_RATE_RULE:
500     //LS DEBUG:  could use other attribute values, or 'isSetActualId'.
501     break;
502   default:
503     if (object.isSetId()) {
504       oss_msg << "with id '" << object.getId() << "' ";
505     }
506     break;
507   }
508   oss_msg << "contains a power that is not an integer and thus may produce ";
509   oss_msg << "invalid units.";
510   safe_free(formula);
511 
512   return oss_msg.str();
513 }
514 
515 
516 //void
517 //PowerUnitsCheck::checkForPowersBeingDivided (const Model& m, const ASTNode& node,
518 //                              const SBase & sb)
519 //{
520 //  ASTNode* left = node.getLeftChild();
521 //  ASTNode* right = node.getRightChild();
522 //
523 //  if (left->getType() == AST_POWER || left->getType() == AST_FUNCTION_POWER)
524 //  {
525 //    if (right->getType() == AST_POWER || right->getType() == AST_FUNCTION_POWER)
526 //    {
527 //      /* have a power divided by a power */
528 //      /* check whether objects have same units */
529 //        UnitFormulaFormatter *unitFormat = new UnitFormulaFormatter(&m);
530 //
531 //        UnitDefinition *tempUD, *tempUD1, *tempUD2, *tempUD3;
532 //        tempUD = unitFormat->getUnitDefinition(left->getLeftChild());
533 //        tempUD1 = unitFormat->getUnitDefinition(right->getLeftChild());
534 //        tempUD2 = unitFormat->getUnitDefinition(right->getRightChild());
535 //        tempUD3 = unitFormat->getUnitDefinition(left->getRightChild());
536 //
537 //        if (!areEquivalent(tempUD, tempUD1))
538 //        {
539 //          checkChildren(m, node, sb);
540 //        }
541 //        else
542 //        {
543 //          if(!areEquivalent(tempUD2, tempUD3))
544 //          {
545 //            logUnitConflict(node, sb);
546 //          }
547 //          else
548 //          {
549 //            /* create an ASTNode with pow(object, left_exp - right_exp) */
550 //            ASTNode *newPower = new ASTNode(AST_POWER);
551 //            ASTNode * newMinus = new ASTNode(AST_MINUS);
552 //            newMinus->addChild(left->getRightChild()->deepCopy());
553 //            newMinus->addChild(right->getRightChild()->deepCopy());
554 //            newPower->addChild(left->getLeftChild()->deepCopy());
555 //            newPower->addChild(newMinus);
556 //
557 //            checkUnitsFromPower(m, *newPower, sb);
558 //
559 //            delete newPower;
560 //          }
561 //        }
562 //
563 //        delete unitFormat;
564 //        delete tempUD;
565 //        delete tempUD1;
566 //        delete tempUD2;
567 //        delete tempUD3;
568 //    }
569 //    else
570 //    {
571 //      checkChildren(m, node, sb);
572 //    }
573 //  }
574 //  else
575 //  {
576 //    checkChildren(m, node, sb);
577 //  }
578 //}
579 /*
580  * Logs a message about a function that should return same units
581  * as the arguments
582  */
583 void
logNonDimensionlessPowerConflict(const ASTNode & node,const SBase & sb)584 PowerUnitsCheck::logNonDimensionlessPowerConflict (const ASTNode & node,
585                                              const SBase & sb)
586 {
587   char * formula = SBML_formulaToString(&node);
588   msg = "The formula '";
589   msg += formula;
590   msg += "' in the ";
591   msg += getFieldname();
592   msg += " element of the <" + sb.getElementName();
593   msg += "> ";
594   switch(sb.getTypeCode()) {
595   case SBML_INITIAL_ASSIGNMENT:
596   case SBML_EVENT_ASSIGNMENT:
597   case SBML_ASSIGNMENT_RULE:
598   case SBML_RATE_RULE:
599     //LS DEBUG:  could use other attribute values, or 'isSetActualId'.
600     break;
601   default:
602     if (sb.isSetId()) {
603       msg += "with id '";
604       msg += sb.getId() + "' ";
605     }
606     break;
607   }
608   msg += "contains a power that is not dimensionless and thus may produce ";
609   msg += "invalid units.";
610   safe_free(formula);
611 
612   logFailure(sb, msg);
613 
614 }
615 
616 
617 void
logNonIntegerPowerConflict(const ASTNode & node,const SBase & sb)618 PowerUnitsCheck::logNonIntegerPowerConflict (const ASTNode & node,
619                                              const SBase & sb)
620 {
621   char * formula = SBML_formulaToString(&node);
622   msg = "The formula '";
623   msg += formula;
624   msg += "' in the ";
625   msg += getFieldname();
626   msg += " element of the <" + sb.getElementName();
627   msg += "> ";
628   switch(sb.getTypeCode()) {
629   case SBML_INITIAL_ASSIGNMENT:
630   case SBML_EVENT_ASSIGNMENT:
631   case SBML_ASSIGNMENT_RULE:
632   case SBML_RATE_RULE:
633     //LS DEBUG:  could use other attribute values, or 'isSetActualId'.
634     break;
635   default:
636     if (sb.isSetId()) {
637       msg += "with id '";
638       msg += sb.getId() + "' ";
639     }
640     break;
641   }
642   msg += "contains a power that is not an integer and thus may produce ";
643   msg += "invalid units.";
644   safe_free(formula);
645 
646   logFailure(sb, msg);
647 
648 }
649 
650 void
logRationalPowerConflict(const ASTNode & node,const SBase & sb)651 PowerUnitsCheck::logRationalPowerConflict (const ASTNode & node,
652                                              const SBase & sb)
653 {
654   char * formula = SBML_formulaToString(&node);
655   msg = "The formula '";
656   msg += formula;
657   msg += "' in the ";
658   msg += getFieldname();
659   msg += " element of the <" + sb.getElementName();
660   msg += "> ";
661   switch(sb.getTypeCode()) {
662   case SBML_INITIAL_ASSIGNMENT:
663   case SBML_EVENT_ASSIGNMENT:
664   case SBML_ASSIGNMENT_RULE:
665   case SBML_RATE_RULE:
666     //LS DEBUG:  could use other attribute values, or 'isSetActualId'.
667     break;
668   default:
669     if (sb.isSetId()) {
670       msg += "with id '";
671       msg += sb.getId() + "' ";
672     }
673     break;
674   }
675   msg += "contains a rational power that is inconsistent and thus may produce ";
676   msg += "invalid units.";
677   safe_free(formula);
678 
679   logFailure(sb, msg);
680 
681 }
682 
683 
684 void
logExpressionPowerConflict(const ASTNode & node,const SBase & sb)685 PowerUnitsCheck::logExpressionPowerConflict (const ASTNode & node,
686                                              const SBase & sb)
687 {
688   char * formula = SBML_formulaToString(&node);
689   msg = "The formula '";
690   msg += formula;
691   msg += "' in the ";
692   msg += getFieldname();
693   msg += " element of the <" + sb.getElementName();
694   msg += "> ";
695   switch(sb.getTypeCode()) {
696   case SBML_INITIAL_ASSIGNMENT:
697   case SBML_EVENT_ASSIGNMENT:
698   case SBML_ASSIGNMENT_RULE:
699   case SBML_RATE_RULE:
700     //LS DEBUG:  could use other attribute values, or 'isSetActualId'.
701     break;
702   default:
703     if (sb.isSetId()) {
704       msg += "with id '";
705       msg += sb.getId() + "' ";
706     }
707     break;
708   }
709   msg += "contains an expression for the exponent of the power function ";
710   msg += "and thus cannot be checked for unit validity.";
711 
712 
713   safe_free(formula);
714 
715   logFailure(sb, msg);
716 
717 }
718 
719 
720 LIBSBML_CPP_NAMESPACE_END
721 /** @endcond */
722 
723