Source code for refinegems.curation.miriam

#!/usr/bin/env python
"""General functions to conform annotations to the MIRIAM standards

The functions can be used to change the CURIE pattern and to clean the CURIEs, CVTerm qualifiers and qualifier types
up.
"""

__author__ = "Gwendolyn O. Döbel and Carolin Brune"

################################################################################
# requirements
################################################################################

import re
import libsbml
import logging

from colorama import Fore, Style
from datetime import date
from functools import reduce

from libsbml import Model as libModel
from libsbml import SBMLDocument, UnitDefinition, SBase, ListOf
from libsbml import (
    MODEL_QUALIFIER,
    BQM_IS,
    BIOLOGICAL_QUALIFIER,
    BQB_IS,
    BQB_HAS_PROPERTY,
    BQB_IS_HOMOLOG_TO,
)
from libsbml import BiolQualifierType_toString, ModelQualifierType_toString

from pathlib import Path
from sortedcontainers import SortedDict, SortedSet
from tqdm.auto import tqdm


from ..utility.cvterms import generate_cvterm, MIRIAM, OLD_MIRIAM
from ..utility.db_access import BIOCYC_TIER1_DATABASES_PREFIXES
from ..utility.io import parse_dict_to_dataframe
from ..developement.optional import require_bioregistry

################################################################################
# setup logging
################################################################################

logger = logging.getLogger(__name__)

################################################################################
# variables
################################################################################

non_alnum_pattern = re.compile(r'[\W_]+') # :meta: 
DEPRECATED_PREFIXES = {
    'ecogene',
    'ncbigi' # Converted to accession version since 2016 according to NCBI
} # :meta:

################################################################################
# functions
################################################################################


