Skip to content

Commit

Permalink
feat: refactored task preprocessing, added license management for Gurobi
Browse files Browse the repository at this point in the history
  • Loading branch information
jonrkarr committed Sep 11, 2021
1 parent 0b860aa commit 0fc3e97
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build/

# coverage files
htmlcov/
.coverage
.coverage*
coverage.xml

# compiled documentation
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Base OS
FROM python:3.9-slim-buster

ARG VERSION="0.1.15"
ARG VERSION="0.1.16"
ARG SIMULATOR_VERSION="0.22.1"

# metadata
Expand Down Expand Up @@ -48,7 +48,7 @@ RUN apt-get update -y \

# Copy code for command-line interface into image and install it
COPY . /root/Biosimulators_COBRApy
RUN pip install /root/Biosimulators_COBRApy \
RUN pip install /root/Biosimulators_COBRApy/[gurobi] \
&& mkdir -p /.cache/cobrapy \
&& chmod ugo+rw /.cache/cobrapy \
&& rm -rf /root/Biosimulators_COBRApy
Expand Down
2 changes: 1 addition & 1 deletion biosimulators_cobrapy/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.1.15'
__version__ = '0.1.16'
173 changes: 120 additions & 53 deletions biosimulators_cobrapy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
apply_variables_to_simulation_method_args, validate_variables, get_results_of_variables)
from biosimulators_utils.combine.exec import exec_sedml_docs_in_archive
from biosimulators_utils.config import get_config, Config # noqa: F401
from biosimulators_utils.licensing.gurobi import GurobiLicenseManager
from biosimulators_utils.log.data_model import CombineArchiveLog, TaskLog, StandardOutputErrorCapturerLevel # noqa: F401
from biosimulators_utils.viz.data_model import VizFormat # noqa: F401
from biosimulators_utils.report.data_model import ReportFormat, VariableResults, SedDocumentResults # noqa: F401
from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, SteadyStateSimulation, # noqa: F401
Variable)
from biosimulators_utils.sedml import validation
from biosimulators_utils.sedml.data_model import (Task, ModelLanguage, ModelAttributeChange, SteadyStateSimulation, # noqa: F401
Variable)
from biosimulators_utils.sedml.exec import exec_sed_doc as base_exec_sed_doc
from biosimulators_utils.sedml.utils import apply_changes_to_xml_model
from biosimulators_utils.simulator.utils import get_algorithm_substitution_policy
from biosimulators_utils.utils.core import raise_errors_warnings
from biosimulators_utils.warnings import warn, BioSimulatorsWarning
Expand All @@ -26,7 +28,9 @@
from kisao.utils import get_preferred_substitute_algorithm_by_ids
from lxml import etree
import cobra.io
import copy
import os
import tempfile

__all__ = [
'exec_sedml_docs_in_combine_archive',
Expand Down Expand Up @@ -110,7 +114,7 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None
Args:
task (:obj:`Task`): task
variables (:obj:`list` of :obj:`Variable`): variables that should be recorded
preprocessed_task (:obj:`object`, optional): preprocessed information about the task, including possible
preprocessed_task (:obj:`dict`, optional): preprocessed information about the task, including possible
model changes and variables. This can be used to avoid repeatedly executing the same initialization
for repeated calls to this method.
log (:obj:`TaskLog`, optional): log for the task
Expand All @@ -135,24 +139,112 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None
if preprocessed_task is None:
preprocessed_task = preprocess_sed_task(task, variables, config=config)

# modify model
raise_errors_warnings(validation.validate_model_change_types(task.model.changes, (ModelAttributeChange, )),
error_summary='Changes for model `{}` are not supported.'.format(task.model.id))
if task.model.changes:
model_etree = preprocessed_task['model']['etree']

model = copy.deepcopy(task.model)
for change in model.changes:
change.new_value = str(change.new_value)

apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=None)

model_file, model_filename = tempfile.mkstemp(suffix='.xml')
os.close(model_file)

model_etree.write(model_filename,
xml_declaration=True,
encoding="utf-8",
standalone=False,
pretty_print=False)
else:
model_filename = task.model.source

# get the model
cobra_model = cobra.io.read_sbml_model(model_filename)
if task.model.changes:
os.remove(model_filename)

variable_xpath_sbml_id_map = preprocessed_task['model']['variable_xpath_sbml_id_map']
variable_xpath_sbml_fbc_id_map = preprocessed_task['model']['variable_xpath_sbml_fbc_id_map']

# set solver
cobra_model.solver = preprocessed_task['simulation']['solver']

