From 82e3d1bacd03d25ddf1180ade9a821e5416e5df1 Mon Sep 17 00:00:00 2001 From: Aldrich Fan Date: Wed, 17 Apr 2024 14:27:18 -0700 Subject: [PATCH] 0.2.5 See readme --- .gitignore | 3 + MANIFEST.in | 30 ++ README.md | 12 + ratesb_python/__init__.py | 3 +- ratesb_python/common/analyzer.py | 390 ++++++++-------------- ratesb_python/common/custom_classifier.py | 26 +- ratesb_python/common/reaction_data.py | 310 +++++++++++++++++ ratesb_python/common/results.py | 2 + ratesb_python/common/util.py | 61 ++-- tests/test_analyzer.py | 52 +-- tests/test_classifier.py | 67 ++-- 11 files changed, 599 insertions(+), 357 deletions(-) create mode 100644 MANIFEST.in create mode 100644 ratesb_python/common/reaction_data.py diff --git a/.gitignore b/.gitignore index 02ca880..516bd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +tests/biomodels/* +tests/test_biomodels.py # Translations *.mo @@ -67,6 +69,7 @@ instance/ # Scrapy stuff: .scrapy +.DS_Store # Sphinx documentation docs/_build/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..58a4e08 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,30 @@ +# Include the README, LICENSE, and CHANGELOG files +include README.md +include LICENSE +include CHANGELOG.md + +# Include JSON data files in the specified package directory +recursive-include ratesb_python/common *.json + +# Exclude all bytecode +global-exclude *.pyc +global-exclude __pycache__ +global-exclude *.pyo + +# Exclude files and directories related to development, version control, or builds +exclude .gitignore +exclude .gitattributes +exclude .devcontainer +exclude .coverage +exclude dist +exclude tests +exclude venv +exclude paper +exclude *.egg-info +recursive-exclude *.egg-info * +recursive-exclude build * +recursive-exclude .devcontainer * +recursive-exclude dist * +recursive-exclude tests * +recursive-exclude venv * +recursive-exclude paper * diff --git a/README.md b/README.md index 35f8818..fc4f3d7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ results = analyzer.results # Display all errors and warnings print(results) +# Check selected errors and warnings +analyzer.checks([1, 2, 1001, 1002]) + +# No need to set results = analyzer.results again as results updates automatically +print(results) + # Display only warnings warnings = results.get_warnings() for reaction, messages in warnings.items(): @@ -183,6 +189,11 @@ python -m unittest ### 0.2.4 * updated instructions in readme +### 0.2.5 +* Separated model reading from analysis +* Tested on 1054 biomodels and fixed bugs +* Added check_model method to allow user to use the package with one line + ## Contributing Contributions to `ratesb_python` are welcomed! Whether it's bug reports, feature requests, or new code contributions, we value your feedback and contributions. Please submit a pull request or open an issue on our [GitHub repo](https://github.com/sys-bio/ratesb_python). @@ -199,6 +210,7 @@ Contributions to `ratesb_python` are welcomed! Whether it's bug reports, feature * Implement stoichiometry checks for mass actions. * Perform checks after default classification to optimize performance. +* Give user option to not use the default rate law classification to improve performance ## Known Issues diff --git a/ratesb_python/__init__.py b/ratesb_python/__init__.py index fb48c99..9c67d2d 100644 --- a/ratesb_python/__init__.py +++ b/ratesb_python/__init__.py @@ -1 +1,2 @@ -from ratesb_python.common.analyzer import Analyzer \ No newline at end of file +from ratesb_python.common.analyzer import Analyzer +from ratesb_python.common.analyzer import check_model \ No newline at end of file diff --git a/ratesb_python/common/analyzer.py b/ratesb_python/common/analyzer.py index f77418f..a2129d9 100644 --- a/ratesb_python/common/analyzer.py +++ b/ratesb_python/common/analyzer.py @@ -2,27 +2,21 @@ import json import sys import os + current_dir = os.path.abspath(os.path.dirname(__file__)) parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) sys.path.append(current_dir) sys.path.append(parent_dir) -from custom_classifier import _CustomClassifier -# from SBMLKinetics.common.simple_sbml import SimpleSBML -# from SBMLKinetics.common.reaction import Reaction -from typing import List, Dict, Optional -from SBMLKinetics.common.simple_sbml import SimpleSBML -import util +from typing import List, Optional from common import util -from results import Results +from reaction_data import AnalyzerData -import antimony -import libsbml import os import re import sympy as sp -from typing import List, Any +from typing import List ZERO = "ZERO" UNDR1 = "UNDR1" @@ -79,6 +73,8 @@ HILL_SBOS = [192, 195, 198] +CLASSIFICATION_RELATED_CHECKS = [1002, 1020, 1021, 1022, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1040, 1041, 1042, 1043, 1044] + ALL_CHECKS = [] ERROR_CHECKS = [] WARNING_CHECKS = [] @@ -124,6 +120,23 @@ class ReactionData: codes: List[int] non_constant_params: List[str] +@staticmethod +def check_model(model_str: str, rate_law_classifications_path: str=None, abort_on_complicated_rate_laws: bool=True, excluded_codes: List[int]=[]): + """ + Checks the SBML model for rate law errors and warnings. + + Args: + model_str (str): Path to the model file, or the string representation of model. + rate_law_classifications_path (str): Path to the rate law classification file. + abort_on_complicated_rate_laws (bool): If True, the check will abort if the rate law is too complicated to process. + excluded_codes (List[int]): List of codes of the checks to exclude. If None, all checks are performed. + + Returns: + The results of the checks as a result object, can be printed or converted to string. + """ + analyzer = Analyzer(model_str, rate_law_classifications_path, abort_on_complicated_rate_laws) + analyzer.check_except(excluded_codes) + return analyzer.results class Analyzer: """ @@ -165,7 +178,7 @@ def list_check(code): return ret - def __init__(self, model_str: str, rate_law_classifications_path: str=None): + def __init__(self, model_str: str, rate_law_classifications_path: str=None, abort_on_complicated_rate_laws: bool=True): """ Initializes the Analyzer class. @@ -182,53 +195,8 @@ def __init__(self, model_str: str, rate_law_classifications_path: str=None): print(str(results)) str(results) """ - # check if parameters are strings - if not isinstance(model_str, str): - raise ValueError("Invalid model_str format, should be string.") - if rate_law_classifications_path and not isinstance(rate_law_classifications_path, str): - raise ValueError("Invalid rate_law_classifications_path format, should be string.") - # check if the model_str is sbml - if ' 0: - xml = antimony.getSBMLString() - elif model_str.endswith('.ant') or model_str.endswith('.txt') or model_str.endswith('.xml'): - # model_str is path to model - xml = '' - if model_str.endswith('.ant') or model_str.endswith('.txt'): - ant = util.get_model_str(model_str, False) - load_int = antimony.loadAntimonyString(ant) - if load_int > 0: - xml = antimony.getSBMLString() - else: - raise ValueError("Invalid Antimony model.") - else: - xml = util.get_model_str(model_str, True) - else: - raise ValueError("Invalid model_str format, should be SBML or Antimony string, or path to model file.") - reader = libsbml.SBMLReader() - document = reader.readSBMLFromString(xml) - util.checkSBMLDocument(document) - self.model = document.getModel() - self.simple = SimpleSBML(self.model) - self.custom_classifier = None - self.default_classifications = {} - self.custom_classifications = {} - self.results = Results() - default_classifier_path = os.path.join(current_dir, "default_classifier.json") - self.default_classifier = _CustomClassifier(default_classifier_path) - - if rate_law_classifications_path: - self.custom_classifier = _CustomClassifier( - rate_law_classifications_path) - if len(self.custom_classifier.warning_message) > 0: - print(self.custom_classifier.warning_message) + self.data = AnalyzerData(model_str, rate_law_classifications_path) + self.results = self.data.results def check_except(self, excluded_codes: Optional[List[int]]=[]): """ @@ -263,156 +231,51 @@ def checks(self, codes): The results of the checks to self.results. """ - self.default_classifications = {} - self.custom_classifications = {} - self.results.clear_results() - - for reaction in self.simple.reactions: - reaction.kinetic_law.formula = reaction.kinetic_law.formula.replace( - '^', '**') - ids_list = list(dict.fromkeys(reaction.kinetic_law.symbols)) - - libsbml_kinetics = reaction.kinetic_law.libsbml_kinetics - sbo_term = -1 - if libsbml_kinetics: - sbo_term = libsbml_kinetics.getSBOTerm() - - reaction_id = reaction.getId() - sorted_species = self._get_sorted_species(reaction.reaction) - species_list, parameter_list, compartment_list, kinetics, kinetics_sim = self._preprocess_reactions( - reaction) - reactant_list, product_list = self._extract_kinetics_details( - reaction) - species_in_kinetic_law, parameters_in_kinetic_law_only, compartment_in_kinetic_law, others_in_kinetic_law = self._identify_parameters_in_kinetics( - ids_list, species_list, parameter_list, compartment_list) - boundary_species = self._get_boundary_species( - reactant_list, product_list) - non_constant_params = self._get_non_constant_params(parameters_in_kinetic_law_only) - - - data = ReactionData( - reaction_id=reaction_id, - kinetics=kinetics, - kinetics_sim=kinetics_sim, - reactant_list=reactant_list, - product_list=product_list, - species_in_kinetic_law=species_in_kinetic_law, - parameters_in_kinetic_law=parameters_in_kinetic_law_only + others_in_kinetic_law, - ids_list=ids_list, - sorted_species=sorted_species, - boundary_species=boundary_species, - parameters_in_kinetic_law_only=parameters_in_kinetic_law_only, - compartment_in_kinetic_law=compartment_in_kinetic_law, - is_reversible=reaction.reaction.getReversible(), - sbo_term=sbo_term, - codes=codes, - non_constant_params=non_constant_params - ) - - self._set_kinetics_type(**data.__dict__) - if 1 in codes: - self._check_empty_kinetics(**data.__dict__) - if 2 in codes: - self._check_floating_species(**data.__dict__) - if 1001 in codes: - self._check_pure_number(**data.__dict__) - if 1002 in codes: - self._check_unrecognized_rate_law(**data.__dict__) - if 1003 in codes: - self._check_flux_increasing_with_reactant(**data.__dict__) - if 1004 in codes: - self._check_flux_decreasing_with_product(**data.__dict__) - if 1005 in codes: - self._check_boundary_floating_species(**data.__dict__) - if 1006 in codes: - self._check_constant_parameters(**data.__dict__) - if 1010 in codes: - self._check_irreversibility(**data.__dict__) - if 1020 in codes or 1021 in codes or 1022 in codes: - self._check_naming_conventions(**data.__dict__) - if any(isinstance(num, int) and 1030 <= num <= 1037 for num in codes): - self._check_formatting_conventions(**data.__dict__) - if any(isinstance(num, int) and 1040 <= num <= 1044 for num in codes): - self._check_sboterm_annotations(**data.__dict__) - - # self._check_empty_kinetics(**kwargs) - # self._check_boundary_floating_species(**kwargs) - - def _get_sorted_species(self, reaction): - sorted_species_reference = [reaction.getReactant(n) for n in range( - reaction.getNumReactants())] + [reaction.getProduct(n) for n in range(reaction.getNumProducts())] - sorted_species = [] - for species_reference in sorted_species_reference: - sorted_species.append(species_reference.getSpecies()) - return sorted_species - - def _preprocess_reactions(self, reaction): - # Extract and process necessary data from the reaction - species_num = self.model.getNumSpecies() - parameter_num = self.model.getNumParameters() - compartment_num = self.model.getNumCompartments() - - species_list = [self.model.getSpecies( - i).getId() for i in range(species_num)] - parameter_list = [self.model.getParameter( - i).getId() for i in range(parameter_num)] - compartment_list = [self.model.getCompartment( - i).getId() for i in range(compartment_num)] - - kinetics = reaction.kinetic_law.expanded_formula - + self.data.default_classifications = {} + self.data.custom_classifications = {} + self.data.results.clear_results() + self.data.errors = [] try: - kinetics_sim = str(sp.simplify(kinetics)) - except: - kinetics_sim = kinetics - - return species_list, parameter_list, compartment_list, kinetics, kinetics_sim - - def _extract_kinetics_details(self, reaction): - reactant_list = [r.getSpecies() for r in reaction.reactants] - product_list = [p.getSpecies() for p in reaction.products] - return reactant_list, product_list - - def _identify_parameters_in_kinetics(self, ids_list, species_list, parameter_list, compartment_list): - species_in_kinetic_law = [] - parameters_in_kinetic_law_only = [] - compartment_in_kinetic_law = [] - others_in_kinetic_law = [] - - for id in ids_list: - if id in species_list: - species_in_kinetic_law.append(id) - elif id in parameter_list: - parameters_in_kinetic_law_only.append(id) - elif id in compartment_list: - compartment_in_kinetic_law.append(id) - others_in_kinetic_law.append(id) - else: - others_in_kinetic_law.append(id) - - return species_in_kinetic_law, parameters_in_kinetic_law_only, compartment_in_kinetic_law, others_in_kinetic_law - - def _get_boundary_species(self, reactant_list, product_list): - boundary_species = [reactant for reactant in reactant_list if self.model.getSpecies( - reactant).getBoundaryCondition()] - boundary_species += [product for product in product_list if self.model.getSpecies( - product).getBoundaryCondition()] - return boundary_species - - def _get_non_constant_params(self, parameters_in_kinetic_law_only): - non_constant_params = [] - for param in parameters_in_kinetic_law_only: - libsbml_param = self.model.getParameter(param) - if not libsbml_param.getConstant(): - non_constant_params.append(param) - return non_constant_params - + for data in self.data.reactions: + data.__dict__["codes"] = codes + # if any code is related to classification, classify the rate law + if any(code in CLASSIFICATION_RELATED_CHECKS for code in codes): + self._set_kinetics_type(**data.__dict__) + if 1 in codes: + self._check_empty_kinetics(**data.__dict__) + if 2 in codes: + self._check_floating_species(**data.__dict__) + if 1001 in codes: + self._check_pure_number(**data.__dict__) + if 1002 in codes: + self._check_unrecognized_rate_law(**data.__dict__) + if 1003 in codes: + self._check_flux_increasing_with_reactant(**data.__dict__) + if 1004 in codes: + self._check_flux_decreasing_with_product(**data.__dict__) + if 1005 in codes: + self._check_boundary_floating_species(**data.__dict__) + if 1006 in codes: + self._check_constant_parameters(**data.__dict__) + if 1010 in codes: + self._check_irreversibility(**data.__dict__) + if 1020 in codes or 1021 in codes or 1022 in codes: + self._check_naming_conventions(**data.__dict__) + if any(isinstance(num, int) and 1030 <= num <= 1037 for num in codes): + self._check_formatting_conventions(**data.__dict__) + if any(isinstance(num, int) and 1040 <= num <= 1044 for num in codes): + self._check_sboterm_annotations(**data.__dict__) + except Exception as e: + self.data.errors.append(str(e)) + return "Error: " + str(e) + return "Success" + def _set_kinetics_type(self, **kwargs): reaction_id = kwargs["reaction_id"] - self.default_classifications[reaction_id] = self.default_classifier.custom_classify( + self.data.default_classifications[reaction_id] = self.data.default_classifier.custom_classify( is_default=True, **kwargs) - if self.custom_classifier: - self.custom_classifications[reaction_id] = self.custom_classifier.custom_classify( + if self.data.custom_classifier: + self.data.custom_classifications[reaction_id] = self.data.custom_classifier.custom_classify( **kwargs) def _check_empty_kinetics(self, **kwargs): @@ -430,7 +293,7 @@ def _check_empty_kinetics(self, **kwargs): reaction_id = kwargs["reaction_id"] kinetics = kwargs["kinetics"] if len(kinetics.replace(' ', '')) == 0: - self.results.add_message( + self.data.results.add_message( reaction_id, 1, f"No rate law entered.", False) def _check_floating_species(self, **kwargs): @@ -459,7 +322,7 @@ def _check_floating_species(self, **kwargs): floating_species.append(reactant) if len(floating_species) > 0: floating_species = ",".join(floating_species) - self.results.add_message( + self.data.results.add_message( reaction_id, 2, f"Expecting reactants in rate law: {floating_species}", False) def _check_pure_number(self, **kwargs): @@ -478,7 +341,7 @@ def _check_pure_number(self, **kwargs): kinetics_sim = kwargs["kinetics_sim"] try: float(kinetics_sim) - self.results.add_message( + self.data.results.add_message( reaction_id, 1001, "Rate law contains only number.") except: return @@ -495,13 +358,13 @@ def _check_unrecognized_rate_law(self, **kwargs): A warning message to results specifying that the rate law is unrecognized. """ reaction_id = kwargs["reaction_id"] - if len(self.custom_classifications) > 0: - if not (any(self.default_classifications[reaction_id].values()) or any(self.custom_classifications[reaction_id].values())): - self.results.add_message( + if len(self.data.custom_classifications) > 0: + if not (any(self.data.default_classifications[reaction_id].values()) or any(self.data.custom_classifications[reaction_id].values())): + self.data.results.add_message( reaction_id, 1002, "Unrecognized rate law from the standard list and the custom list.") else: - if not any(self.default_classifications[reaction_id].values()): - self.results.add_message( + if not any(self.data.default_classifications[reaction_id].values()): + self.data.results.add_message( reaction_id, 1002, "Unrecognized rate law from the standard list.") def _check_flux_increasing_with_reactant(self, **kwargs): @@ -520,9 +383,15 @@ def _check_flux_increasing_with_reactant(self, **kwargs): reaction_id = kwargs["reaction_id"] reactant_list = kwargs["reactant_list"] kinetics = kwargs["kinetics"] + + # first check if kinetics can be sympified, TODO: support functions in MathML + try: + sympified_kinetics = sp.sympify(kinetics) + except: + return - if not util.check_symbols_derivative(sp.sympify(kinetics), reactant_list): - self.results.add_message( + if not util.check_symbols_derivative(sympified_kinetics, reactant_list): + self.data.results.add_message( reaction_id, 1003, "Flux is not increasing as reactant increases.") def _check_flux_decreasing_with_product(self, **kwargs): @@ -543,11 +412,16 @@ def _check_flux_decreasing_with_product(self, **kwargs): is_reversible = kwargs["is_reversible"] product_list = kwargs["product_list"] kinetics = kwargs["kinetics"] - + + # first check if kinetics can be sympified, TODO: support functions in MathML if is_reversible: - if not util.check_symbols_derivative(sp.sympify(kinetics), product_list, False): - self.results.add_message( - reaction_id, 1004, "Flux is not decreasing as product increases.") + try: + sympified_kinetics = sp.sympify(kinetics) + if not util.check_symbols_derivative(sympified_kinetics, product_list, False): + self.data.results.add_message( + reaction_id, 1004, "Flux is not decreasing as product increases.") + except: + return def _check_boundary_floating_species(self, **kwargs): """ @@ -575,7 +449,7 @@ def _check_boundary_floating_species(self, **kwargs): boundary_floating_species.append(reactant) if len(boundary_floating_species) > 0: boundary_floating_species = ",".join(boundary_floating_species) - self.results.add_message( + self.data.results.add_message( reaction_id, 1005, f"Expecting boundary species reactant in rate law: {boundary_floating_species}") def _check_constant_parameters(self, **kwargs): @@ -600,7 +474,7 @@ def _check_constant_parameters(self, **kwargs): non_constant_params_in_kinetic_law.append(param) if len(non_constant_params_in_kinetic_law) > 0: non_constant_params_in_kinetic_law = ",".join(non_constant_params_in_kinetic_law) - self.results.add_message( + self.data.results.add_message( reaction_id, 1006, f"Expecting these parameters to be constants: {non_constant_params_in_kinetic_law}") def _check_irreversibility(self, **kwargs): @@ -629,7 +503,7 @@ def _check_irreversibility(self, **kwargs): inconsistent_products.append(product) if len(inconsistent_products) > 0: inconsistent_products = ",".join(inconsistent_products) - self.results.add_message( + self.data.results.add_message( reaction_id, 1010, f"Irreversible reaction kinetic law contains products: {inconsistent_products}") def _check_naming_conventions(self, **kwargs): @@ -644,50 +518,50 @@ def _check_naming_conventions(self, **kwargs): A warning message to results specifying that certain parameters in the rate law are not following the recommended naming convention. """ reaction_id = kwargs["reaction_id"] - parameters_in_kinetic_law = kwargs["parameters_in_kinetic_law"] + parameters_in_kinetic_law_only = kwargs["parameters_in_kinetic_law_only"] kinetics_sim = kwargs["kinetics_sim"] ids_list = kwargs["ids_list"] codes = kwargs["codes"] naming_convention_warnings = {'k': [], 'K': [], 'V': []} - if any(self.default_classifications[reaction_id][key] for key in NON_MM_KEYS): + if any(self.data.default_classifications[reaction_id][key] for key in NON_MM_KEYS): naming_convention_warnings['k'] = self._check_symbols_start_with( - 'k', parameters_in_kinetic_law) - elif self.default_classifications[reaction_id]['MM']: + 'k', parameters_in_kinetic_law_only) + elif self.data.default_classifications[reaction_id]['MM']: eq = self._numerator_denominator(kinetics_sim, ids_list) - eq0 = [param for param in parameters_in_kinetic_law if param in eq[0]] - eq1 = [param for param in parameters_in_kinetic_law if param in eq[1]] + eq0 = [param for param in parameters_in_kinetic_law_only if param in eq[0]] + eq1 = [param for param in parameters_in_kinetic_law_only if param in eq[1]] naming_convention_warnings['V'] = self._check_symbols_start_with( 'V', eq0) naming_convention_warnings['K'] = self._check_symbols_start_with( 'K', eq1) - elif self.default_classifications[reaction_id]['MMcat']: + elif self.data.default_classifications[reaction_id]['MMcat']: eq = self._numerator_denominator(kinetics_sim, ids_list) - eq0 = [param for param in parameters_in_kinetic_law if param in eq[0]] - eq1 = [param for param in parameters_in_kinetic_law if param in eq[1]] + eq0 = [param for param in parameters_in_kinetic_law_only if param in eq[0]] + eq1 = [param for param in parameters_in_kinetic_law_only if param in eq[1]] naming_convention_warnings['K'] = self._check_symbols_start_with('K', eq0) naming_convention_warnings['K'] = self._check_symbols_start_with('K', eq1) - elif self.default_classifications[reaction_id]['Hill']: + elif self.data.default_classifications[reaction_id]['Hill']: eq = self._numerator_denominator(kinetics_sim, ids_list) - eq0 = [param for param in parameters_in_kinetic_law if param in eq[0]] - eq1 = [param for param in parameters_in_kinetic_law if param in eq[1]] + eq0 = [param for param in parameters_in_kinetic_law_only if param in eq[0]] + eq1 = [param for param in parameters_in_kinetic_law_only if param in eq[1]] naming_convention_warnings['K'] = self._check_symbols_start_with( 'K', eq1) if 1020 in codes and len(naming_convention_warnings['k']) > 0: naming_convention_warnings_k = ",".join( naming_convention_warnings['k']) - self.results.add_message( + self.data.results.add_message( reaction_id, 1020, f"We recommend that these parameters start with 'k': {naming_convention_warnings_k}") if 1021 in codes and len(naming_convention_warnings['K']) > 0: naming_convention_warnings_K = ",".join( naming_convention_warnings['K']) - self.results.add_message( + self.data.results.add_message( reaction_id, 1021, f"We recommend that these parameters start with 'K': {naming_convention_warnings_K}") if 1022 in codes and len(naming_convention_warnings['V']) > 0: naming_convention_warnings_V = ",".join( naming_convention_warnings['V']) - self.results.add_message( + self.data.results.add_message( reaction_id, 1022, f"We recommend that these parameters start with 'V': {naming_convention_warnings_V}") def _check_symbols_start_with(self, start, symbols): @@ -810,8 +684,8 @@ def _check_formatting_conventions(self, **kwargs): sorted_species = kwargs["sorted_species"] flag = 0 - assert len(self.default_classifications[reaction_id]) > 0 - if any(self.default_classifications[reaction_id][key] for key in MM_KEYS): + assert len(self.data.default_classifications[reaction_id]) > 0 + if any(self.data.default_classifications[reaction_id][key] for key in MM_KEYS): eq = self._numerator_denominator_order_remained( kinetics, ids_list) # _numerator_denominator not provided flag = self._check_expression_format( @@ -822,29 +696,29 @@ def _check_formatting_conventions(self, **kwargs): flag = self._check_expression_format( kinetics, compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species) if flag == 1 and 1030 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1030, f"Elements of the same type are not ordered alphabetically") if flag == 2 and 1031 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1031, f"Formatting convention not followed (compartment before parameters before species)") # TODO: currently the default classification does not classify these as MM, so these checks are not performed if flag == 3 and 1032 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1032, f"Denominator not in alphabetical order") if flag == 4 and 1033 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1033, f"Numerator and denominator not in alphabetical order") if flag == 5 and 1034 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1034, f"Numerator convention not followed and denominator not in alphabetical order") # if flag == 6 and 1035 in codes: - # self.results.add_message( + # self.data.results.add_message( # reaction_id, 1035, f"Denominator convention not followed") # if flag == 7 and 1036 in codes: - # self.results.add_message( + # self.data.results.add_message( # reaction_id, 1036, f"Numerator not in alphabetical order and denominator convention not followed") # if flag == 8 and 1037 in codes: - # self.results.add_message( + # self.data.results.add_message( # reaction_id, 1037, f"Numerator and denominator convention not followed") def _check_sboterm_annotations(self, **kwargs): @@ -862,40 +736,40 @@ def _check_sboterm_annotations(self, **kwargs): reaction_id = kwargs["reaction_id"] sbo_term = kwargs["sbo_term"] codes = kwargs["codes"] - assert len(self.default_classifications) > 0 + assert len(self.data.default_classifications) > 0 flag = 0 if sbo_term < 0: flag = 0 - elif any(self.default_classifications[reaction_id][key] for key in UNDR_KEYS): + elif any(self.data.default_classifications[reaction_id][key] for key in UNDR_KEYS): if sbo_term not in UNDR_SBOS: flag = 1 - elif any(self.default_classifications[reaction_id][key] for key in UNDR_A_KEYS): + elif any(self.data.default_classifications[reaction_id][key] for key in UNDR_A_KEYS): if sbo_term not in UNDR_A_SBOS: flag = 2 - elif any(self.default_classifications[reaction_id][key] for key in BIDR_ALL_KEYS): + elif any(self.data.default_classifications[reaction_id][key] for key in BIDR_ALL_KEYS): if sbo_term not in BI_SBOS: flag = 3 - elif self.default_classifications[reaction_id]['MM']: + elif self.data.default_classifications[reaction_id]['MM']: if sbo_term not in MM_SBOS: flag = 4 - elif any(self.default_classifications[reaction_id][key] for key in MM_CAT_KEYS): + elif any(self.data.default_classifications[reaction_id][key] for key in MM_CAT_KEYS): if sbo_term not in MM_CAT_SBOS: flag = 5 - # elif self.default_classifications[reaction_id]['Hill']: + # elif self.data.default_classifications[reaction_id]['Hill']: # if sbo_term not in HILL_SBOS: # flag = 6 if flag == 1 and 1040 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1040, f"Uni-directional mass action annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000430, SBO_0000041") elif flag == 2 and 1041 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1041, f"Uni-Directional Mass Action with an Activator annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000041") elif flag == 3 and 1042 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1042, f"Bi-directional mass action (with an Activator) annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000042") elif flag == 4 and 1043 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1043, f"Michaelis-Menten kinetics without an explicit enzyme annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000028") elif flag == 5 and 1044 in codes: - self.results.add_message( + self.data.results.add_message( reaction_id, 1044, f"Michaelis-Menten kinetics with an explicit enzyme annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000028, SBO_0000430") diff --git a/ratesb_python/common/custom_classifier.py b/ratesb_python/common/custom_classifier.py index 4c6d305..0164a98 100644 --- a/ratesb_python/common/custom_classifier.py +++ b/ratesb_python/common/custom_classifier.py @@ -12,6 +12,9 @@ import re import sympy +# timeout if rate law is too complex +TIMEOUT = 10000 + class _CustomClassifier: """Custom Classifier for rate laws. @@ -208,17 +211,22 @@ def custom_classify(self, is_default = False, **kwargs): kinetics_expression = item['expression'].replace("^", "**") optional_symbols = item['optional_symbols'] all_expr = self.get_all_expr(kinetics_expression, optional_symbols) + all_expr = [sympy.sympify(expr) for expr in all_expr] power_limited_species = item['power_limited_species'] classified_true = False - for replaced_kinetics in replaced_kinetics_list: - replaced_kinetics_sympify = self.lower_powers(sympy.sympify(replaced_kinetics), power_limited_species) - replaced_kinetics_sympify = self.remove_constant_multiplier(replaced_kinetics_sympify) - comparison_result = any(util.check_equal(sympy.simplify(expr), replaced_kinetics_sympify) for expr in all_expr) - if comparison_result: - ret[item['name']] = True - classified_true = True - any_true = True - break + try: + for replaced_kinetics in replaced_kinetics_list: + replaced_kinetics_sympify = self.lower_powers(sympy.sympify(replaced_kinetics), power_limited_species) + replaced_kinetics_sympify = self.remove_constant_multiplier(replaced_kinetics_sympify) + comparison_result = any(util.check_equal(expr, replaced_kinetics_sympify) for expr in all_expr) + if comparison_result: + ret[item['name']] = True + classified_true = True + any_true = True + break + except: + ret[item['name']] = False + continue if not classified_true: ret[item['name']] = False return ret diff --git a/ratesb_python/common/reaction_data.py b/ratesb_python/common/reaction_data.py new file mode 100644 index 0000000..8c9f29a --- /dev/null +++ b/ratesb_python/common/reaction_data.py @@ -0,0 +1,310 @@ +from dataclasses import dataclass +import json +import sys +import os +current_dir = os.path.abspath(os.path.dirname(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sys.path.append(current_dir) +sys.path.append(parent_dir) + +from custom_classifier import _CustomClassifier +# from SBMLKinetics.common.simple_sbml import SimpleSBML +# from SBMLKinetics.common.reaction import Reaction +from typing import List +from SBMLKinetics.common.simple_sbml import SimpleSBML +from common import util +from results import Results + +import antimony +import libsbml +import os +import sympy as sp + +sp + +from typing import List + +ZERO = "ZERO" +UNDR1 = "UNDR1" +UNDR2 = "UNDR2" +UNDR3 = "UNDR3" +UNDR_A1 = "UNDR-A1" +UNDR_A2 = "UNDR-A2" +UNDR_A3 = "UNDR-A3" +BIDR11 = "BIDR11" +BIDR12 = "BIDR12" +BIDR21 = "BIDR21" +BIDR22 = "BIDR22" +BIDR_A11 = "BIDR-A11" +BIDR_A12 = "BIDR-A12" +BIDR_A21 = "BIDR-A21" +BIDR_A22 = "BIDR-A22" +MM = "MM" +MM_CAT = "MMcat" +AMM = "AMM" +IMM = "IMM" +RMM = "RMM" +RMM_CAT = "RMMcat" +HILL = "Hill" + +NON_MM_KEYS = [ + ZERO, UNDR1, UNDR2, UNDR3, UNDR_A1, UNDR_A2, UNDR_A3, + BIDR11, BIDR12, BIDR21, BIDR22, BIDR_A11, BIDR_A12, BIDR_A21, BIDR_A22 +] + +MM_KEYS = [MM, MM_CAT, AMM, IMM, RMM, RMM_CAT] + +UNDR_KEYS = [UNDR1, UNDR2, UNDR3] + +UNDR_A_KEYS = [UNDR_A1, UNDR_A2, UNDR_A3] + +BIDR_ALL_KEYS = [BIDR11, BIDR12, BIDR21, BIDR22, + BIDR_A11, BIDR_A12, BIDR_A21, BIDR_A22] + +MM_CAT_KEYS = [MM_CAT, AMM, IMM, RMM_CAT] + +UNDR_SBOS = [41, 43, 44, 45, 47, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 140, 141, 142, 143, 144, 145, 146, 163, 166, 333, 560, 561, 562, 563, 564, 430, 270, 458, + 275, 273, 379, 440, 443, 451, 454, 456, 260, 271, 378, 387, 262, 265, 276, 441, 267, 274, 444, 452, 453, 455, 457, 386, 388, 442, 277, 445, 446, 447, 448, 266, 449, 450] + +UNDR_A_SBOS = [41, 43, 44, 45, 47, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 140, 141, 142, 143, 144, 145, 146, 163, 166, 333, 560, 561, 562, 563, 564] + +BI_SBOS = [42, 69, 78, 88, 109, 646, 70, 71, 74, 79, 80, 81, 84, 89, 99, 110, 120, 130, 72, 73, 75, 76, 77, 82, 83, 85, 86, 87, 90, 91, 92, 95, 100, 101, 102, 105, 111, 112, + 113, 116, 121, 122, 123, 126, 131, 132, 133, 136, 93, 94, 96, 97, 98, 103, 104, 106, 107, 108, 114, 115, 117, 118, 119, 124, 125, 127, 128, 129, 134, 135, 137, 138, 139] + +MM_SBOS = [28, 29, 30, 31, 199] + +MM_CAT_SBOS = [28, 29, 30, 31, 199, 430, 270, 458, 275, 273, 379, 440, 443, 451, 454, 456, 260, 271, 378, 387, + 262, 265, 276, 441, 267, 274, 444, 452, 453, 455, 457, 386, 388, 442, 277, 445, 446, 447, 448, 266, 449, 450] + +HILL_SBOS = [192, 195, 198] + +ALL_CHECKS = [] +ERROR_CHECKS = [] +WARNING_CHECKS = [] +for i in range(1, 3): + ALL_CHECKS.append(i) + ERROR_CHECKS.append(i) +for i in range(1001, 1007): + ALL_CHECKS.append(i) + WARNING_CHECKS.append(i) +for i in range(1010, 1011): + ALL_CHECKS.append(i) + WARNING_CHECKS.append(i) +for i in range(1020, 1023): + ALL_CHECKS.append(i) + WARNING_CHECKS.append(i) +for i in range(1030, 1038): + ALL_CHECKS.append(i) + WARNING_CHECKS.append(i) +for i in range(1040, 1045): + ALL_CHECKS.append(i) + WARNING_CHECKS.append(i) + +messages_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "messages.json") +with open(messages_path) as file: + MESSAGES = json.load(file) + +@dataclass +class ReactionData: + reaction_id: str + kinetics: str + kinetics_sim: str + reactant_list: List[str] + product_list: List[str] + species_in_kinetic_law: List[str] + parameters_in_kinetic_law: List[str] + ids_list: List[str] + sorted_species: List[str] + boundary_species: List[str] + parameters_in_kinetic_law_only: List[str] + compartment_in_kinetic_law: List[str] + is_reversible: bool + sbo_term: int + codes: List[int] + non_constant_params: List[str] + + +class AnalyzerData: + def __init__(self, model_str: str, rate_law_classifications_path: str=None): + """ + Initializes the AnalyzerData object. + + Args: + model_str (str): Path to the model file, or the string representation of model. + rate_law_classifications_path (str): Path to the rate law classification file. + customized rate law classification. + + Examples: + import Analyzer from ratesb_python.common.analyzer + analyzer = Analyzer("path/to/biomodel.xml", "path/to/rate_laws.json") + analyzer.check_all() + results = analyzer.results + print(str(results)) + str(results) + """ + # check if parameters are strings + if not isinstance(model_str, str): + raise ValueError("Invalid model_str format, should be string.") + if rate_law_classifications_path and not isinstance(rate_law_classifications_path, str): + raise ValueError("Invalid rate_law_classifications_path format, should be string.") + # check if the model_str is sbml + if ' 0: + xml = antimony.getSBMLString() + elif model_str.endswith('.ant') or model_str.endswith('.txt') or model_str.endswith('.xml'): + # model_str is path to model + xml = '' + if model_str.endswith('.ant') or model_str.endswith('.txt'): + ant = util.get_model_str(model_str, False) + load_int = antimony.loadAntimonyString(ant) + if load_int > 0: + xml = antimony.getSBMLString() + else: + raise ValueError("Invalid Antimony model.") + else: + xml = util.get_model_str(model_str, True) + else: + raise ValueError("Invalid model_str format, should be SBML or Antimony string, or path to model file.") + reader = libsbml.SBMLReader() + document = reader.readSBMLFromString(xml) + util.checkSBMLDocument(document) + self.model = document.getModel() + self.simple = SimpleSBML(self.model) + self.custom_classifier = None + self.default_classifications = {} + self.custom_classifications = {} + self.results = Results() + self.errors = [] + default_classifier_path = os.path.join(current_dir, "default_classifier.json") + self.default_classifier = _CustomClassifier(default_classifier_path) + + if rate_law_classifications_path: + self.custom_classifier = _CustomClassifier( + rate_law_classifications_path) + if len(self.custom_classifier.warning_message) > 0: + print(self.custom_classifier.warning_message) + + self.reactions = [] + for reaction in self.simple.reactions: + reaction.kinetic_law.formula = reaction.kinetic_law.formula.replace( + '^', '**') + ids_list = list(dict.fromkeys(reaction.kinetic_law.symbols)) + + libsbml_kinetics = reaction.kinetic_law.libsbml_kinetics + sbo_term = -1 + if libsbml_kinetics: + sbo_term = libsbml_kinetics.getSBOTerm() + + reaction_id = reaction.getId() + sorted_species = self._get_sorted_species(reaction.reaction) + species_list, parameter_list, compartment_list, kinetics, kinetics_sim = self._preprocess_reactions( + reaction) + reactant_list, product_list = self._extract_kinetics_details( + reaction) + species_in_kinetic_law, parameters_in_kinetic_law_only, compartment_in_kinetic_law, others_in_kinetic_law = self._identify_parameters_in_kinetics( + ids_list, species_list, parameter_list, compartment_list) + boundary_species = self._get_boundary_species( + reactant_list, product_list) + non_constant_params = self._get_non_constant_params(parameters_in_kinetic_law_only) + + is_reversible = reaction.reaction.getReversible() + + codes = [] + + data = ReactionData( + reaction_id=reaction_id, + kinetics=kinetics, + kinetics_sim=kinetics_sim, + reactant_list=reactant_list, + product_list=product_list, + species_in_kinetic_law=species_in_kinetic_law, + parameters_in_kinetic_law=parameters_in_kinetic_law_only + others_in_kinetic_law, + ids_list=ids_list, + sorted_species=sorted_species, + boundary_species=boundary_species, + parameters_in_kinetic_law_only=parameters_in_kinetic_law_only, + compartment_in_kinetic_law=compartment_in_kinetic_law, + is_reversible=is_reversible, + sbo_term=sbo_term, + codes=codes, + non_constant_params=non_constant_params + ) + + self.reactions.append(data) + + def _get_sorted_species(self, reaction): + sorted_species_reference = [reaction.getReactant(n) for n in range( + reaction.getNumReactants())] + [reaction.getProduct(n) for n in range(reaction.getNumProducts())] + sorted_species = [] + for species_reference in sorted_species_reference: + sorted_species.append(species_reference.getSpecies()) + return sorted_species + + def _preprocess_reactions(self, reaction): + # Extract and process necessary data from the reaction + species_num = self.model.getNumSpecies() + parameter_num = self.model.getNumParameters() + compartment_num = self.model.getNumCompartments() + + species_list = [self.model.getSpecies( + i).getId() for i in range(species_num)] + parameter_list = [self.model.getParameter( + i).getId() for i in range(parameter_num)] + compartment_list = [self.model.getCompartment( + i).getId() for i in range(compartment_num)] + + kinetics = reaction.kinetic_law.expanded_formula + + try: + kinetics_sim = str(sp.simplify(kinetics)) + except: + kinetics_sim = kinetics + + return species_list, parameter_list, compartment_list, kinetics, kinetics_sim + + def _extract_kinetics_details(self, reaction): + reactant_list = [r.getSpecies() for r in reaction.reactants] + product_list = [p.getSpecies() for p in reaction.products] + return reactant_list, product_list + + def _identify_parameters_in_kinetics(self, ids_list, species_list, parameter_list, compartment_list): + species_in_kinetic_law = [] + parameters_in_kinetic_law_only = [] + compartment_in_kinetic_law = [] + others_in_kinetic_law = [] + + for id in ids_list: + if id in species_list: + species_in_kinetic_law.append(id) + elif id in parameter_list: + parameters_in_kinetic_law_only.append(id) + elif id in compartment_list: + compartment_in_kinetic_law.append(id) + others_in_kinetic_law.append(id) + else: + others_in_kinetic_law.append(id) + + return species_in_kinetic_law, parameters_in_kinetic_law_only, compartment_in_kinetic_law, others_in_kinetic_law + + def _get_boundary_species(self, reactant_list, product_list): + boundary_species = [reactant for reactant in reactant_list if self.model.getSpecies( + reactant).getBoundaryCondition()] + boundary_species += [product for product in product_list if self.model.getSpecies( + product).getBoundaryCondition()] + return boundary_species + + def _get_non_constant_params(self, parameters_in_kinetic_law_only): + non_constant_params = [] + for param in parameters_in_kinetic_law_only: + libsbml_param = self.model.getParameter(param) + if not libsbml_param.getConstant(): + non_constant_params.append(param) + return non_constant_params \ No newline at end of file diff --git a/ratesb_python/common/results.py b/ratesb_python/common/results.py index 423fb6c..ef3f1a9 100644 --- a/ratesb_python/common/results.py +++ b/ratesb_python/common/results.py @@ -111,6 +111,8 @@ def __repr__(self): Returns: str: A string representation of the results. """ + if self.count_messages() == 0: + return 'No errors or warnings found.' str_repr = '' for reaction_name, messages in self.results.items(): str_repr += f'{reaction_name}:\n' diff --git a/ratesb_python/common/util.py b/ratesb_python/common/util.py index bda8b01..dc850ff 100644 --- a/ratesb_python/common/util.py +++ b/ratesb_python/common/util.py @@ -45,7 +45,7 @@ def checkSBMLDocument(document): if (document.getNumErrors() > 0): print("SBML Document Error") -def check_equal(expr1, expr2, n=10, sample_min=1, sample_max=10): +def check_equal(expr1, expr2, n=4, sample_min=1, sample_max=10): """check if two sympy expressions are equal by plugging random numbers into each symbols in both expressions and test if they are equal @@ -60,22 +60,22 @@ def check_equal(expr1, expr2, n=10, sample_min=1, sample_max=10): bool: if the two expressions are equal """ # Regroup all free symbols from both expressions - free_symbols = set(expr1.free_symbols) | set(expr2.free_symbols) + free_symbols = list(set(expr1.free_symbols) | set(expr2.free_symbols)) - # Numeric (brute force) equality testing n-times - prev_frac = None + # Precompile expressions to numerical functions for faster evaluation + expr1_func = sp.lambdify(free_symbols, expr1, "numpy") + expr2_func = sp.lambdify(free_symbols, expr2, "numpy") + for i in range(n): your_values = [random.uniform(sample_min, sample_max) for _ in range(len(free_symbols))] - expr1_num=expr1 - expr2_num=expr2 - for symbol,number in zip(free_symbols, your_values): - expr1_num=expr1_num.subs(symbol, sp.Float(number)) - expr2_num=expr2_num.subs(symbol, sp.Float(number)) - expr1_num=float(expr1_num) - expr2_num=float(expr2_num) - if not math.isclose(expr1_num, expr2_num) and (prev_frac is not None and prev_frac != expr1_num/expr2_num): + + # Evaluate both expressions with the generated values + expr1_num = expr1_func(*your_values) + expr2_num = expr2_func(*your_values) + + # Check for numerical closeness + if not math.isclose(expr1_num, expr2_num, rel_tol=1e-9): return False - prev_frac = expr1_num/expr2_num return True def check_symbols_derivative(expr, symbols, is_positive_derivative=True): @@ -101,12 +101,29 @@ def check_symbols_derivative(expr, symbols, is_positive_derivative=True): else: prev = math.inf for i in range(1, 1001, 10): - curr = sp.Float(temp_expr.subs(symbol, sp.Float(i/100))) - if is_positive_derivative: - if curr <= prev: - return False - else: - if curr >= prev: - return False - prev = curr - return True \ No newline at end of file + try: + curr = sp.Float(temp_expr.subs(symbol, sp.Float(i/100))) + if is_positive_derivative: + if curr <= prev: + return False + else: + if curr >= prev: + return False + prev = curr + except: + return False + return True + + +def substitute_sympy_builtin_symbols_with_underscore(symbol): + """ + Substitute sympy built-in symbols with underscored symbols + This method recognizes which symbols are built-in sympy symbols and substitutes them with underscored symbols + + Args: + symbol (sympy.Symbol): symbol to substitute + + Returns: + str: symbol name with underscore in front of the symbol + """ + \ No newline at end of file diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py index de16933..74fe024 100644 --- a/tests/test_analyzer.py +++ b/tests/test_analyzer.py @@ -145,15 +145,15 @@ def test_check_0001(self): true_case_analyzer.checks([1]) false_case_analyzer.checks([1]) # self.assertEqual(self.rate_analyzer.classification_cp, []) - self.assertEqual(str(true_case_analyzer.results), '') - self.assertEqual(str(false_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') + self.assertEqual(str(false_case_analyzer.results), 'No errors or warnings found.') def test_check_0002(self): true_case_analyzer = Analyzer(TRUE_PATH_2) false_case_analyzer = Analyzer(FALSE_PATH_2) true_case_analyzer.checks([2]) false_case_analyzer.checks([2]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Error 0002: Expecting reactants in rate law: a\n') def test_check_1001(self): @@ -161,7 +161,7 @@ def test_check_1001(self): false_case_analyzer = Analyzer(FALSE_PATH_1001) true_case_analyzer.checks([1001]) false_case_analyzer.checks([1001]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1001: Rate law contains only number.\n') def test_check_1002(self): @@ -169,7 +169,7 @@ def test_check_1002(self): false_case_analyzer = Analyzer(FALSE_PATH_1002) true_case_analyzer.checks([1002]) false_case_analyzer.checks([1002]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1002: Unrecognized rate law from the standard list.\n') false_case_2_analyzer = Analyzer(FALSE_PATH_1002, REVERSIBLE_MM_PATH) false_case_2_analyzer.checks([1002]) @@ -180,7 +180,7 @@ def test_check_1003(self): false_case_analyzer = Analyzer(FALSE_PATH_1003) true_case_analyzer.checks([1003]) false_case_analyzer.checks([1003]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1003: Flux is not increasing as reactant increases.\n_J1:\n Warning 1003: Flux is not increasing as reactant increases.\n_J2:\n Warning 1003: Flux is not increasing as reactant increases.\n') def test_check_1004(self): @@ -188,7 +188,7 @@ def test_check_1004(self): false_case_analyzer = Analyzer(FALSE_PATH_1004) true_case_analyzer.checks([1004]) false_case_analyzer.checks([1004]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1004: Flux is not decreasing as product increases.\n') def test_check_1005(self): @@ -196,7 +196,7 @@ def test_check_1005(self): false_case_analyzer = Analyzer(FALSE_PATH_1005) true_case_analyzer.checks([1005]) false_case_analyzer.checks([1005]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1005: Expecting boundary species reactant in rate law: a\n') def test_check_1006(self): @@ -204,7 +204,7 @@ def test_check_1006(self): false_case_analyzer = Analyzer(FALSE_PATH_1006) true_case_analyzer.checks([1006]) false_case_analyzer.checks([1006]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1006: Expecting these parameters to be constants: k1\n') def test_check_1010(self): @@ -212,7 +212,7 @@ def test_check_1010(self): false_case_analyzer = Analyzer(FALSE_PATH_1010) true_case_analyzer.checks([1010]) false_case_analyzer.checks([1010]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), '_J0:\n Warning 1010: Irreversible reaction kinetic law contains products: b\n') def test_check_1020(self): @@ -220,7 +220,7 @@ def test_check_1020(self): false_case_analyzer = Analyzer(FALSE_PATH_1020) true_case_analyzer.checks([1020]) false_case_analyzer.checks([1020]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1020: We recommend that these parameters start with 'k': v1\n_J1:\n Warning 1020: We recommend that these parameters start with 'k': K1\n_J2:\n Warning 1020: We recommend that these parameters start with 'k': K1\n_J3:\n Warning 1020: We recommend that these parameters start with 'k': v1\n") def test_check_1021(self): @@ -228,7 +228,7 @@ def test_check_1021(self): false_case_analyzer = Analyzer(FALSE_PATH_1021) true_case_analyzer.checks([1021]) false_case_analyzer.checks([1021]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1021: We recommend that these parameters start with 'K': km\n_J1:\n Warning 1021: We recommend that these parameters start with 'K': km\n_J2:\n Warning 1021: We recommend that these parameters start with 'K': k3\n") def test_check_1022(self): @@ -236,7 +236,7 @@ def test_check_1022(self): false_case_analyzer = Analyzer(FALSE_PATH_1022) true_case_analyzer.checks([1022]) false_case_analyzer.checks([1022]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1022: We recommend that these parameters start with 'V': vm\n") def test_check_1030(self): @@ -244,7 +244,7 @@ def test_check_1030(self): false_case_analyzer = Analyzer(FALSE_PATH_1030) true_case_analyzer.checks([1030]) false_case_analyzer.checks([1030]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1030: Elements of the same type are not ordered alphabetically\n_J1:\n Warning 1030: Elements of the same type are not ordered alphabetically\n") def test_check_1031(self): @@ -252,7 +252,7 @@ def test_check_1031(self): false_case_analyzer = Analyzer(FALSE_PATH_1031) true_case_analyzer.checks([1031]) false_case_analyzer.checks([1031]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1031: Formatting convention not followed (compartment before parameters before species)\n_J1:\n Warning 1031: Formatting convention not followed (compartment before parameters before species)\n") # TODO: implement convention checks for fractional rate laws @@ -261,7 +261,7 @@ def test_check_1032(self): false_case_analyzer = Analyzer(FALSE_PATH_1032) true_case_analyzer.checks([1032]) false_case_analyzer.checks([1032]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1032: Denominator not in alphabetical order\n") def test_check_1033(self): @@ -269,7 +269,7 @@ def test_check_1033(self): false_case_analyzer = Analyzer(FALSE_PATH_1033) true_case_analyzer.checks([1033]) false_case_analyzer.checks([1033]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1033: Numerator and denominator not in alphabetical order\n") def test_check_1034(self): @@ -277,7 +277,7 @@ def test_check_1034(self): false_case_analyzer = Analyzer(FALSE_PATH_1034) true_case_analyzer.checks([1034]) false_case_analyzer.checks([1034]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1034: Numerator convention not followed and denominator not in alphabetical order\n") # def test_check_1035(self): @@ -285,7 +285,7 @@ def test_check_1034(self): # false_case_analyzer = Analyzer(FALSE_PATH_1035) # true_case_analyzer.checks([1035]) # false_case_analyzer.checks([1035]) - # self.assertEqual(str(true_case_analyzer.results), '') + # self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') # self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1035: Denominator convention not followed\n") # def test_check_1036(self): @@ -293,7 +293,7 @@ def test_check_1034(self): # false_case_analyzer = Analyzer(FALSE_PATH_1036) # true_case_analyzer.checks([1036]) # false_case_analyzer.checks([1036]) - # # self.assertEqual(str(true_case_analyzer.results), '') + # # self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') # self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1036: Numerator not in alphabetical order and denominator convention not followed\n") # def test_check_1037(self): @@ -301,7 +301,7 @@ def test_check_1034(self): # false_case_analyzer = Analyzer(FALSE_PATH_1037) # true_case_analyzer.checks([1037]) # false_case_analyzer.checks([1037]) - # self.assertEqual(str(true_case_analyzer.results), '') + # self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') # self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1037: Numerator and denominator convention not followed\n") def test_check_1040(self): @@ -309,7 +309,7 @@ def test_check_1040(self): false_case_analyzer = Analyzer(FALSE_PATH_1040) true_case_analyzer.checks([1040]) false_case_analyzer.checks([1040]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1040: Uni-directional mass action annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000430, SBO_0000041\n") def test_check_1041(self): @@ -317,7 +317,7 @@ def test_check_1041(self): false_case_analyzer = Analyzer(FALSE_PATH_1041) true_case_analyzer.checks([1041]) false_case_analyzer.checks([1041]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1041: Uni-Directional Mass Action with an Activator annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000041\n") def test_check_1042(self): @@ -325,7 +325,7 @@ def test_check_1042(self): false_case_analyzer = Analyzer(FALSE_PATH_1042) true_case_analyzer.checks([1042]) false_case_analyzer.checks([1042]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1042: Bi-directional mass action (with an Activator) annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000042\n") def test_check_1043(self): @@ -333,7 +333,7 @@ def test_check_1043(self): false_case_analyzer = Analyzer(FALSE_PATH_1043) true_case_analyzer.checks([1043]) false_case_analyzer.checks([1043]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1043: Michaelis-Menten kinetics without an explicit enzyme annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000028\n") def test_check_1044(self): @@ -341,7 +341,7 @@ def test_check_1044(self): false_case_analyzer = Analyzer(FALSE_PATH_1044) true_case_analyzer.checks([1044]) false_case_analyzer.checks([1044]) - self.assertEqual(str(true_case_analyzer.results), '') + self.assertEqual(str(true_case_analyzer.results), 'No errors or warnings found.') self.assertEqual(str(false_case_analyzer.results), "_J0:\n Warning 1044: Michaelis-Menten kinetics with an explicit enzyme annotation not following recommended SBO terms, we recommend annotations to be subclasses of: SBO_0000028, SBO_0000430\n") def test_check_except(self): diff --git a/tests/test_classifier.py b/tests/test_classifier.py index 558e5a7..d3498b3 100644 --- a/tests/test_classifier.py +++ b/tests/test_classifier.py @@ -1,20 +1,5 @@ import unittest -from unittest.mock import patch -from unittest.mock import MagicMock -from unittest.mock import Mock -from unittest.mock import call -from unittest.mock import ANY import os -import math -import random - -import sympy as sp -import numpy as np -import pandas as pd -import libsbml -import antimony as sb -import json - import sys # setting path @@ -74,15 +59,15 @@ def test_json_warnings(self): def test_false(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "false.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): for k, v in val.items(): self.assertFalse(v) def test_zero(self): analyzer = Analyzer(ZERO_PATH) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[ZERO]) # assert all other values are false for k, v in val.items(): @@ -91,8 +76,8 @@ def test_zero(self): def test_undr(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "undr.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[UNDR1] or val[UNDR2] or val[UNDR3]) for k, v in val.items(): if k != UNDR1 and k != UNDR2 and k != UNDR3: @@ -100,8 +85,8 @@ def test_undr(self): def test_undr_a(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "undr_a.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[UNDR_A1] or val[UNDR_A2] or val[UNDR_A3]) for k, v in val.items(): if k != UNDR_A1 and k != UNDR_A2 and k != UNDR_A3: @@ -109,8 +94,8 @@ def test_undr_a(self): def test_bidr(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "bidr.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[BIDR11] or val[BIDR12] or val[BIDR21] or val[BIDR22]) for k, v in val.items(): if k != BIDR11 and k != BIDR12 and k != BIDR21 and k != BIDR22: @@ -118,8 +103,8 @@ def test_bidr(self): def test_bidr_a(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "bidr_a.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[BIDR_A11] or val[BIDR_A12] or val[BIDR_A21] or val[BIDR_A22]) for k, v in val.items(): if k != BIDR_A11 and k != BIDR_A12 and k != BIDR_A21 and k != BIDR_A22: @@ -127,8 +112,8 @@ def test_bidr_a(self): def test_mm(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "mm.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[MM]) for k, v in val.items(): if k != MM: @@ -136,8 +121,8 @@ def test_mm(self): def test_mmcat(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "mmcat.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[MM_CAT]) for k, v in val.items(): if k != MM_CAT: @@ -145,8 +130,8 @@ def test_mmcat(self): def test_amm(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "amm.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[AMM]) for k, v in val.items(): if k != AMM: @@ -154,8 +139,8 @@ def test_amm(self): def test_imm(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "imm.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[IMM]) for k, v in val.items(): if k != IMM: @@ -163,8 +148,8 @@ def test_imm(self): def test_rmm(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "rmm.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[RMM]) for k, v in val.items(): if k != RMM: @@ -172,8 +157,8 @@ def test_rmm(self): def test_rmmcat(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "rmmcat.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[RMM_CAT]) for k, v in val.items(): if k != RMM_CAT: @@ -181,8 +166,8 @@ def test_rmmcat(self): def test_hill(self): analyzer = Analyzer(os.path.join(DIR, TEST_CLASSIFIER_MODELS, "hill.ant")) - analyzer.checks([]) - for key, val in analyzer.default_classifications.items(): + analyzer.checks([1002]) + for key, val in analyzer.data.default_classifications.items(): self.assertTrue(val[HILL]) for k, v in val.items(): if k != HILL: