From 242654eedabb8177461779fcba98c2659f3bda2a Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Thu, 5 Aug 2021 12:30:19 +0200 Subject: [PATCH 01/10] Update pyproject.toml Bump version to 0.9.0-alpha.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a26836e..f5b4531 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "miom" -version = "0.9.0-alpha.3" +version = "0.9.0-alpha.4" description = "Mixed Integer Optimization for Metabolism" authors = ["Pablo R. Mier "] keywords = ["optimization", "LP", "MIP", "metabolism", "metabolic-networks"] From 2ee8a9cccc959edb4a0f7e332d280a1a477e6a3b Mon Sep 17 00:00:00 2001 From: LouisonF Date: Thu, 5 Aug 2021 16:37:14 +0200 Subject: [PATCH 02/10] Fix Issue #4. Add standardized solver status for PICOS and Python-MIP --- miom/miom.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/miom/miom.py b/miom/miom.py index 40334f9..34bcfa9 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -176,6 +176,11 @@ def __init__(self, flux_vars=None, indicator_vars=None, assigned_reactions=None) self._flux_vars = flux_vars self._indicator_vars = indicator_vars self._assigned_reactions = assigned_reactions + self._solver_correspondance = {'CUTOFF':'premature','ERROR':'failure', + 'FEASIBLE':'feasible','INFEASIBLE':'infeasible', + 'INT-INFEASIBLE':'infeasible','LOADED':'detached', + 'NO-SOLUTION-FOUND':'illposed','OPTIMAL':'optimal', + 'UNBOUNDED':'unbounded'} @property def indicators(self): @@ -202,6 +207,10 @@ def assigned_reactions(self): def values(self): return self.flux_values, self.indicator_values + @property + def solver_correspondance(self): + return self._solver_correspondance + class _PicosVariables(_Variables): def __init__(self): @@ -948,7 +957,7 @@ def _copy(self, **kwargs): def get_solver_status(self): solver_status = {} - solver_status['status'] = str(self.problem.status.name) + solver_status['status'] = self.variables.solver_correspondance[str(self.problem.status.name)] solver_status['objective_value'] = float(self.problem.objective_value) solver_status['solver_time_seconds'] = self.problem.search_progress_log.log[-1:][0][0] return solver_status From b31ccd3f8fe304c46dac20959f342eeebfdf6b05 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Tue, 10 Aug 2021 11:52:33 +0200 Subject: [PATCH 03/10] Update miom.py Update docstrings --- miom/miom.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/miom/miom.py b/miom/miom.py index 40334f9..efeecb4 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -398,7 +398,10 @@ def keep(self, reactions): """Force the inclusion of a list of reactions in the solution. Reactions have to be associated with positive weights in order to - keep them in the final solution. + keep them in the final solution. Note that once keep() is called, + the weights associated to the reactions selected to be kept will + not be taken into account, as they will be forced to be kept in + the solution. Args: reactions (list): List of reaction names, a binary vector @@ -441,7 +444,9 @@ def subset_selection(self, rxn_weights, eps=1e-2): state constraints and/or additional constraints on fluxes, and maximizing the weighted sum of the (absolute) weights for the successfully selected reactions (with positive weights) and the successfully removed reactions (with negative - weights) + weights). Selected reactions are forced to have an absolute flux value greater + or equal to the threshold `eps` (1e-2 by default). Removed reactions should have a + flux equal to 0. Each reaction is associated with a weight (positive or negative) provided in the parameter `rxn_weights`, and the objective is to select the reactions @@ -454,11 +459,12 @@ def subset_selection(self, rxn_weights, eps=1e-2): where $x_i$ are the indicator variables for the reactions $i$ and $w_i$ are the weights for the reactions associated to the indicator variable. Indicator variables are automatically created for each reaction associated to a non-zero - weight. Two (mutually exclusive) indicator variables are used for positive weighted reactions - that are reversible to indicate whether there is positive or negative flux + weight. Two (mutually exclusive) indicator variables are used for positive weighted + reactions that are reversible to indicate whether there is positive or negative flux through the reaction. A single indicator variable is created for positive weighted - non-reversible reactions, to indicate if the reaction is selected (has a non-zero flux) - in which case the indicator variable is 1, or 0 otherwise. + non-reversible reactions, to indicate if the reaction is selected (has a non-zero + flux greater or equal to `eps`) in which case the indicator variable is 1, + or 0 otherwise. A single binary indicator variable is also created for negative weighted reactions, indicating whether the reaction was not selected (i.e, has 0 flux, in which case the @@ -476,7 +482,8 @@ def subset_selection(self, rxn_weights, eps=1e-2): # Calculate min valid EPS based on integrality tolerance min_eps = self._options["_min_eps"] if eps < min_eps: - warnings.warn(f"The minimum epsilon value is {min_eps}, which is less than {eps}.") + warnings.warn(f"The minimum epsilon value for the current solver \ + parameters is {min_eps}, which is less than {eps}.") eps = max(eps, min_eps) if not isinstance(rxn_weights, Iterable): rxn_weights = [rxn_weights] * self.network.num_reactions @@ -520,6 +527,13 @@ def add_constraints(self, constraints): @_autochain def add_constraint(self, constraint): + """Add a specific constraint to the model. + + The constraint should use existing variables already included in the model. + + Args: + constraint: affine expression using model's variables. + """ pass @_autochain @@ -708,6 +722,13 @@ def get_fluxes(self, reactions=None): @_autochain def solve(self, verbosity=None, max_seconds=None): + """Solve the current model and assign the values to the variables of the model. + + Args: + verbosity (int, optional): Level of verbosity for the solver. + Values above 0 will force the backend to show output information of the search. Defaults to None. + max_seconds (int, optional): Max time in seconds for the search. Defaults to None. + """ pass @_autochain @@ -882,7 +903,7 @@ def _set_flux_bounds(self, *args, **kwargs): if max_flux is not None: self.variables.fluxes[i].ub = max_flux - def _subset_selection(self, rxn_weights, **kwargs): + def _subset_selection(self, *args, **kwargs): eps = kwargs["eps"] weighted_rxns = self.variables.assigned_reactions P = self.problem From 1354449de44e92d3ad4ec7d051c943382d07e803 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Tue, 10 Aug 2021 12:19:11 +0200 Subject: [PATCH 04/10] Fix mkdocs --- .github/workflows/deploy-mkdocs.yml | 2 +- docs/overrides/main.html | 15 ++++++--------- mkdocs.yml | 4 ++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy-mkdocs.yml b/.github/workflows/deploy-mkdocs.yml index 8253bb0..a05fc72 100644 --- a/.github/workflows/deploy-mkdocs.yml +++ b/.github/workflows/deploy-mkdocs.yml @@ -17,7 +17,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install flake8 pytest - python -m pip install mkdocs-material mkdocstrings + python -m pip install mkdocs-material mkdocstrings mkdocs-minify-plugin if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Set up Python 3.7 diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0eab152..1dc294a 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -24,22 +24,19 @@ {% endblock %} {% block scripts %} - - {% for path in config["extra_javascript"] %} - - {% endfor %} + {{ super() }} {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index f2fa9a6..f9b8e08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,10 @@ theme: custom_dir: docs/overrides plugins: - search +- minify: + minify_html: true + htmlmin_opts: + remove_comments: true - mkdocstrings: handlers: python: From 180d36f9bd370da095e18438115e495a183d7d2f Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Tue, 10 Aug 2021 12:50:41 +0200 Subject: [PATCH 05/10] Minor fixes --- miom/miom.py | 10 ++++++++++ mkdocs.yml | 2 ++ tests/test_miom.py | 3 +-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/miom/miom.py b/miom/miom.py index 579562f..f4d2bf4 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -10,6 +10,10 @@ import warnings +class Status(str, Enum): + # TODO: Use common status for PICOS and PythonMIP + pass + class Solvers(str, Enum): """Solvers supported by the miom module. @@ -811,11 +815,15 @@ def _subset_selection(self, *args, **kwargs): def _solve(self, **kwargs): max_seconds = kwargs["max_seconds"] if "max_seconds" in kwargs else None verbosity = kwargs["verbosity"] if "verbosity" in kwargs else None + init_max_seconds = self.problem.options["timelimit"] + init_verbosity = self.problem.options["verbosity"] if max_seconds is not None: self.problem.options["timelimit"] = max_seconds if verbosity is not None: self.problem.options["verbosity"] = verbosity self.solutions = self.problem.solve() + self.problem.options["timelimit"] = init_max_seconds + self.problem.options["verbosity"] = init_verbosity return True def _add_constraint(self, constraint, **kwargs): @@ -937,9 +945,11 @@ def _subset_selection(self, *args, **kwargs): def _solve(self, **kwargs): max_seconds = kwargs["max_seconds"] if "max_seconds" in kwargs else 10 * 60 verbosity = kwargs["verbosity"] if "verbosity" in kwargs else None + init_verbosity = self.problem.verbose if verbosity is not None: self.setup(verbosity=verbosity) solutions = self.problem.optimize(max_seconds=max_seconds) + self.setup(verbosity=init_verbosity) return True def _add_constraint(self, constraint, **kwargs): diff --git a/mkdocs.yml b/mkdocs.yml index f9b8e08..09a40b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,8 @@ copyright: Copyright © 2021 INRAE Toxalim theme: name: material custom_dir: docs/overrides + features: + - navigation.instant plugins: - search - minify: diff --git a/tests/test_miom.py b/tests/test_miom.py index a363ca1..ec554aa 100644 --- a/tests/test_miom.py +++ b/tests/test_miom.py @@ -5,7 +5,6 @@ PythonMipModel, PicosModel ) -import unittest import pytest from miom.mio import load_gem import pathlib @@ -159,7 +158,7 @@ def test_keep_rxn(model): .solve() .get_values() ) - assert True + assert abs(V[i]) > 1e-8 def test_copy(model): c = model.copy() From d8d79ac8b70167fa7080510d10357510b2d93282 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Tue, 10 Aug 2021 16:19:20 +0200 Subject: [PATCH 06/10] Compute elapsed time This fixes #8 --- examples/introduction_example.py | 1 + miom/miom.py | 43 ++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/examples/introduction_example.py b/examples/introduction_example.py index 7908936..5111949 100644 --- a/examples/introduction_example.py +++ b/examples/introduction_example.py @@ -31,3 +31,4 @@ .get_values()) print("Number of reactions with non-zero flux:", sum(abs(V) > 1e-8)) +print("Solver status:", model.get_solver_status()) diff --git a/miom/miom.py b/miom/miom.py index f4d2bf4..664a3ca 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -1,18 +1,27 @@ +import numpy as np +import warnings +import picos as pc +import mip from abc import ABC, abstractmethod from functools import wraps from collections.abc import Iterable from miom.mio import load_gem, MiomNetwork -import picos as pc -import mip from typing import NamedTuple from enum import Enum, auto -import numpy as np -import warnings +from time import perf_counter + +_STATUS_MAPPING = { + mip.OptimizationStatus.OPTIMAL: pc.modeling.solution.SS_OPTIMAL, + mip.OptimizationStatus.FEASIBLE: pc.modeling.solution.SS_FEASIBLE, + mip.OptimizationStatus.INFEASIBLE: pc.modeling.solution.SS_INFEASIBLE, + mip.OptimizationStatus.UNBOUNDED: pc.modeling.solution.PS_UNBOUNDED, + mip.OptimizationStatus.INT_INFEASIBLE: pc.modeling.solution.SS_INFEASIBLE, + mip.OptimizationStatus.NO_SOLUTION_FOUND: pc.modeling.solution.PS_ILLPOSED, + mip.OptimizationStatus.LOADED: pc.modeling.solution.VS_EMPTY, + mip.OptimizationStatus.CUTOFF: pc.modeling.solution.SS_PREMATURE +} -class Status(str, Enum): - # TODO: Use common status for PICOS and PythonMIP - pass class Solvers(str, Enum): """Solvers supported by the miom module. @@ -327,6 +336,8 @@ def __init__(self, previous_step_model=None, miom_network=None, solver_name=None self.network = miom_network self.variables = None self.objective = None + self._last_start_time = None + self.last_solver_time = None if previous_step_model is not None: self._options = previous_step_model._options if miom_network is None: @@ -742,7 +753,7 @@ def solve(self, verbosity=None, max_seconds=None): Values above 0 will force the backend to show output information of the search. Defaults to None. max_seconds (int, optional): Max time in seconds for the search. Defaults to None. """ - pass + self._last_start_time = perf_counter() @_autochain def copy(self): @@ -824,6 +835,7 @@ def _solve(self, **kwargs): self.solutions = self.problem.solve() self.problem.options["timelimit"] = init_max_seconds self.problem.options["verbosity"] = init_verbosity + self.last_solver_time = perf_counter() - self._last_start_time return True def _add_constraint(self, constraint, **kwargs): @@ -950,6 +962,7 @@ def _solve(self, **kwargs): self.setup(verbosity=verbosity) solutions = self.problem.optimize(max_seconds=max_seconds) self.setup(verbosity=init_verbosity) + self.last_solver_time = perf_counter() - self._last_start_time return True def _add_constraint(self, constraint, **kwargs): @@ -987,8 +1000,12 @@ def _copy(self, **kwargs): return PythonMipModel(previous_step_model=self) def get_solver_status(self): - solver_status = {} - solver_status['status'] = self.variables.solver_correspondance[str(self.problem.status.name)] - solver_status['objective_value'] = float(self.problem.objective_value) - solver_status['solver_time_seconds'] = self.problem.search_progress_log.log[-1:][0][0] - return solver_status + #solver_status['elapsed_seconds'] = self.problem.search_progress_log.log[-1:][0][0] + return { + "status": _STATUS_MAPPING[self.problem.status] \ + if self.problem.status in _STATUS_MAPPING \ + else pc.modeling.solution.PS_UNKNOWN, + "objective_value": self.problem.objective_value, + "elapsed_seconds": self.last_solver_time + } + From acc52581684afbce4406a920e77aaa4d05416de1 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Fri, 13 Aug 2021 09:48:41 +0200 Subject: [PATCH 07/10] Fix solver status Fixes #8 --- miom/miom.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/miom/miom.py b/miom/miom.py index 664a3ca..ea3de47 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -867,11 +867,11 @@ def _copy(self, **kwargs): return PicosModel(previous_step_model=self) def get_solver_status(self): - solver_status = {} - solver_status['status'] = str(self.solutions.claimedStatus) - solver_status['objective_value'] = float(self.problem.value) - solver_status['solver_time_seconds'] = self.solutions.searchTime - return solver_status + return { + "status": self.solutions.claimedStatus, + "objective_value": self.problem.value, + "elapsed_seconds": self.last_solver_time + } class PythonMipModel(BaseModel): From 6f7d4739b737e0a4a7fa13a8de39d369b3d1b434 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Fri, 13 Aug 2021 21:18:50 +0200 Subject: [PATCH 08/10] Calculate reaction activity from indicator vars --- examples/introduction_example.py | 6 ++- miom/miom.py | 87 +++++++++++++++++++++++--------- tests/test_miom.py | 21 ++++++-- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/examples/introduction_example.py b/examples/introduction_example.py index 5111949..c2cbbb0 100644 --- a/examples/introduction_example.py +++ b/examples/introduction_example.py @@ -30,5 +30,7 @@ # Get continuos vars (fluxes) and binary vars .get_values()) -print("Number of reactions with non-zero flux:", sum(abs(V) > 1e-8)) -print("Solver status:", model.get_solver_status()) +print("Number of reactions with an absolute flux value above 1e-8:", sum(abs(V) > 1e-8)) +print("Active reactions:", sum(1 if activity == 1 else 0 for activity in model.variables.indicator_rxn_activity)) +print("Inconsistencies:", sum(1 if activity != activity else 0 for activity in model.variables.indicator_rxn_activity)) +print("Solver status:", model.get_solver_status()) \ No newline at end of file diff --git a/miom/miom.py b/miom/miom.py index ea3de47..80b75c5 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -120,7 +120,8 @@ class ExtractionMode(str, Enum): ``` """ ABSOLUTE_FLUX_VALUE = "flux_value", - INDICATOR_VALUE = "indicator_value" + INDICATOR_VALUE = "indicator_value", + REACTION_ACTIVITY = "reaction_activity" class Comparator(str, Enum): @@ -146,6 +147,7 @@ class Comparator(str, Enum): ("type", _ReactionType) ]) + def miom(network, solver=Solvers.COIN_OR_CBC): """ Create a MIOM optimization model for a given solver. @@ -184,16 +186,12 @@ def miom(network, solver=Solvers.COIN_OR_CBC): else: return PicosModel(miom_network=network, solver_name=solver) + class _Variables(ABC): def __init__(self, flux_vars=None, indicator_vars=None, assigned_reactions=None): self._flux_vars = flux_vars self._indicator_vars = indicator_vars self._assigned_reactions = assigned_reactions - self._solver_correspondance = {'CUTOFF':'premature','ERROR':'failure', - 'FEASIBLE':'feasible','INFEASIBLE':'infeasible', - 'INT-INFEASIBLE':'infeasible','LOADED':'detached', - 'NO-SOLUTION-FOUND':'illposed','OPTIMAL':'optimal', - 'UNBOUNDED':'unbounded'} @property def indicators(self): @@ -213,6 +211,48 @@ def flux_values(self): def indicator_values(self): pass + @property + def reaction_activity(self): + """Returns a list indicating whether a reaction is active or not. + + It uses the value of the indicator variables of the subset selection + problem to indicate whether a reaction is active (has positive or + negative flux, in which case the value is 1 or -1. A value of None + indicates that the reaction has no associated indicator variable. + A value of `nan` indicates that the value of the reaction is + inconsistent after solving the subset selection problem. + + Returns: + list: list of the same length as the number of reactions in the model. + """ + activity = [None] * len(self.fluxes) + values = self.indicator_values + if values is None: + raise ValueError("The indicator values are not set. This means that \ + the problem is not a MIP (subset_selection method was not called). \ + If you want to select a subset of reactions based on a flux value, \ + use the method select_subnetwork or obtain_subnetwork instead.") + for i, rxn in enumerate(self.assigned_reactions): + curr_value = activity[rxn.index] + if rxn.type == _ReactionType.RH_POS or rxn.type == _ReactionType.RH_NEG: + v = 0 if curr_value is None else curr_value + v += values[i] + activity[rxn.index] = v + elif rxn.type == _ReactionType.RL: + if curr_value is not None: + raise ValueError("Multiple indicator variables for the same RL type reaction") + activity[rxn.index] = 1 - values[i] + # Replace inconsistent values (can happen due to numerical issues) + for i in range(len(activity)): + if activity[i] is not None and activity[i] > 1: + activity[i] = float('nan') + # Add sign + for i, rxn in enumerate(self.assigned_reactions): + if rxn.type == _ReactionType.RH_NEG and values[i] > 0: + activity[rxn.index] = -1 * activity[rxn.index] + return activity + + @property def assigned_reactions(self): return self._assigned_reactions @@ -220,10 +260,6 @@ def assigned_reactions(self): def values(self): return self.flux_values, self.indicator_values - @property - def solver_correspondance(self): - return self._solver_correspondance - class _PicosVariables(_Variables): def __init__(self): @@ -265,10 +301,10 @@ def indicator_values(self): return None -def _autochain(fn): +def _partial(fn): """Annotation for methods that return the instance itself to enable chaining. - If a method `my_method` is annotated with @_autochain, a method called `_my_method` + If a method `my_method` is annotated with @_partial, a method called `_my_method` is expected to be provided by a subclass. Parent method `my_method` is called first and the result is passed to the child method `_my_method`. @@ -287,7 +323,7 @@ def wrapper(self, *args, **kwargs): # Find subclass implementation fname = '_' + fn.__name__ if not hasattr(self, fname): - raise ValueError(f'Method "{fn.__name__}()" is marked as @_autochain ' + raise ValueError(f'Method "{fn.__name__}()" is marked as @_partial ' f'but the expected implementation "{fname}()" was not provided ' f'by {self.__class__.__name__}') func = getattr(self, fname) @@ -364,7 +400,7 @@ def initialize_problem(self): def get_solver_status(self): pass - @_autochain + @_partial def setup(self, **kwargs): """Provide the options for the solver. @@ -405,7 +441,7 @@ def setup(self, **kwargs): self._options["_min_eps"] = np.sqrt(2*self._options["int_tol"]) return self._options - @_autochain + @_partial def steady_state(self): """Add the required constraints for finding steady-state fluxes @@ -417,7 +453,7 @@ def steady_state(self): """ pass - @_autochain + @_partial def keep(self, reactions): """Force the inclusion of a list of reactions in the solution. @@ -459,7 +495,7 @@ def keep(self, reactions): if r.index in valid_rxn_idx] return dict(idxs=idxs, valid_rxn_idx=valid_rxn_idx) - @_autochain + @_partial def subset_selection(self, rxn_weights, eps=1e-2): """Transform the current model into a subset selection problem. @@ -519,7 +555,7 @@ def subset_selection(self, rxn_weights, eps=1e-2): warnings.warn("Indicator variables were already assigned") return False - @_autochain + @_partial def set_flux_bounds(self, rxn_id, min_flux=None, max_flux=None): """Change the flux bounds of a reaction. @@ -534,7 +570,7 @@ def set_flux_bounds(self, rxn_id, min_flux=None, max_flux=None): i, _ = self.network.find_reaction(rxn_id) return i - @_autochain + @_partial def add_constraints(self, constraints): """Add a list of constraint to the model @@ -549,7 +585,7 @@ def add_constraints(self, constraints): self.add_constraint(c) return len(constraints) > 0 - @_autochain + @_partial def add_constraint(self, constraint): """Add a specific constraint to the model. @@ -560,7 +596,7 @@ def add_constraint(self, constraint): """ pass - @_autochain + @_partial def set_objective(self, cost_vector, variables, direction='max'): """Set the optmization objective of the model. @@ -619,7 +655,7 @@ def set_fluxes_for(self, reactions, tolerance=1e-6): self.add_constraint(self.variables.fluxes[i] <= ub) return self - @_autochain + @_partial def reset(self): """Resets the original problem (removes all modifications) @@ -683,7 +719,7 @@ def obtain_subnetwork( S_sub = S_sub[act_met, :] return MiomNetwork(S_sub, R_sub, M_sub) - @_autochain + @_partial def select_subnetwork( self, mode=ExtractionMode.ABSOLUTE_FLUX_VALUE, @@ -721,6 +757,7 @@ def get_values(self): called) """ return self.variables.values() + def get_fluxes(self, reactions=None): """Get the flux values. @@ -744,7 +781,7 @@ def get_fluxes(self, reactions=None): else: raise ValueError("reactions should be an iterable of strings or a single string") - @_autochain + @_partial def solve(self, verbosity=None, max_seconds=None): """Solve the current model and assign the values to the variables of the model. @@ -755,7 +792,7 @@ def solve(self, verbosity=None, max_seconds=None): """ self._last_start_time = perf_counter() - @_autochain + @_partial def copy(self): pass diff --git a/tests/test_miom.py b/tests/test_miom.py index ec554aa..1eee3e7 100644 --- a/tests/test_miom.py +++ b/tests/test_miom.py @@ -63,7 +63,6 @@ def test_subset_selection(model): .solve() .get_values() ) - # Indicator variables (X) should contain... assert np.sum(X) == model.network.num_reactions - 6 def test_network_selection_using_indicators(model): @@ -127,8 +126,8 @@ def test_subset_selection_custom_weights(model): c = [-100] * model.network.num_reactions # Positive weight only for R_f_i. Should not be selected # based on the objective function and the steady state constraints - i, _ = model.network.find_reaction('R_f_i') - c[i] = 1 + #i, _ = model.network.find_reaction('R_f_i') + c[model.network.get_reaction_id('R_f_i')] = 1 V, X = ( prepare_fba(model) .subset_selection(c) @@ -144,12 +143,26 @@ def test_subset_selection_custom_weights(model): # selected. assert np.sum(X > 0.99) == np.sum(np.array(c) < 0) +def test_activity_values(model): + # Same problem as above, but now we use the activity values instead + c = [-100] * model.network.num_reactions + c[model.network.get_reaction_id('R_f_i')] = 1 + V, X = ( + prepare_fba(model) + .subset_selection(c) + .solve() + .get_values() + ) + active = sum(1 if abs(activity) == 1 else 0 for activity in model.variables.reaction_activity) + inconsistent = sum(1 if activity != activity else 0 for activity in model.variables.reaction_activity) + assert active == 0 and inconsistent == 0 + def test_keep_rxn(model): # Large negative values c = [-100] * model.network.num_reactions # Positive weight only for R_f_i. Should not be selected # based on the objective function and the steady state constraints - i, _ = model.network.find_reaction('R_f_i') + i = model.network.get_reaction_id('R_f_i') c[i] = 1 V, X = ( prepare_fba(model) From cfe3690f65e6c843073cfac867a3e89f8ff23c74 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Fri, 13 Aug 2021 21:20:20 +0200 Subject: [PATCH 09/10] Add support for versioning --- docs/overrides/main.html | 7 +++++++ mkdocs.yml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 1dc294a..412bb7d 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -41,3 +41,10 @@ {% endblock %} +{% block outdated %} + You're not viewing the latest version. + + + Click here to go to latest. + +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index 09a40b0..bcbbc6d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,8 @@ extra: analytics: provider: google property: G-5SV6WE1KMZ + version: + provider: mike extra_javascript: - https://polyfill.io/v3/polyfill.min.js?features=es6 - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js \ No newline at end of file From 4362f582c8222e64ce7d85d22eb4a2813d61b7c0 Mon Sep 17 00:00:00 2001 From: "Pablo R. Mier" Date: Fri, 13 Aug 2021 21:41:35 +0200 Subject: [PATCH 10/10] Add support for selection of subnetworks using activity --- miom/miom.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miom/miom.py b/miom/miom.py index 80b75c5..e277d09 100644 --- a/miom/miom.py +++ b/miom/miom.py @@ -697,6 +697,12 @@ def obtain_subnetwork( raise ValueError("The model does not contain indicator variables. " "You need to transform it to a subset selection problem " "by invoking subset_selection() first.") + elif extraction_mode == ExtractionMode.REACTION_ACTIVITY: + variables = self.variables.reaction_activity + if variables is None: + raise ValueError("The model does not contain reaction activity variables. " + "You need to transform it to a subset selection problem " + "by invoking subset_selection() first.") else: raise ValueError("Invalid extraction mode")