# -*- coding: utf-8 -*-
"""Provide unified interfaces for optimization solutions for concentrations.
Based on solution implementations in :mod:`cobra.core.solution`
"""
import pandas as pd
from cobra.util.solver import check_solver_status
from numpy import array, exp, nan
from optlang.interface import OPTIMAL
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.pd.Series
Contains the metabolite concentrations which are the primal values
of metabolite variables.
concentration_reduced_costs : pandas.pd.Series
Contains metabolite reduced costs, which are the dual values of
metabolites variables.
Keqs : pandas.pd.Series
Contains the reaction equilibrium constant values, which are primal
values of Keq variables.
Keq_reduced_costs : pandas.pd.Series
Contains reaction equilibrium constant reduced costs, which are the
dual values of Keq variables.
shadow_prices : pandas.pd.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.pd.DataFrame` of concs. and reduced costs."""
return pd.DataFrame(
{
"concentrations": self.concentrations,
"reduced_costs": self.concentration_reduced_costs,
}
)
[docs] def Keqs_to_frame(self):
"""Get a :class:`pandas.pd.DataFrame` of Keqs and reduced costs."""
return pd.DataFrame(
{"Keqs": self.Keqs, "reduced_costs": self.Keq_reduced_costs}
)
[docs] def to_frame(self):
"""Get a :class:`pandas.pd.DataFrame` of variables and reduced costs."""
return pd.DataFrame(
{
"variables": pd.concat((self.concentrations, self.Keqs)),
"reduced_costs": pd.concat(
(self.concentration_reduced_costs, 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 pd.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,
pd.Series(concs, metabolites, name="concentrations"),
pd.Series(Keqs, Keq_ids, name="Keqs"),
pd.Series(reduced_concs, metabolites, name="concentration_reduced_costs"),
pd.Series(reduced_Keqs, Keq_ids, name="Keq_reduced_costs"),
pd.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",
)