Source code for common.analyzer

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 typing import List, Optional
from common import util
from reaction_data import AnalyzerData

import os
import re
import sympy as 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]

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 = []
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)

[docs] @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]
[docs] 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
[docs] class Analyzer: """ The Analyzer class analyzes SBML models to check if the rate laws are following some rules. It uses sympy for processing mathematical symbol expressions, libsbmlpython for processing SBML models, and SBMLKinetics for analyzing SBML models and classifying rate laws. The class can return errors and warnings based on the specified checks. Attributes: results (Results): An instance of the Results class used to store and retrieve analysis results. For more details, refer to the documentation of the Results class. """
[docs] @staticmethod def list_all_checks(): """ Returns a string representation of all the checks. """ ret = "" ret += "Error checks:\n" for code in ERROR_CHECKS: ret += str(code) + ": " + Analyzer.list_check(code) + "\n" ret += "\nWarning checks:\n" for code in WARNING_CHECKS: ret += Analyzer.list_check(code) + "\n" return ret
[docs] @staticmethod def list_check(code): """ Returns a string representation of the check corresponding to the provided code. Args: code (int): The code of the check. """ ret = str(code) + ": " if code not in ALL_CHECKS: return None if code < 1000: ret += MESSAGES["errors"][str(code)] else: ret += MESSAGES["warnings"][str(code)] return ret
def __init__(self, model_str: str, rate_law_classifications_path: str=None, abort_on_complicated_rate_laws: bool=True): """ Initializes the Analyzer class. 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) """ self.data = AnalyzerData(model_str, rate_law_classifications_path) self.results = self.data.results
[docs] def check_except(self, excluded_codes: Optional[List[int]]=[]): """ Performs all checks except the ones corresponding to the provided list of error or warning codes. Args: excluded_codes (Optional[List[int]]): List of codes of the checks to exclude. If None, all checks are performed. Updates: The results of the check(s) to self.results. """ self.checks(list(set(ALL_CHECKS) - set(excluded_codes)))
[docs] def check_all(self): """ Performs all checks. Updates: The results of the check_all to self.results. """ self.check_except([])
[docs] def checks(self, codes): """ Performs multiple checks based on the provided list of error or warning codes. If no codes are provided, all checks are performed. Args: codes (List[int]): List of codes of the checks to perform. Updates: The results of the checks to self.results. """ self.data.default_classifications = {} self.data.custom_classifications = {} self.data.results.clear_results() self.data.errors = [] try: 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.data.default_classifications[reaction_id] = self.data.default_classifier.custom_classify( is_default=True, **kwargs) if self.data.custom_classifier: self.data.custom_classifications[reaction_id] = self.data.custom_classifier.custom_classify( **kwargs) def _check_empty_kinetics(self, **kwargs): """ Checks if the given reaction has an empty kinetic law. Code: 1 Args: reaction_id (str): The reaction's id'. kinetics (str): The kinetic law Adds: An error message to results specifying that no rate law was entered for the reaction. """ reaction_id = kwargs["reaction_id"] kinetics = kwargs["kinetics"] if len(kinetics.replace(' ', '')) == 0: self.data.results.add_message( reaction_id, 1, f"No rate law entered.", False) def _check_floating_species(self, **kwargs): """ Checks if all reactants in the rate law of the given reaction are defined as species, excluding boundary species. Code: 2 Args: reaction_id (str): The reaction's id'. species_in_kinetic_law (list): A list of species present in the rate law. reactant_list (list): A list of reactants for the reaction. boundary_species (list): A list of boundary species in the model. Adds: An error message to results specifying the missing reactants that are expected in the rate law. """ reaction_id = kwargs["reaction_id"] species_in_kinetic_law = kwargs["species_in_kinetic_law"] reactant_list = kwargs["reactant_list"] boundary_species = kwargs["boundary_species"] floating_species = [] for reactant in reactant_list: if reactant not in species_in_kinetic_law: if reactant not in boundary_species: floating_species.append(reactant) if len(floating_species) > 0: floating_species = ",".join(floating_species) self.data.results.add_message( reaction_id, 2, f"Expecting reactants in rate law: {floating_species}", False) def _check_pure_number(self, **kwargs): """ Checks if the rate law contains only number Code: 1001 Args: reaction_id (str): The reaction's id'. kinetics_sim: string-simplified kinetics Adds: A warning message to results specifying that the rate law contains only numbers. """ reaction_id = kwargs["reaction_id"] kinetics_sim = kwargs["kinetics_sim"] try: float(kinetics_sim) self.data.results.add_message( reaction_id, 1001, "Rate law contains only number.") except: return def _check_unrecognized_rate_law(self, **kwargs): """ Checks if the rate law from the standard and custom list (if given) is recognized Code: 1002 Args: reaction_id (str): The reaction's id'. Adds: A warning message to results specifying that the rate law is unrecognized. """ reaction_id = kwargs["reaction_id"] 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.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): """ Checks if the flux is increasing as the reactant increases. Code: 1003 Args: reaction_id (str): The reaction's id'. reactant_list (str): List of reactants in reaction kinetics (str): the kinetic law Adds: A warning message to results specifying if the flux is not increasing as reactant increases. """ reaction_id = kwargs["reaction_id"] ids_list = kwargs["ids_list"] reactant_list = kwargs["reactant_list"] kinetics = kwargs["kinetics"] if not util.check_kinetics_derivative(kinetics, ids_list, 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): """ Checks if the flux is increasing as the product decreases. Code: 1004 Args: reaction_id (str): The reaction's id'. is_reversible (bool): the reaction's reversibility product_list (list): List of products in reaction kinetics (str): the kinetic law Adds: A warning message to results specifying if the flux is not decreasing as product increases. """ reaction_id = kwargs["reaction_id"] ids_list = kwargs["ids_list"] 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: try: if not util.check_kinetics_derivative(kinetics, ids_list, product_list, is_positive_derivative=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): """ Checks if any reactant in the rate law of the given reaction is a boundary species. Code: 1005 Args: reaction_id (str): The reaction's id'. species_in_kinetic_law: A list of species present in the rate law. reactant_list: A list of reactants for the reaction. boundary_species: A list of boundary species in the model. Adds: A warning message to results specifying the boundary species reactants found in the rate law. """ reaction_id = kwargs["reaction_id"] species_in_kinetic_law = kwargs["species_in_kinetic_law"] reactant_list = kwargs["reactant_list"] boundary_species = kwargs["boundary_species"] boundary_floating_species = [] for reactant in reactant_list: if reactant not in species_in_kinetic_law: if reactant in boundary_species: boundary_floating_species.append(reactant) if len(boundary_floating_species) > 0: boundary_floating_species = ",".join(boundary_floating_species) 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): """ Checks if the parameters in the rate law are constants. Code: 1006 TODO remove libsbml dependency Args: reaction_id (str): The reaction's id'. parameters_in_kinetic_law_only (list): the parameters in kinetic law Adds: A warning message to results specifying if the parameters are not constants. """ reaction_id = kwargs["reaction_id"] parameters_in_kinetic_law_only = kwargs["parameters_in_kinetic_law_only"] non_constant_params = kwargs["non_constant_params"] non_constant_params_in_kinetic_law = [] for param in parameters_in_kinetic_law_only: if param in non_constant_params: 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.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): """ Checks if an irreversible reaction's kinetic law contains products. Code: 1010 Args: reaction_id (str): The reaction's id'. is_reversible (bool): the reaction's reversibility species_in_kinetic_law: A list of species present in the rate law. product_list: A list of products for the reaction. Returns: An error message specifying the products found in the rate law of an irreversible reaction. """ reaction_id = kwargs["reaction_id"] is_reversible = kwargs["is_reversible"] species_in_kinetic_law = kwargs["species_in_kinetic_law"] product_list = kwargs["product_list"] inconsistent_products = [] if not is_reversible: for product in product_list: if product in species_in_kinetic_law: inconsistent_products.append(product) if len(inconsistent_products) > 0: inconsistent_products = ",".join(inconsistent_products) self.data.results.add_message( reaction_id, 1010, f"Irreversible reaction kinetic law contains products: {inconsistent_products}") def _check_naming_conventions(self, **kwargs): """ Checks if certain parameters in the rate law follow the recommended naming convention (starting with 'k', 'K', or 'V'). Code: 1020, 1021, 1022 Args: reaction_id (str): The reaction's id'. Adds: 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_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.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_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_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.data.default_classifications[reaction_id]['MMcat']: eq = self._numerator_denominator(kinetics_sim, ids_list) 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.data.default_classifications[reaction_id]['Hill']: eq = self._numerator_denominator(kinetics_sim, ids_list) 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.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.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.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): ret = [] for symbol in symbols: if not symbol.startswith(start): ret.append(symbol) return ret def _numerator_denominator(self, kinetics_sim, ids_list): """ Get the numerator and denominator of a "fraction" function. Parameters ---- kinetics_sim: string-simplified kinetics ids_list: list-id list including all the ids in kinetics, reactants and products Returns ------- Type - the numerator and the denominator of the fraction """ strange_func = 0 pre_symbols = '' ids_with_underscore = [] kinetics_with_underscore = util.add_underscore_to_ids(ids_list, kinetics_sim, ids_with_underscore) for i in range(len(ids_with_underscore)): pre_symbols += ids_with_underscore[i] pre_symbols += ' ' pre_symbols = pre_symbols[:-1] # remove the space at the end pre_symbols_comma = pre_symbols.replace(" ", ",") stmt = "%s = sp.symbols('%s')" % (pre_symbols_comma, pre_symbols) try: # sometimes there is "invalid syntax error" exec(stmt, globals()) except: strange_func = 1 try: # check if there is strange func (i.e. delay) in kinetic law; # sometimes ids_list is not enough for all the ids in kinetics eq_stat = "kinetics_eq = " + kinetics_with_underscore exec(eq_stat, globals()) except: strange_func = 1 eq = ['', ''] if strange_func == 0: try: numerator = str(kinetics_eq.as_numer_denom()[0]) denominator = str(kinetics_eq.as_numer_denom()[1]) eq[0] = numerator eq[1] = denominator except: pass eq[0] = util.remove_underscore_from_ids(ids_list, eq[0]) eq[1] = util.remove_underscore_from_ids(ids_list, eq[1]) return eq def _numerator_denominator_order_remained(self, kinetics, ids_list): # Split the fraction at the '/' character split_fraction = kinetics.split('/') # Check if there's a numerator and a denominator if len(split_fraction) != 2: return ['', ''] # Return the numerator and the denominator as a tuple return split_fraction[0].strip(), split_fraction[1].strip() def _find_positions_in_rate_law(self, element_list, rate_law): largest_position = -1 smallest_position = sys.maxsize prev_position = -1 increasing_flag = True for element in element_list: pattern = re.compile(rf"\b{re.escape(element)}\b") match = pattern.search(rate_law) if match: index = match.start() largest_position = max(index, largest_position) smallest_position = min(index, smallest_position) if index < prev_position: increasing_flag = False prev_position = index return largest_position, smallest_position, increasing_flag def _check_product_of_terms_format(self, kinetics, compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species): comp_stats = self._find_positions_in_rate_law( compartment_in_kinetic_law, kinetics) param_stats = self._find_positions_in_rate_law( parameters_in_kinetic_law_only, kinetics) spec_stats = self._find_positions_in_rate_law(sorted_species, kinetics) is_formatted = (comp_stats[0] < param_stats[1] or param_stats[1] == sys.maxsize) and \ (param_stats[0] < spec_stats[1] or spec_stats[1] == sys.maxsize) and \ (comp_stats[0] < spec_stats[1] or spec_stats[1] == sys.maxsize) increasing_flag = comp_stats[2] and param_stats[2] and spec_stats[2] if is_formatted: return 0 if increasing_flag else 1 return 2 def _check_expression_format(self, kinetics, compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species): compartment_in_kinetic_law.sort() parameters_in_kinetic_law_only.sort() product_of_terms = re.split(r"[+-]", kinetics) ret = 0 for term in product_of_terms: format_status = self._check_product_of_terms_format( term, compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species) if format_status > ret: ret = format_status return ret def _check_formatting_conventions(self, **kwargs): reaction_id = kwargs["reaction_id"] compartment_in_kinetic_law = kwargs["compartment_in_kinetic_law"] kinetics = kwargs["kinetics"] ids_list = kwargs["ids_list"] parameters_in_kinetic_law_only = kwargs["parameters_in_kinetic_law_only"] codes = kwargs["codes"] sorted_species = kwargs["sorted_species"] flag = 0 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( eq[0], compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species) flag += 3 * self._check_expression_format( eq[1], compartment_in_kinetic_law, parameters_in_kinetic_law_only, sorted_species) else: 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.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.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.data.results.add_message( reaction_id, 1032, f"Denominator not in alphabetical order") if flag == 4 and 1033 in codes: self.data.results.add_message( reaction_id, 1033, f"Numerator and denominator not in alphabetical order") if flag == 5 and 1034 in codes: 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.data.results.add_message( # reaction_id, 1035, f"Denominator convention not followed") # if flag == 7 and 1036 in codes: # 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.data.results.add_message( # reaction_id, 1037, f"Numerator and denominator convention not followed") def _check_sboterm_annotations(self, **kwargs): """ Checks if the SBOTerm annotations for the rate law follow recommended SBO terms. Code: 1040, 1041, 1042, 1043, 1044 Args: reaction_id (str): The reaction's id'. sbo_term (int): the sbo_term annotation for the kinetic law, -1 if not annotated Adds: A warning message to results specifying that the annotations for the rate law do not follow recommended SBO terms. """ reaction_id = kwargs["reaction_id"] sbo_term = kwargs["sbo_term"] codes = kwargs["codes"] assert len(self.data.default_classifications) > 0 flag = 0 if sbo_term < 0: flag = 0 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.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.data.default_classifications[reaction_id][key] for key in BIDR_ALL_KEYS): if sbo_term not in BI_SBOS: flag = 3 elif self.data.default_classifications[reaction_id]['MM']: if sbo_term not in MM_SBOS: flag = 4 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.data.default_classifications[reaction_id]['Hill']: # if sbo_term not in HILL_SBOS: # flag = 6 if flag == 1 and 1040 in codes: 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.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.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.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.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")