# Load the simulation method specified by ``sim.algorithm``
method_props = preprocessed_task['simulation']['method_props']
method_kw_args = copy.copy(preprocessed_task['simulation']['method_kw_args'])

# encode variables into arguments of the simulation methods
apply_variables_to_simulation_method_args(variable_xpath_sbml_id_map, method_props, variables, method_kw_args)

# execute simulation
with GurobiLicenseManager():
solution = method_props['method'](cobra_model, **method_kw_args)

# check that solution was optimal
if method_props['check_status'] and solution.status != 'optimal':
raise cobra.exceptions.OptimizationError("A solution could not be found. The solver status was `{}`.".format(
solution.status))

if method_props['kisao_id'] in ['KISAO_0000527', 'KISAO_0000528']:
solution.objective_value = cobra_model.optimize().objective_value

# Get the results of each variable
variable_results = get_results_of_variables(variable_xpath_sbml_id_map, variable_xpath_sbml_fbc_id_map,
preprocessed_task['model']['active_objective_sbml_fbc_id'],
method_props, variables, solution)

# log action
if config.LOG:
log.algorithm = preprocessed_task['simulation']['algorithm_kisao_id'],
log.simulator_details = {
'method': method_props['method'].__module__ + '.' + method_props['method'].__name__,
'arguments': method_kw_args,
}

# Return the results of each variable and log
return variable_results, log


def preprocess_sed_task(task, variables, config=None):
""" Preprocess a SED task, including its possible model changes and variables. This is useful for avoiding
repeatedly initializing tasks on repeated calls of :obj:`exec_sed_task`.
Args:
task (:obj:`Task`): task
variables (:obj:`list` of :obj:`Variable`): variables that should be recorded
config (:obj:`Config`, optional): BioSimulators common configuration
Returns:
:obj:`dict`: preprocessed information about the task
"""
config = config or get_config()

model = task.model
sim = task.simulation

