Source code for yaml2sbml.yaml2sbml

"""Translate ODEs in the YAML format into SBML."""
import argparse
import warnings
from pathlib import Path

import libsbml as sbml
import yaml
from yaml.scanner import ScannerError

from .yaml_validation import _validate_yaml_from_dict


[docs]def yaml2sbml(yaml_dir: str, sbml_dir: str, observables_as_assignments: bool = False): """ Parse a YAML file with the specification of ODEs and write it to SBML. SBML is written within this function. If `observables_as_assignments=True`, observables will be translated into parameter assignments of the form `observable_<observable_id>`. Arguments: yaml_dir: directory to the YAML file with the ODEs specification sbml_dir: directory to the SBML file to be written out observables_as_assignments: indicates whether there should be parameter assignments of the form `observable_<observable_id>`. """ # check file extension in sbml_dir if not (sbml_dir.endswith('.xml') or sbml_dir.endswith('.sbml')): raise ValueError('sbml_dir should end with .xml or .sbml.') model_name = Path(sbml_dir).stem sbml_as_string = _parse_yaml(yaml_dir, model_name, observables_as_assignments) # write sbml file with open(sbml_dir, 'w') as f_out: f_out.write(sbml_as_string)
def _parse_yaml(yaml_dir: str, model_name: str, observables_as_assignments: bool = False) -> str: """ Parse a YAML file with the specification of ODEs to SBML. The SBML is returned as string. Arguments: yaml_dir: path to the YAML file with the ODEs specification model_name: model name as specified in the SBML observables_as_assignments: indicates if observables should be translated into parameter assignments Returns: sbml_string: a string containing the ODEs in SBML format Raises: SystemExit """ yaml_dict = _load_yaml_file(yaml_dir) _validate_yaml_from_dict(yaml_dict) sbml_string = _parse_yaml_dict(yaml_dict, model_name, observables_as_assignments) return sbml_string def _parse_yaml_dict(yaml_dict: dict, model_name: str, observables_as_assignments: bool = False) -> str: """ Generate a string, containing the SBML from a `yaml_dict. Arguments: yaml_dict: dictionary, containing to the YAML file with the ODEs specification.` model_name: model name as specified in the SBML observables_as_assignments: indicates if observables should be translated into parameter assignments Returns: sbml_string: a string containing the ODEs in SBML format. """ try: document = sbml.SBMLDocument(3, 1) except ValueError: raise SystemExit('Could not create SBMLDocument object') model = document.createModel() # remove file extension if model_name.endswith('.xml') or model_name.endswith('.sbml'): model_name = Path(model_name).stem model.setId(model_name) model.setName(model_name) model = _create_compartment(model) _convert_yaml_blocks_to_sbml(model, yaml_dict, observables_as_assignments) # check consistency and give warnings for errors in SBML: document.setConsistencyChecks(sbml.LIBSBML_CAT_UNITS_CONSISTENCY, False) if document.checkConsistency(): for error_num in range(document.getErrorLog().getNumErrors()): if not document.getErrorLog().getError(error_num).isWarning(): warnings.warn( document.getErrorLog().getError(error_num).getMessage(), RuntimeWarning) sbml_string = sbml.writeSBMLToString(document) return sbml_string def _create_compartment(model: sbml.Model): """ Create a default compartment for the model. yaml2sbml doesn't support multiple compartments. Arguments: model: SBML model Returns: model: SBML model with added compartment """ c = model.createCompartment() c.setId('Compartment') c.setConstant(True) c.setSize(1) return model def _load_yaml_file(yaml_file: str) -> dict: """ Load YAML file from a dictionary. Arguments: yaml_file: directory to the YAML model Returns: yaml_dic: dictionary with parsed YAML file contents Raises: RuntimeError, if YAML can not be parsed, e.g. due to incorrectly formatted entries """ try: with open(yaml_file, 'r') as f_in: yaml_contents = f_in.read() yaml_dict = yaml.full_load(yaml_contents) except ScannerError: raise RuntimeError('YAML file can not be parsed due to a Scanner ' 'Error. This commonly happens if formulas begin ' 'with a minus. Please set them inside of brackets ' '"(...)" or quotation marks.') return yaml_dict def _convert_yaml_blocks_to_sbml(model: sbml.Model, yaml_dict: dict, observables_as_assignments): """ Convert each block in the YAML dictionary to SBML. Arguments: model: SBML model yaml_dict: dictionary with YAML contents Returns: model: SBML model with added entities """ def _read_observables_with_assignments(model, block): return _read_observables_block(model, block, observables_as_assignments) function_dict = {'time': _read_time_block, 'parameters': _read_parameters_block, 'assignments': _read_assignments_block, 'functions': _read_functions_block, 'observables': _read_observables_with_assignments, 'odes': _read_odes_block, 'conditions': _read_conditions_block} for block in yaml_dict: function_dict[block](model, yaml_dict[block]) return model def _read_time_block(model: sbml.Model, time_dic: dict): """ Read and process the time block. Arguments: model: SBML model to which the rate rule will be added. time_dic: a dictionary with the time block in the ODE YAML file. """ if time_dic['variable'] == 'time': return else: _create_time(model, time_dic['variable']) def _create_time(model: sbml.Model, time_var: str): """ Create the time variable, add assignment to 'time'. Arguments: model: the SBML model to which the species will be added. time_var: str, the time variable """ if time_var == 'time': return time_parameter = model.createParameter() if time_parameter.setId(time_var) != sbml.LIBSBML_OPERATION_SUCCESS: raise RuntimeError(f'Unable to generate time parameter with id ' f'{time_var}. Invalid SBML identifier.') time_parameter.setName(time_var) time_parameter.setConstant(False) time_assignment = model.createAssignmentRule() time_assignment.setVariable(time_var) time_assignment.setMath(sbml.parseL3Formula('time')) def _read_parameters_block(model: sbml.Model, parameter_list: list): """ Read and process the parameters block in the YAML file. The expected format for parameter definition is {'parameterId': <parameterId>, 'nominalValue': <nominalValue>} Arguments: model: the SBML model parameter_list: block containing the parameter definitions """ for parameter_def in parameter_list: if 'nominalValue' in parameter_def.keys(): _create_parameter(model, parameter_def['parameterId'], parameter_def['nominalValue']) else: _create_parameter(model, parameter_def['parameterId']) def _create_parameter(model: sbml.Model, parameter_id: str, value: str = None): """ Create a parameter and add it to the given SBML model. Arguments: model: the SBML model to which the parameter will be added. parameter_id: the parameter ID value: the parameter value, if value is None, no parameter is set. """ k = model.createParameter() if k.setId(parameter_id) != sbml.LIBSBML_OPERATION_SUCCESS: raise RuntimeError(f'Unable to generate parameter with id ' f'{parameter_id}. Invalid SBML identifier.') k.setName(parameter_id) k.setConstant(True) if value is not None: k.setValue(float(value)) def _read_assignments_block(model: sbml.Model, assignment_list: list): """ Read and process the assignments block in the YAML file. The expected format of a state definition is: {'assignmentId': <assignmentId>, 'formula': <formula>} This is used to assign a formula (possibly time-dependent) to a variable. Arguments: model: the SBML model assignment_list: a list of dictionaries where each entry is an assignment definition """ for assignment_def in assignment_list: _create_assignment(model, assignment_def['assignmentId'], assignment_def['formula']) def _create_assignment(model: sbml.Model, assignment_id: str, formula: str): """ Create an assignment rule, that assigns <id> to <formula>. Arguments: model: SBML model to which the assignment rule will be added. assignment_id: str, the id of the assignment rule formula: str: contains the equation for the assignment rule """ assignment_parameter = model.createParameter() if assignment_parameter.setId(assignment_id): raise RuntimeError(f'Unable to generate assignment with id ' f'{assignment_id}. Invalid SBML identifier.') assignment_parameter.setName(assignment_id) assignment_parameter.setConstant(False) assignment_rule = model.createAssignmentRule() assignment_rule.setVariable(assignment_id) sbml_formula = sbml.parseL3Formula(formula) if sbml_formula is not None: assignment_rule.setMath(sbml.parseL3Formula(formula)) else: raise RuntimeError(f'Unable to generate assignment for formula ' f'{formula}, libsbml can not parse the given ' f'expression.') def _read_functions_block(model: sbml.Model, functions_list: list): """ Read and process the functions block in the YAML file. The expected format of a function definition is: {'functionId': <functionId>, 'arguments': <arguments>, 'formula' : <formula>} Arguments: model: a SBML model functions_list: a list of dictionaries where each entry is a function definition """ for function_def in functions_list: _create_function(model, function_def['functionId'], function_def['arguments'], function_def['formula']) def _create_function(model: sbml.Model, function_id: str, arguments: str, formula: str): """ Create a function definition and add it to the SBML model. Arguments: model: SBML model to which the function will be added. function_id: the function id/name arguments: the arguments of the function (species AND parameters) formula: the formula of the function """ f = model.createFunctionDefinition() if f.setId(function_id) != sbml.LIBSBML_OPERATION_SUCCESS: raise RuntimeError(f'Unable to generate function with id ' f'{function_id}. Invalid SBML identifier.') math = sbml.parseL3Formula('lambda(' + arguments + ', ' + formula + ')') if math is not None: f.setMath(math) else: raise RuntimeError(f'Unable to generate assignment for funtion ' f'{function_id}, libsbml can not parse the given ' f'function expression, given by ' f'lambda({arguments} , {formula}).') def _read_odes_block(model: sbml.Model, odes_list: list): """ Read and process the odes block in the YAML file. In particular, read the ODEs and add a species for the corresponding state and the right hand side as rateRules to the given SBML file. The expected format of an ode definition is: {'stateId': <state_variable>, 'rightHandSide' : <right_hand_side>, 'initialValue' = <initial_value>} Arguments: model: a SBML model odes_list: block of ODE definitions """ for ode_def in odes_list: _create_species(model, ode_def['stateId'], ode_def['initialValue']) _create_rate_rule(model, ode_def['stateId'], ode_def['rightHandSide']) def _create_species(model: sbml.Model, species_id: str, initial_amount: str): """ Create a species and add it to the SBML model. Arguments: model: the SBML model to which the species will be added. species_id: the species ID initial_amount: the species initial amount Returns: s: the SBML species """ s = model.createSpecies() if s.setId(species_id) != sbml.LIBSBML_OPERATION_SUCCESS: raise RuntimeError(f'Unable to generate species with id ' f'{species_id}. Invalid SBML identifier.') try: s.setInitialAmount(float(initial_amount)) except ValueError: init = model.createInitialAssignment() init.setId('init_' + species_id) init.setSymbol(species_id) init.setMath(sbml.parseL3Formula(initial_amount)) s.setConstant(False) s.setBoundaryCondition(False) s.setHasOnlySubstanceUnits(False) s.setCompartment('Compartment') return s def _create_rate_rule(model: sbml.Model, species_id: str, formula: str): """ Create an SBML rateRule for a species and add it to the SBML model. This is where the ODEs are encoded. Arguments: model: SBML model to which the rate rule will be added. species_id: the id of the state of the ODE formula: the right-hand-side of the ODE """ r = model.createRateRule() r.setId('d_dt_' + species_id) r.setVariable(species_id) math_ast = sbml.parseL3Formula(formula) if math_ast is None: raise RuntimeError(f'Unable to generate the rate rule for the state ' f'{species_id}, libsbml can not parse the right-' f'hand side, given by {formula}).') r.setMath(math_ast) def _read_observables_block(model: sbml.Model, observable_list: list, observables_as_assignments: bool): """ Read and process the observables block in the YAML file. Since the observables are not represented in the SBML, it only gives a warning to inform the user. Arguments: model: SBML model (libsbml) observable_list: observables block containing all observable definitions. observables_as_assignments: indicates whether there should be parameter assignments of the form `observable_<observable_id>`. """ if observables_as_assignments: for observable_def in observable_list: _create_assignment(model, f'observable_{observable_def["observableId"]}', observable_def['observableFormula']) else: warnings.warn( 'Observables are not represented in the SBML and therefore only ' 'have an effect on the output when called via yaml2petab') def _read_conditions_block(model: sbml.Model, conditions_list: list): """ Read and process the conditions block in the YAML file. Since conditions are not represented in the SBML, it only gives a warning to inform the user. Arguments: model: SBML model (libsbml) conditions_list: conditions block containing all conditions definitions. """ warnings.warn( 'Conditions are not represented in the SBML and therefore only have ' 'an effect on the output when called via yaml2petab')
[docs]def main(): """Command-Line Interface.""" parser = argparse.ArgumentParser( description='Takes in an ODE model in .yaml and converts it to SBML.') parser.add_argument('yaml_file', type=str, help='Path to input YAML file.') parser.add_argument('sbml_file', type=str, help='Path to output SBML file.') parser.add_argument('-o', '--observables_as_assignments', action='store_true', help='Optional argument, flag, which indicates, if ' 'observables should be represented in the SBML' 'as assignments. Potential Values: 1/0 (yes/no).') args = parser.parse_args() print(f'Path to YAML file: {args.yaml_file}') print(f'Path to SBML file: {args.sbml_file}') print('Converting...') yaml2sbml(args.yaml_file, args.sbml_file, args.observables_as_assignments)
if __name__ == '__main__': main()