# -*- coding: utf-8 -*-
"""Provide unified interfaces for optimization solutions for concentrations.
Based on solution implementations in :mod:`cobra.core.solution`
"""
from cobra.util.solver import check_solver_status
from numpy import array, exp, nan
from optlang.interface import OPTIMAL
from pandas import DataFrame, Series, option_context
from mass.core.mass_configuration import MassConfiguration
from mass.util.util import (
_check_kwargs, apply_decimal_precision, get_public_attributes_and_methods)
MASSCONFIGURATION = MassConfiguration()
[docs]class ConcSolution:
"""A unified interface to a :class:`.ConcSolver` optimization solution.
Notes
-----
The :class:`.ConcSolution` is meant to be constructed by
:func:`get_concentration_solution` please look at that function to fully
understand the :class:`ConcSolution` class.
Attributes
----------
objective_value : float
The (optimal) value for the objective function.
status : str
The solver status related to the solution.
concentrations : pandas.Series
Contains the metabolite concentrations which are the primal values
of metabolite variables.
concentration_reduced_costs : pandas.Series
Contains metabolite reduced costs, which are the dual values of
metabolites variables.
Keqs : pandas.Series
Contains the reaction equilibrium constant values, which are primal
values of Keq variables.
Keq_reduced_costs : pandas.Series
Contains reaction equilibrium constant reduced costs, which are the
dual values of Keq variables.
shadow_prices : pandas.Series
Contains reaction shadow prices (dual values of constraints).
"""
def __init__(self, objective_value, status, concentrations, Keqs,
concentration_reduced_costs=None, Keq_reduced_costs=None,
shadow_prices=None):
"""Initialize the ConcSolution."""
super(ConcSolution, self).__init__()
# For solver objective value and status
self.objective_value = objective_value
self.status = status
# For solver variables
self.concentrations = concentrations
self.Keqs = Keqs
# For variable reduced costs
self.concentration_reduced_costs = concentration_reduced_costs
self.Keq_reduced_costs = Keq_reduced_costs
# For constraint shadow prices
self.shadow_prices = shadow_prices
[docs] def concentrations_to_frame(self):
"""Get a :class:`pandas.DataFrame` of concs. and reduced costs."""
return DataFrame({'concentrations': self.concentrations,
'reduced_costs': self.concentration_reduced_costs})
[docs] def Keqs_to_frame(self):
"""Get a :class:`pandas.DataFrame` of Keqs and reduced costs."""
return DataFrame({'Keqs': self.Keqs,
'reduced_costs': self.Keq_reduced_costs})
[docs] def to_frame(self):
"""Get a :class:`pandas.DataFrame` of variables and reduced costs."""
return DataFrame({
'variables': self.concentrations.append(self.Keqs),
'reduced_costs': self.concentration_reduced_costs.append(
self.Keq_reduced_costs)
})
[docs] def _repr_html_(self):
"""HTML representation of the overview for the ConcSolution.
Warnings
--------
This method is intended for internal use only.
"""
if self.status == OPTIMAL:
with option_context('display.max_rows', 10):
html = ('<strong><em>Optimal</em> solution with objective '
'value {:.3f}</strong><br>{}'
.format(self.objective_value,
self.to_frame()._repr_html_()))
else:
html = '<strong><em>{}</em> solution</strong>'.format(self.status)
return html
[docs] def __repr__(self):
"""Set string representation of the solution instance.
Warnings
--------
This method is intended for internal use only.
"""
if self.status != OPTIMAL:
return "<Solution {0:s} at 0x{1:x}>".format(self.status, id(self))
return "<Solution {0:.3f} at 0x{1:x}>".format(self.objective_value,
id(self))
[docs] def __getitem__(self, variable):
"""Return the value of a metabolite concentration or reaction Keq.
Parameters
----------
variable : str
A variable ID for a variable in the solution.
Warnings
--------
This method is intended for internal use only.
"""
try:
return self.concentrations[str(variable)]
except KeyError:
pass
try:
return self.Keqs[str(variable)]
except KeyError as e:
raise ValueError(
"{0!r} is not a str ID of a ConcSolution variable.".format(
str(e)))
[docs] def __dir__(self):
"""Override default dir() implementation to list only public items.
Warnings
--------
This method is intended for internal use only.
"""
return get_public_attributes_and_methods(self)
[docs] get_primal_by_id = __getitem__
[docs]def get_concentration_solution(concentration_solver, metabolites=None,
reactions=None, raise_error=False,
**kwargs):
"""Generate a solution representation of a :class:`.ConcSolver` state.
Parameters
---------
concentration_solver : ConcSolver
The :class:`.ConcSolver` containing the mathematical problem solved.
metabolites : list
An iterable of :class:`.MassMetabolite` objects.
Uses :attr:`.ConcSolver.included_metabolites` by default.
reactions : list
An iterable of :class:`.MassReaction` objects.
Uses :attr:`.ConcSolver.included_reactions` by default.
raise_error : bool
Whether to raise an OptimizationError if solver status is not optimal.
**kwargs
decimal_precision :
``bool`` indicating whether to apply the
:attr:`~.MassBaseConfiguration.decimal_precision` attribute of
the :class:`.MassConfiguration` to the solution values.
Default is ``False``.
Returns
-------
ConcSolution
The solution of the optimization as a :class:`ConcSolution` object.
"""
kwargs = _check_kwargs({
"decimal_precision": False,
}, kwargs)
check_solver_status(concentration_solver.solver.status,
raise_error=raise_error)
# Get included metabolites and reactions
metabolites = concentration_solver._get_included_metabolites(metabolites)
reactions = concentration_solver._get_included_reactions(reactions)
# Get variable IDs, metabolites, and reactions for Keqs and constraints
metabolites = [m.id for m in metabolites
if m.id in concentration_solver.variables]
Keq_ids = [r.Keq_str for r in reactions
if r.Keq_str in concentration_solver.variables]
reactions = [r.id for r in reactions
if r.id in concentration_solver.constraints]
# Get metabolite and Keq primal values
concs = array([concentration_solver.solver.primal_values[m]
for m in metabolites])
Keqs = array([concentration_solver.solver.primal_values[Keq]
for Keq in Keq_ids])
if concentration_solver.solver.is_integer:
# Fill irrelevant arrays with nan
reduced_concs = array([nan] * len(metabolites))
reduced_Keqs = array([nan] * len(Keq_ids))
shadow = array([nan] * len(reactions))
else:
# Get reduced cost values and shadow prices
reduced_concs = array([concentration_solver.solver.reduced_costs[m]
for m in metabolites])
reduced_Keqs = array([concentration_solver.solver.reduced_costs[Keq]
for Keq in Keq_ids])
shadow = array([concentration_solver.solver.shadow_prices[r]
for r in reactions])
def transform_values(arr, **kwargs):
"""Transform array from logs to linear space and round if desired."""
if kwargs.get("decimal_precision"):
arr = apply_decimal_precision(
arr, MASSCONFIGURATION.decimal_precision)
return arr
objective_value = transform_values(
exp(concentration_solver.solver.objective.value), **kwargs)
concs = transform_values(exp(concs), **kwargs)
Keqs = transform_values(exp(Keqs), **kwargs)
reduced_concs = transform_values(reduced_concs, **kwargs)
reduced_Keqs = transform_values(reduced_Keqs, **kwargs)
shadow = transform_values(shadow, **kwargs)
return ConcSolution(
objective_value, concentration_solver.solver.status,
Series(concs, metabolites, name="concentrations"),
Series(Keqs, Keq_ids, name="Keqs"),
Series(reduced_concs, metabolites, name="concentration_reduced_costs"),
Series(reduced_Keqs, Keq_ids, name="Keq_reduced_costs"),
Series(shadow, reactions, name="shadow_prices"))
[docs]def update_model_with_concentration_solution(model, concentration_solution,
concentrations=True, Keqs=True,
inplace=True):
"""Update a :mod:`mass` model with values from a :class:`ConcSolution`.
Parameters
----------
model : MassModel
A :mod:`mass` model to update with the new solution values.
concentration_solution : ConcSolution
The :class:`ConcSolution` containing the solution values to use.
concentrations : bool
Whether to update the metabolite concentrations of the model
(the :attr:`.MassMetabolite.initial_condition` values).
Keqs : bool
Whether to update the reaction equilibrium constants of the model
(the :attr:`.MassReaction.equilibrium_constant` values).
inplace : bool
Whether to modify the given model or to modify a copy of the model.
Returns
-------
MassModel
Either the given model if ``inplace=True``, or a new copy of the model
``inplace=False``.
"""
if not isinstance(concentration_solution, ConcSolution):
raise TypeError("Must be a ConcSolution object.")
if not inplace:
model = model.copy()
if concentrations:
model.update_initial_conditions(
concentration_solution.concentrations.to_dict(), verbose=False)
if Keqs:
model.update_parameters(
concentration_solution.Keqs.to_dict(), verbose=False)
return model
__all__ = (
"ConcSolution", "get_concentration_solution",
"update_model_with_concentration_solution")