# validate simulation
if config.VALIDATE_SEDML:
raise_errors_warnings(validation.validate_model_language(model.language, ModelLanguage.SBML),
error_summary='Language for model `{}` is not supported.'.format(model.id))
raise_errors_warnings(validation.validate_model_change_types(model.changes, ()),
raise_errors_warnings(validation.validate_model_change_types(model.changes, (ModelAttributeChange, )),
error_summary='Changes for model `{}` are not supported.'.format(model.id))
raise_errors_warnings(validation.validate_simulation_type(sim, (SteadyStateSimulation, )),
error_summary='{} `{}` is not supported.'.format(sim.__class__.__name__, sim.id))

if not os.path.isfile(model.source):
# check model source exists
if model.source and not os.path.isfile(model.source):
raise FileNotFoundError('Model source `{}` is not a file.'.format(model.source))
model_etree = etree.parse(model.source)
target_x_paths_ids = validation.validate_target_xpaths(
variables, model_etree, attr='id')
namespaces = get_namespaces_for_xml_doc(model_etree)
target_x_paths_fbc_ids = validation.validate_target_xpaths(

# preprocess variables
variable_xpath_sbml_id_map = validation.validate_target_xpaths(
variables, model_etree, attr='id')
variable_xpath_sbml_fbc_id_map = validation.validate_target_xpaths(
variables,
model_etree,
attr={
Expand All @@ -168,10 +260,9 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None
cobra_model = cobra.io.read_sbml_model(model.source)

# get the SBML-FBC id of the active objective
active_objective_fbc_id = get_active_objective_sbml_fbc_id(model.source)
active_objective_sbml_fbc_id = get_active_objective_sbml_fbc_id(model.source)

# Load the simulation method specified by ``sim.algorithm``
algorithm_kisao_id = sim.algorithm.kisao_id
algorithm_substitution_policy = get_algorithm_substitution_policy(config=config)
exec_kisao_id = get_preferred_substitute_algorithm_by_ids(
sim.algorithm.kisao_id, KISAO_ALGORITHMS_PARAMETERS_MAP.keys(),
Expand All @@ -180,7 +271,7 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None

# set up method parameters specified by ``simulation.algorithm.changes``
method_kw_args = {}
if exec_kisao_id == algorithm_kisao_id:
if exec_kisao_id == sim.algorithm.kisao_id:
for method_arg_change in sim.algorithm.changes:
try:
set_simulation_method_arg(method_props, method_arg_change, cobra_model, method_kw_args)
Expand All @@ -205,49 +296,25 @@ def exec_sed_task(task, variables, preprocessed_task=None, log=None, config=None
else:
raise

solver_change = next((change for change in sim.algorithm.changes if change.kisao_id == 'KISAO_0000553'), None)
if solver_change is None and GurobiLicenseManager().is_package_available():
cobra_model.solver = 'gurobi'

# validate variables
validate_variables(method_props, variables)

# encode variables into arguments of the simulation metods
apply_variables_to_simulation_method_args(target_x_paths_ids, method_props, variables, method_kw_args)

# execute simulation
solution = method_props['method'](cobra_model, **method_kw_args)

# check that solution was optimal
if method_props['check_status'] and solution.status != 'optimal':
raise cobra.exceptions.OptimizationError("A solution could not be found. The solver status was `{}`.".format(
solution.status))

if method_props['kisao_id'] in ['KISAO_0000527', 'KISAO_0000528']:
solution.objective_value = cobra_model.optimize().objective_value

# Get the results of each variable
variable_results = get_results_of_variables(target_x_paths_ids, target_x_paths_fbc_ids,
active_objective_fbc_id, method_props, variables, solution)

# log action
if config.LOG:
log.algorithm = exec_kisao_id
log.simulator_details = {
'method': method_props['method'].__module__ + '.' + method_props['method'].__name__,
'arguments': method_kw_args,
# Return processed information about the task
return {
'model': {
'etree': model_etree,
'active_objective_sbml_fbc_id': active_objective_sbml_fbc_id,
'variable_xpath_sbml_id_map': variable_xpath_sbml_id_map,
'variable_xpath_sbml_fbc_id_map': variable_xpath_sbml_fbc_id_map,
},
'simulation': {
'algorithm_kisao_id': exec_kisao_id,
'method_props': method_props,
'method_kw_args': method_kw_args,
'solver': cobra_model.solver,
}

# Return the results of each variable and log
return variable_results, log


def preprocess_sed_task(task, variables, config=None):
""" Preprocess a SED task, including its possible model changes and variables. This is useful for avoiding
repeatedly initializing tasks on repeated calls of :obj:`exec_sed_task`.
Args:
task (:obj:`Task`): task
variables (:obj:`list` of :obj:`Variable`): variables that should be recorded
config (:obj:`Config`, optional): BioSimulators common configuration
Returns:
:obj:`object`: preprocessed information about the task
"""
pass
}
2 changes: 2 additions & 0 deletions requirements.optional.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[gurobi]
gurobipy
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
biosimulators_utils[logging] >= 0.1.116
biosimulators_utils[logging] >= 0.1.119
cobra
kisao
lxml
Expand Down
43 changes: 43 additions & 0 deletions tests/test_core_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,49 @@ def test_exec_sed_task_successfully(self):
for var_id, result in variable_results.items():
numpy.testing.assert_allclose(result, numpy.array(expected_results[var_id]), rtol=1e-4, atol=1e-8)

def test_exec_sed_task_with_changes(self):
task = sedml_data_model.Task(
model=sedml_data_model.Model(
source=os.path.join(os.path.dirname(__file__), 'fixtures', 'textbook.xml'),
language=sedml_data_model.ModelLanguage.SBML.value,
),
simulation=sedml_data_model.SteadyStateSimulation(
algorithm=sedml_data_model.Algorithm(
kisao_id='KISAO_0000437',
),
),
)

variables = [
sedml_data_model.Variable(
id='active_objective',
target="/sbml:sbml/sbml:model/fbc:listOfObjectives/fbc:objective[@fbc:id='obj']/@value",
target_namespaces=self.NAMESPACES,
task=task),
]

preprocessed_task = core.preprocess_sed_task(task, variables)

results, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task)
numpy.testing.assert_allclose(results['active_objective'].tolist(), 0.8739215069684301, rtol=1e-4, atol=1e-8)

task.model.changes.append(sedml_data_model.ModelAttributeChange(
target="/sbml:sbml/sbml:model/sbml:listOfParameters/sbml:parameter[@id='R_EX_glc__D_e_lower_bound']/@value",
target_namespaces=self.NAMESPACES,
new_value=-1,
))
results2, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task)
self.assertLess(results2['active_objective'].tolist(), results['active_objective'].tolist())

task.model.changes[-1].new_value = '-2'
results3, _ = core.exec_sed_task(task, variables, preprocessed_task=preprocessed_task)
self.assertLess(results3['active_objective'].tolist(), results['active_objective'].tolist())
self.assertGreater(results3['active_objective'].tolist(), results2['active_objective'].tolist())

task.model.source = 'not a file'
with self.assertRaises(FileNotFoundError):
core.preprocess_sed_task(task, variables)

def test_exec_sed_task_error_handling(self):
# unsupported algorithm
with self.assertRaisesRegex(ValueError, 'invalid KiSAO id'):
Expand Down

0 comments on commit 0fc3e97

Please sign in to comment.