[docs] def _parse_bioregistry_uri(bioregistry, uri: str) -> tuple[str, str] | None: """Parse a URI while tolerating incompatible bioregistry/curies releases.""" try: parsed_uri = bioregistry.manager.parse_uri(uri) except Exception: try: parsed_uri = bioregistry.manager.converter.parse_uri(uri) except Exception: return None if not parsed_uri: return None prefix, identifier = parsed_uri if prefix is None or identifier is None: return None return prefix, identifier
[docs] def _is_valid_bioregistry_uri(bioregistry, uri: str) -> bool: """Return whether a URI parses to a valid bioregistry identifier.""" parsed_uri = _parse_bioregistry_uri(bioregistry, uri) try: return bool(parsed_uri and bioregistry.is_valid_identifier(*parsed_uri)) except Exception: return False
# Change CURIE pattern/Correct CURIEs # ------------------------------------ # @FIXME Check if compatibility with bioregistry versions starting from 0.12.19 is possible
[docs] def get_set_of_curies( uri_list: list[str], ) -> tuple[SortedDict[str : SortedSet[str]], list[str]]: """Gets a list of URIs & maps the database prefixes to their respective identifier sets Args: - uri_list (list[str]): List containing CURIEs Returns: tuple: A sorted dictionary (1) & a list (2) (1) SortedDict: Sorted dictionary mapping database prefixes from the provided CURIEs to their respective identifier sets also provided by the CURIEs (2) list: List of CURIEs that are invalid according to bioregistry """ curie_dict = SortedDict() prefix, identifier = None, None invalid_curies = [] bioregistry = require_bioregistry("polish and validate MIRIAM annotations") for uri in uri_list: # Extracts the CURIE part from the URI/IRI if MIRIAM in uri: extracted_curie = uri.split(MIRIAM)[1] elif OLD_MIRIAM in uri: extracted_curie = uri.split(OLD_MIRIAM)[1] else: extracted_curie = uri curie = bioregistry.manager.parse_curie( extracted_curie ) # Contains valid db prefix to identifiers pairs curie = list(curie) # Turn tuple into list to allow item assignment # @DEBUG #print(f'Prefix:CURIE at start of get_set_of_curies: {curie[0]}:{curie[1]}') # Prefix is valid but to have same result for same databases need to do a bit of own parsing if curie[0]: # Check for presence of '<' or '>' in the identifier part of the CURIE # This is to avoid issues with HTML-like strings that might be present in the identifier # as these are hard to fix as it is not clear what the correct identifier should be if "<" in curie[1] or ">" in curie[1]: invalid_curies.append(f"{curie[0]}:{curie[1]}") continue if re.fullmatch( r"^biocyc$", curie[0], re.IGNORECASE ): # Check for biocyc to also add metacyc if possible # Always add META if BioCyc sub-datbase prefixes are missing curie = ( curie if curie[1].split(r":")[0] in BIOCYC_TIER1_DATABASES_PREFIXES else [curie[0], f"META:{curie[1]}"] ) if "META" in curie[1]: if bioregistry.is_valid_identifier( *curie ): # Get the valid BioCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add the MetaCyc identifier additionally curie[1] = curie[1].split(r"META:")[ 1 ] # Metacyc identifier comes after 'META:' in biocyc identifier if re.search(r"^(rxn|trans-rxn)[-]*|[-]*(rxn)$", curie[1], re.IGNORECASE): curie[0] = "metacyc.reaction" else: curie[0] = "metacyc.compound" elif "metacyc." in curie[0]: if bioregistry.is_valid_identifier( *curie ): # Get the valid MetaCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add the BioCyc identifier additionally curie = [ "biocyc", f"META:{curie[1]}", ] # Metacyc identifier comes after 'META:' in biocyc identifier elif re.fullmatch( r"^brenda$", curie[0], re.IGNORECASE ): # Brenda & EC code is the same curie[0] = "eccode" elif not curie[0]: # Need to do own parsing if prefix is not valid # Get CURIEs irrespective of pattern if "/" in extracted_curie: extracted_curie = extracted_curie.split(r"/") # Check for NaN identifiers if re.fullmatch( r"^nan$", extracted_curie[0], re.IGNORECASE ) or re.fullmatch(r"^nan$", extracted_curie[1], re.IGNORECASE): # Only return strings where the database prefix is 'NaN' but a possible identifier could be contained if re.fullmatch( r"^nan$", extracted_curie[0], re.IGNORECASE ) and not re.fullmatch(r"^nan$", extracted_curie[1], re.IGNORECASE): invalid_curies.append( f"{extracted_curie[0]}:{extracted_curie[1]}" ) continue # Check for certain special cases if re.search( r"inchi", extracted_curie[0], re.IGNORECASE ): # Check for inchi as splitting by '/' splits too much if re.fullmatch(r"^inchi$", extracted_curie[0], re.IGNORECASE): curie = ( extracted_curie[0].lower(), "/".join(extracted_curie[1 : len(extracted_curie)]), ) elif re.fullmatch(r"^inchikey$", extracted_curie[0], re.IGNORECASE): curie = (extracted_curie[0].lower(), extracted_curie[1]) # make sure no non-alphanumeric characters are in the InChIKey elif non_alnum_pattern.sub('', extracted_curie[0]) == 'inchikey': curie = ('inchikey', extracted_curie[1]) else: wrong_prefix = extracted_curie[0].split(r":") curie = ( wrong_prefix[0], f'{wrong_prefix[1]}/{"/".join(extracted_curie[1:len(extracted_curie)])}', ) elif re.fullmatch( r"^brenda$", extracted_curie[0], re.IGNORECASE ): # Brenda & EC code is the same curie = ("eccode", extracted_curie[1]) elif re.fullmatch( r"^biocyc$", extracted_curie[0], re.IGNORECASE ): # Check for biocyc to also add metacyc if possible # Always add META if BioCyc sub-datbase prefixes are missing extracted_curie[1] = ( extracted_curie[1] if extracted_curie[1].split(r":")[0] in BIOCYC_TIER1_DATABASES_PREFIXES else f"META:{extracted_curie[1]}" ) curie = ["biocyc", extracted_curie[1]] if "META" in curie[1]: if bioregistry.is_valid_identifier( *curie ): # Get the valid BioCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add additionallly the MetaCyc identifier curie[1] = curie[1].split(r"META:")[ 1 ] # Metacyc identifier comes after 'META:' in biocyc identifier if re.search(r"^(rxn|trans-rxn)[-]*|[-]*(rxn)$", curie[1], re.IGNORECASE): curie[0] = "metacyc.reaction" else: curie[0] = "metacyc.compound" elif "metacyc." in extracted_curie[0]: curie = extracted_curie if bioregistry.is_valid_identifier( *curie ): # Get the valid MetaCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add BioCyc identfier additionally curie = [ "biocyc", f"META:{curie[1]}", ] # Metacyc identifier comes after 'META:' in biocyc identifier elif re.fullmatch(r"^eco|chebi$", extracted_curie[0], re.IGNORECASE): if ":" in extracted_curie[1]: new_curie = extracted_curie[1].split(r":") curie = (new_curie[0].lower(), new_curie[1]) else: curie = tuple(extracted_curie) elif re.search( r"^sbo:", extracted_curie[1], re.IGNORECASE ): # Checks for old pattern of SBO term URIs ('MIRIAM/sbo/SBO:identifier') curie = [extracted_curie[0], extracted_curie[1].split(r":")[1]] else: if re.fullmatch( r"^brenda$", extracted_curie[0], re.IGNORECASE ) or re.fullmatch( r"^ec-code$", extracted_curie[0], re.IGNORECASE ): # Brenda equals EC code, EC code in URI = ec-code curie[0] = "eccode" else: curie[0] = extracted_curie[0] curie[1] = extracted_curie[1] elif ":" in extracted_curie: extracted_curie = extracted_curie.split(r":") # Check for NaN identifiers if re.fullmatch( r"^nan$", extracted_curie[0], re.IGNORECASE ) or re.fullmatch(r"^nan$", extracted_curie[1], re.IGNORECASE): # Only return strings where the database prefix is 'NaN' but a possible identifier could be contained if re.fullmatch( r"^nan$", extracted_curie[0], re.IGNORECASE ) and not re.fullmatch(r"^nan$", extracted_curie[1], re.IGNORECASE): invalid_curies.append( f"{extracted_curie[0]}:{extracted_curie[1]}" ) continue elif re.fullmatch( r"^biocyc$", extracted_curie[0], re.IGNORECASE ): # Check for biocyc to also add metacyc if possible # Always add META if BioCyc sub-datbase prefixes are missing extracted_curie[1] = ( extracted_curie[1] if extracted_curie[1].split(r":")[0] in BIOCYC_TIER1_DATABASES_PREFIXES else f"META:{extracted_curie[1]}" ) curie = ["biocyc", extracted_curie[1]] if "META" in curie[1]: if bioregistry.is_valid_identifier( *curie ): # Get the valid BioCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add MetaCyc identifier additionally curie[1] = curie[1].split(r"META:")[ 1 ] # Metacyc identifier comes after 'META:' in biocyc identifier if re.search(r"^(rxn|trans-rxn)[-]*|[-]*(rxn)$", curie[1], re.IGNORECASE): curie[0] = "metacyc.reaction" else: curie[0] = "metacyc.compound" elif "metacyc." in extracted_curie[0]: curie = extracted_curie if bioregistry.is_valid_identifier( *curie ): # Get the valid MetaCyc identifier & Add to dictionary prefix, identifier = bioregistry.normalize_parsed_curie(*curie) if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) else: invalid_curies.append(f"{curie[0]}:{curie[1]}") # Add BioCyc identifier additionally curie = [ "biocyc", f"META:{curie[1]}", ] # Metacyc identifier comes after 'META:' in biocyc identifier else: if re.fullmatch( r"^brenda$", extracted_curie[0], re.IGNORECASE ) or re.fullmatch( r"^ec-code$", extracted_curie[0], re.IGNORECASE ): # Brenda equals EC code, EC code in URI = ec-code curie[0] = "eccode" else: curie[0] = extracted_curie[0] if re.fullmatch(r"^kegg.genes$", extracted_curie[0], re.IGNORECASE): curie[1] = ":".join(extracted_curie[1 : len(extracted_curie)]) else: curie[1] = extracted_curie[1] if bioregistry.is_valid_identifier(*curie): # Get all valid identifiers prefix, identifier = bioregistry.normalize_parsed_curie(*curie) else: if curie[0] == "eccode": correct_id = curie[ 1 ] # EC number needs to have 4 places if splitted at the dots while len(correct_id.split(r".")) < 4: correct_id = f"{correct_id}.-" prefix, identifier = bioregistry.normalize_parsed_curie(curie[0], correct_id) # Report too long EC codes as invalid CURIEs! if len(correct_id.split(r".")) > 4: invalid_curies.append(f"{prefix}:{identifier}") continue else: invalid_curies.append(f"{curie[0]}:{curie[1]}") if prefix and identifier: # Check that a prefix & identifier pair was found! # Use prefix as key & the corresponding set of identifiers as values if not curie_dict or (prefix not in curie_dict): curie_dict[prefix] = SortedSet() curie_dict[prefix].add(identifier) return curie_dict, invalid_curies
[docs] def generate_uri_set_with_old_pattern( prefix2id: SortedDict[str : SortedSet[str]], ) -> SortedSet[str]: """Generate a set of complete URIs from the provided prefix to identifier mapping with the old MIRIAM pattern. Args: - prefix2id (SortedDict[str: SortedSet[str]]): Dictionary containing a mapping from database prefixes to their respective identifier sets Returns: SortedSet: Sorted set containing complete URIs """ uri_set = SortedSet() SEPARATOR = "/" for prefix in prefix2id: current_prefix = prefix for identifier in prefix2id.get(prefix): separator = SEPARATOR if re.search( r"o$", prefix, re.IGNORECASE ): # Ontologies seem only to work with new pattern! separator = ":" prefix = prefix.upper() elif re.fullmatch( r"^chebi$", current_prefix, re.IGNORECASE ): # The old pattern for chebi is different: Just adding '/' does NOT work! prefix = f"chebi/{current_prefix}" separator = ":" elif re.fullmatch( r"^biocyc$", prefix, re.IGNORECASE ): # Get identifier for biocyc prefix = f"biocyc" # META # separator = ':' uri = MIRIAM + prefix + separator + identifier uri_set.add(uri) return uri_set
[docs] def generate_miriam_compliant_uri_set( prefix2id: SortedDict[str : SortedSet[str]], ) -> SortedSet[str]: """Generate a set of complete MIRIAM compliant URIs from the provided prefix to identifier mapping Args: - prefix2id (SortedDict[str: SortedSet[str]]): Dictionary containing a mapping from database prefixes to their respective identifier sets Returns: SortedSet: Sorted set containing complete URIs """ uri_set = SortedSet() bioregistry = require_bioregistry("generate MIRIAM-compliant annotation IRIs") for prefix in prefix2id: for identifier in prefix2id.get(prefix): # @DEBUG #print(f'Prefix:CURIE before identifiers.org IRI is generated: {prefix}:{identifier}') if not prefix in DEPRECATED_PREFIXES: uri = bioregistry.get_identifiers_org_iri(prefix, identifier) # @DEBUG #print(f'Resulting IRI: {uri}') uri_set.add(uri) else: logger.warning( f"The prefix '{prefix}' is deprecated. The corresponding CURIE '{prefix}:{identifier}' cannot be converted to a MIRIAM compliant URI." ) return uri_set
[docs] def add_uri_set(entity: SBase, qt, b_m_qt, uri_set: SortedSet[str]) -> list[str]: """Add a complete URI set to the provided CVTerm Args: - entity (SBase): A libSBML SBase object like model, GeneProduct, etc. - qt: A libSBML qualifier type: BIOLOGICAL_QUALIFIER|MODEL_QUALIFIER - b_m_qt: A libSBML biological or model qualifier type like BQB_IS|BQM_IS - uri_set (SortedSet[str]): SortedSet containing URIs """ new_cvterm = generate_cvterm(qt, b_m_qt) for uri in uri_set: new_cvterm.addResource(uri) entity.addCVTerm(new_cvterm)
[docs] def improve_uri_per_entity(entity: SBase, new_pattern: bool) -> list[str]: """Helper function: Removes duplicates & changes pattern according to new_pattern Args: - entity (SBase): A libSBML SBase object, either a model or an entity - new_pattern (bool): True if new pattern is wanted, otherwise False Returns: list: List of all collected invalid CURIEs of one entity """ collected_invalid_curies = [] pattern = rf"{MIRIAM}|{OLD_MIRIAM}" bioregistry = require_bioregistry("polish and validate MIRIAM annotations") cvterms = entity.getCVTerms() cvterms = [cvterm.clone() for cvterm in cvterms] entity.unsetCVTerms() for cvterm in cvterms: tmp_list = [] current_b_m_qt = None # Needs to be initialised, otherwise UnboundLocalError: local variable 'current_b_m_qt' referenced before assignment # Retrieve QualifierType & Biological/ModelQualifierType before resource is removed! current_qt = cvterm.getQualifierType() match current_qt: case libsbml.BIOLOGICAL_QUALIFIER: current_b_m_qt = cvterm.getBiologicalQualifierType() case libsbml.MODEL_QUALIFIER: current_b_m_qt = cvterm.getModelQualifierType() case _: mes = f"Unknown qualifier type detected in model: {current_qt}" raise TypeError(mes) current_uris = [ cvterm.getResourceURI(i) for i in range(cvterm.getNumResources()) ] # Remove all URIs/CURIEs from model & collect invalid/not miriam compliant URIs for cu in current_uris: current_uri = cu cvterm.removeResource(cu) if ( _is_valid_bioregistry_uri(bioregistry, current_uri) or bioregistry.is_valid_curie(current_uri.lower()) or re.match(pattern, current_uri, re.IGNORECASE) ): # For Rhea entries if version is specified with '#' remove the version if re.search(r"rhea", current_uri, re.IGNORECASE) and re.search( r"#", current_uri ): current_uri = current_uri.split(r"#")[0] # Collect all URIs to be adjusted/newly added tmp_list.append(current_uri) else: collected_invalid_curies.append(current_uri) prefix2id, invalid_curies = get_set_of_curies(tmp_list) collected_invalid_curies.extend(invalid_curies) # Add valid CURIEs with selected pattern & report if no valid CURIEs exist if prefix2id: if new_pattern: # @DEBUG #print(f'Prefix:CURIE dictionary before generate_miriam_compliant_uri_set: {prefix2id}') uri_set = generate_miriam_compliant_uri_set(prefix2id) else: uri_set = generate_uri_set_with_old_pattern(prefix2id) add_uri_set(entity, current_qt, current_b_m_qt, uri_set) else: # Remove annotations if no valid URIs/CURIEs were found if cvterm.getNumResources() < 1: entity_reference = entity.getId() if type(entity) == SBMLDocument: entity.unsetMetaId() # Remove meta ID from SBMLDocument as not necessary entity_reference = entity.getElementName() # Get entity name as SBMLDocument has no ID logger.warning( f"No valid URIs/CURIEs found for {entity_reference}. To resolve manually please inspect file containing invalid CURIEs." ) return collected_invalid_curies
[docs] def improve_uris(entities: SBase, new_pattern: bool) -> dict[str : list[str]]: """Removes duplicates & changes pattern according to bioregistry or new_pattern Args: - entities (SBase): A libSBML SBase object, either a model or a list of entities - bioregistry (bool): Specifies whether the URIs should be changed with the help of bioregistry to be MIRIAM compliant or changed according to new or old pattern - new_pattern (bool): True if new pattern is wanted, otherwise False Returns: dict: Mapping of entity identifier to list of corresponding invalid CURIEs """ entity2invalid_curies = {} if not isinstance(entities, ListOf): # Model & SBMLDocument need to be handled like entity! # type(entities) == libModel or type(entities) == SBMLDocument invalid_curies = improve_uri_per_entity(entities, new_pattern) if invalid_curies: entity_reference = entities.getElementName() if type(entities) == SBMLDocument else entities.getId() entity2invalid_curies[entity_reference] = invalid_curies else: for entity in tqdm(entities): invalid_curies = improve_uri_per_entity(entity, new_pattern) if invalid_curies: entity2invalid_curies[entity.getId()] = invalid_curies if type(entity) == UnitDefinition: for ( unit ) in ( entity.getListOfUnits() ): # Unit needs to be handled within ListOfUnitDefinition invalid_curies = improve_uri_per_entity(unit, new_pattern) if invalid_curies: entity2invalid_curies[unit.getId()] = invalid_curies return entity2invalid_curies
[docs] def polish_annotations( model: libModel, new_pattern: bool, outpath: str = None ) -> libModel: """Polishes all annotations in a model such that no duplicates are present & the same pattern is used for all CURIEs Args: - model (libModel): Model loaded with libSBML - new_pattern (bool): True if new pattern is wanted, otherwise False. Note that bioregistry internally only uses the new patter. - outpath (str, optional): Path to output file for invalid CURIEs detected by improve_uris Defaults to None. Returns: libModel: libSBML model with polished annotations """ list_of_entity2invalid_curies = [] listOf_dict = { "sbml": model.getSBMLDocument(), "model": model, "compartment": model.getListOfCompartments(), "metabolite": model.getListOfSpecies(), "parameter": model.getListOfParameters(), "reaction": model.getListOfReactions(), "unit definition": model.getListOfUnitDefinitions(), } if model.isPackageEnabled("fbc"): listOf_dict["gene product"] = model.getPlugin("fbc").getListOfGeneProducts() if model.isPackageEnabled("groups"): listOf_dict["group"] = model.getPlugin("groups").getListOfGroups() # Adjust annotations in model for lof in listOf_dict: logger.info(f"Polish {lof} annotations...") entity2invalid_curies = improve_uris(listOf_dict[lof], new_pattern) list_of_entity2invalid_curies.append(entity2invalid_curies) all_entity2invalid_curies = reduce( lambda d1, d2: {**d1, **d2}, list_of_entity2invalid_curies ) # Handle CarveMe details correctly (written with help of Copilot) cm_details_locations = { k: [i for i, x in enumerate(v) if re.search('CarveMe', x, re.IGNORECASE)] for k, v in all_entity2invalid_curies.items() if any(re.search('CarveMe', x, re.IGNORECASE) for x in v) } # Get keys and indeces in value list for all CarveMe hits if cm_details_locations: cm_details4notes = set() # Remove all entries for CarveMe and generate set of entries for notes for k in cm_details_locations.keys(): cm_entries_at_k = all_entity2invalid_curies[k] for i in cm_details_locations[k]: cm_details4notes.add(cm_entries_at_k.pop(i).replace(':', ': ')) # Add to set logger.info(f'Moved all invalid CURIE(s) from the annotations of {k} containing details on CarveMe to notes of the model object.') if not all_entity2invalid_curies[k]: del all_entity2invalid_curies[k] logger.warning(f'List of invalid CURIEs for {k} now empty.') # Generate notes string from set to model notes model_metadata = '\n'.join([f'<p>{cm_details}</p>' for cm_details in cm_details4notes]) if model.isSetNotes(): for cm_details in cm_details4notes: model.appendNotes(model_metadata) else: model.unsetNotes() note_string = f'''<html xmlns = "http://www.w3.org/1999/xhtml" > {model_metadata} </html>''' model.setNotes(note_string) # Write invalid CURIEs to file if present if all_entity2invalid_curies: filename = ( f'{model.getId()}_invalid_curies_{str(date.today().strftime("%Y%m%d"))}.csv' ) # Set-up path if outpath: if isinstance(outpath, str): outpath = Path(outpath) outpath.mkdir(parents=True, exist_ok=True) filename = Path(outpath, filename) else: filename = Path(filename) logger.warning( f"In the provided model {model.getId()} for {len(all_entity2invalid_curies)} entities invalid CURIEs were detected. " + f"These invalid CURIEs are saved to {filename}" ) invalid_curies_df = parse_dict_to_dataframe(all_entity2invalid_curies) invalid_curies_df.columns = ["entity", "invalid_curie"] invalid_curies_df[["prefix", "identifier"]] = ( invalid_curies_df.invalid_curie.str.split(r":", n=1, expand=True) ) # Required for identifiers that also contain a ':' invalid_curies_df = invalid_curies_df.drop("invalid_curie", axis=1) invalid_curies_df.to_csv(filename, index=False) return model
# Correct CVTerm qualifiers & qualifier types # -------------------------------------------
[docs] def change_qualifier_per_entity( entity: SBase, new_qt, new_b_m_qt, specific_db_prefix: str = None ) -> list: """Updates Qualifiers to be MIRIAM compliant for an entity Args: - entity (SBase): A libSBML SBase object like model, GeneProduct, etc. - new_qt (Qualifier): A libSBML qualifier type: BIOLOGICAL_QUALIFIER|MODEL_QUALIFIER - new_b_m_qt (QualifierType): A libSBML biological or model qualifier type like BQB_IS|BQM_IS - specific_db_prefix (str): Has to be set if only for a specific database the qualifier type should be changed. Can be 'kegg.genes', 'biocyc', etc. Returns: list: CURIEs that are not MIRIAM compliant """ not_miriam_compliant = [] pattern = f"{MIRIAM}|{OLD_MIRIAM}" cvterms = entity.getCVTerms() # for i in range(len(cvterms)): for cvterm in cvterms: tmp_set = SortedSet() sbo_set = SortedSet() if ( cvterm.getBiologicalQualifierType() == 9 ): # 9 = BQB_OCCURS_IN (Reaction), Check for reactions with occursIn logger.info( f"CVTerm for {Fore.LIGHTYELLOW_EX}{str(entity)}{Style.RESET_ALL}" + f" is left as {Fore.LIGHTYELLOW_EX}{BiolQualifierType_toString(cvterm.getBiologicalQualifierType())}{Style.RESET_ALL}" ) elif ( cvterm.getModelQualifierType() == 1 ): # 1 = BQM_IS_DESCRIBED_BY (UnitDefinition), Check for UnitDefinitions with isDescribedBy logger.info( f"CVTerm for {Fore.LIGHTYELLOW_EX}{str(entity)}{Style.RESET_ALL}" + f" is left as {Fore.LIGHTYELLOW_EX}{ModelQualifierType_toString(cvterm.getModelQualifierType())}{Style.RESET_ALL}" ) else: current_curies = [ cvterm.getResourceURI(j) for j in range(cvterm.getNumResources()) ] for cc in current_curies: current_curie = None if (specific_db_prefix != None) and (specific_db_prefix != ""): if specific_db_prefix in cc: current_curie = cc else: current_curie = cc if (current_curie) and re.match( pattern, current_curie, re.IGNORECASE ): # If model contains identifiers without MIRIAM/OLD_MIRIAM these are kept if re.search(r"sbo:", current_curie, re.IGNORECASE): sbo_set.add(current_curie) else: tmp_set.add(current_curie) cvterm.removeResource(current_curie) else: not_miriam_compliant.append(current_curie) if sbo_set: add_uri_set(entity, BIOLOGICAL_QUALIFIER, BQB_HAS_PROPERTY, sbo_set) add_uri_set(entity, new_qt, new_b_m_qt, tmp_set) # cvterms.remove(i) if not_miriam_compliant: return not_miriam_compliant
[docs] def change_qualifiers( model: libModel, entity_type: str, new_qt, new_b_m_qt, specific_db_prefix: str = None, ) -> libModel: """Updates Qualifiers to be MIRIAM compliant for an entity type of a given model Args: - model (libModel): Model loaded with libSBML - entity_type (str): Any string of the following: model|compartment|metabolite|parameter|reaction|unit definition|unit|gene product|group - new_qt (Qualifier): A libSBML qualifier type: BIOLOGICAL_QUALIFIER|MODEL_QUALIFIER - new_b_m_qt (QualifierType): A libSBML biological or model qualifier type like BQB_IS|BQM_IS - specific_db_prefix (str): Has to be set if only for a specific database the qualifier type should be changed. Can be 'kegg.genes', 'biocyc', etc. Returns: libModel: Model with changed qualifier for given entity type """ not_miriam_compliant = [] listOf_dict = { "model": model, "compartment": model.getListOfCompartments(), "metabolite": model.getListOfSpecies(), "parameter": model.getListOfParameters(), "reaction": model.getListOfReactions(), "unit definition": model.getListOfUnitDefinitions(), } if model.isPackageEnabled("fbc"): listOf_dict["gene product"] = model.getPlugin("fbc").getListOfGeneProducts() if model.isPackageEnabled("groups"): listOf_dict["group"] = model.getPlugin("groups").getListOfGroups() if entity_type == "model": # Model needs to be handled like entity! not_miriam_compliant = change_qualifier_per_entity( listOf_dict.get("model"), new_qt, new_b_m_qt, specific_db_prefix ) elif entity_type == "unit": for unit in listOf_dict.get( "unit definition" ): # Unit needs to be handled within ListOfUnitDefinition not_miriam_compliant = change_qualifier_per_entity( unit, new_qt, new_b_m_qt, specific_db_prefix ) else: try: for entity in tqdm(listOf_dict.get(entity_type)): not_miriam_compliant = change_qualifier_per_entity( entity, new_qt, new_b_m_qt, specific_db_prefix ) except TypeError: logger.info( "The entity " + entity_type + " is not present in " + model.getId() ) if not_miriam_compliant: logger.warning( f"The following {len(not_miriam_compliant)} entities are not MIRIAM compliant: {not_miriam_compliant}" ) return model
[docs] def change_all_qualifiers(model: libModel, lab_strain: bool) -> libModel: """Wrapper function to change qualifiers of all entities at once Args: - model (libModel): Model loaded with libSBML - lab_strain (bool): True if the strain was sequenced in a local lab Returns: libModel: Model with all qualifiers updated to be MIRIAM compliant """ # Change all model entities to have the correct model qualifier entity_list_mod = ["model", "unit definition", "unit"] for entity in entity_list_mod: logger.info(f"Change {str(entity)} qualifiers...") model = change_qualifiers(model, entity, MODEL_QUALIFIER, BQM_IS) # Change all remaining entities to have the correct biological qualifier entity_list = [ "compartment", "metabolite", "parameter", "reaction", ] if model.isPackageEnabled("fbc"): entity_list.append("gene product") if model.isPackageEnabled("groups"): entity_list.append("group") for entity in entity_list: logger.info(f"Change {str(entity)} qualifiers...") if lab_strain and entity == "gene product": model = change_qualifiers( model, "gene product", BIOLOGICAL_QUALIFIER, BQB_IS_HOMOLOG_TO ) else: model = change_qualifiers(model, entity, BIOLOGICAL_QUALIFIER, BQB_IS) return model