Source code for mass.core.mass_solution

# -*- coding: utf-8 -*-
r"""MassSolution is a class for storing the simulation results.

After a :class:`~.Simulation` is used to simulate a :mod:`mass` model,
:class:`MassSolution`\ s are created to store the results computed over the
time interval specified when simulating. These results are divided into two
categories:

    * Concentration solutions (abbreviated as ConcSols)
    * Reaction Flux solutions (abbreviated as FluxSols)

:class:`MassSolution`\ s are therefore given an identifier of the
following format: ``{id_or_model}_{solution_type}Sols`` where ``id_or_model``
and ``solution_type`` correspond to the simulated model and resulting solution
category, respectively.

All solutions in a :class:`MassSolution` can be accessed via attribute
accessors. A :class:`MassSolution` also contains standard ``dict`` methods.

All functions from the :mod:`mass.visualization` submodule are designed
to work seamlessly with :class:`MassSolution`\ s, provided they are properly
created.
"""
from warnings import warn


try:
    import matplotlib.pyplot as plt
except ImportError:
    plt = None
import pandas as pd
from numpy import array
from scipy.interpolate import interp1d
from six import iteritems, iterkeys, itervalues, string_types
from sympy import Symbol, lambdify, sympify

from mass.core.mass_configuration import MassConfiguration
from mass.core.mass_model import MassModel
from mass.util.dict_with_id import DictWithID
from mass.util.util import apply_decimal_precision, ensure_iterable
from mass.visualization import plot_tiled_phase_portraits, plot_time_profile
from mass.visualization.visualization_util import _validate_visualization_packages


# Strings of valid solution types
_CONC_STR = "Conc"
_FLUX_STR = "Flux"

MASSCONFIGURATION = MassConfiguration()


[docs]class MassSolution(DictWithID): r"""Container to store the solutions for the simulation of a model. Parameters ---------- id_or_model : str, MassModel A :class:`MassModel` or a string identifier to associate with the stored solutions. If a :class:`MassModel` is provided, then the model identifier will be used. solution_type : str The type of solution being stored. Must be ``'Conc'`` or ``'flux'``. data_dict : dict A dict containing the solutions to store. If ``None`` provided then the :class:`MassSolution` will be initialized with no solutions. Solutions can be added or changed later using various ``dict`` methods (e.g. :meth:`~dict.update`). time : array-like An array-like object containing the time points used in obtaining the solutions. interpolate : bool If ``True`` then all solutions are converted and stored as :class:`scipy.interpolate.interp1d` objects. If ``False``, solutions are converted and stored as :class:`numpy.ndarray`\ s. Default value is ``False``. """ def __init__( self, id_or_model, solution_type="", data_dict=None, time=None, interpolate=False, initial_values=None, ): """Initialize MassSolution.""" if isinstance(id_or_model, MassModel): id_or_model = "{0}_{1}Sols".format(str(id_or_model), solution_type) if not isinstance(id_or_model, string_types): raise TypeError("'id_or_model' must be a MassModel instance or a string") super(MassSolution, self).__init__(id=id_or_model, data_dict=data_dict) self.solution_type = solution_type self._simulation = None self._time = time self._interpolate = interpolate if time is not None: self.interpolate = interpolate self._initial_values = {} self.initial_values = initial_values @property
[docs] def simulation(self): """Return the associated :class:`~.Simulation`.""" return getattr(self, "_simulation")
@property
[docs] def time(self): r"""Get or set the time points stored in the :class:`MassSolution`. Notes ----- If the solutions stored in the :class:`MassSolution` are :class:`numpy.ndarray`\ s and the numerical arrays of the solutions will be recomputed to correspond to the new time points using :class:`scipy.interpolate.interp1d` interpolating functions Parameters ---------- value : array-like An array-like object containing the time points used in calculating the solutions to be stored. """ return getattr(self, "_time", None)
@time.setter def time(self, value): """Set the time points that are stored in the :class:`MassSolution`.""" value = ensure_iterable(value) if self.interpolate: setattr(self, "_time", value) else: self.interpolate = True setattr(self, "_time", value) self.interpolate = False @property
[docs] def t(self): r"""Shorthand method to get or set the stored time points. Notes ----- If the solutions stored in the :class:`MassSolution` are :class:`numpy.ndarray`\ s and the numerical arrays of the solutions will be recomputed to correspond to the new time points using :class:`~scipy.interpolate.interp1d` interpolating functions Parameters ---------- value : array-like An array-like object containing the time points used in calculating the solutions to be stored. """ return self.time
@t.setter def t(self, value): """Shorthand method to set the stored time points.""" self.time = value @property
[docs] def interpolate(self): """Get or set whether solutions are stored as interpolating functions. Parameters ---------- value : bool If ``True``, solutions are stored in the :class:`MassSolution` as :class:`~scipy.interpolate.interp1d` objects. Otherwise solutions are stored as arrays. """ return getattr(self, "_interpolate", None)
@interpolate.setter def interpolate(self, value): """Set whether solutions are stored as interpolating functions.""" if not isinstance(value, bool): raise ValueError("value must be a bool") if self.time is None: warn( "No time points associated with MassSolution. Cannot convert " "between numerical arrays and interpolating functions." ) return for key, sol in iteritems(self): if value and not isinstance(sol, interp1d): self[key] = interp1d( self.time, sol, kind="cubic", fill_value="extrapolate" ) if not value and isinstance(sol, interp1d): self[key] = sol(self.time) setattr(self, "_interpolate", value) @property
[docs] def initial_values(self): """Get or set a ``dict`` of the initial values for solution variables. Notes ----- Primarily for storing the intial values used in a simulation and for calculating deviations from the initial state. Parameters ---------- initial_values : dict A ``dict`` containining the variables stored in the :class:`MassSolution` and their initial values. """ return getattr(self, "_initial_values")
@initial_values.setter def initial_values(self, initial_values): """Set the initial values for solution variables.""" new_values = {} if initial_values is not None: for key, value in iteritems(initial_values): if key not in self: continue new_values[key] = apply_decimal_precision( value, MASSCONFIGURATION.decimal_precision ) setattr(self, "_initial_values", new_values)
[docs] def view_time_profile(self, deviation=False, plot_function="loglog"): """Generate a quick view of the time profile for the solution. See :mod:`~mass.visualization` documentation for more information. Parameters ---------- deviation : bool Whether to plot time profiles as a deviation from their initial value. plot_function : str The plotting function to use. Accepted values are the following: * ``"plot"`` for a linear x-axis and a linear y-axis via :meth:`Axes.plot() <matplotlib.axes.Axes.plot>` * ``"loglog"`` for a logarithmic x-axis and a logarithmic y-axis via :meth:`Axes.loglog() <matplotlib.axes.Axes.loglog>` * ``"semilogx``" for a logarithmic x-axis and a linear y-axis via :meth:`Axes.semilogx() <matplotlib.axes.Axes.semilogx>` * ``"semilogy"`` for a linear x-axis and a logarithmic y-axis via :meth:`Axes.semilogy() <matplotlib.axes.Axes.semilogy>` Notes ----- Will clear and use the current axis (accessible via :func:`matplotlib.pyplot.gca`). """ _validate_visualization_packages("matplotlib") # Get solution type for title solution_type = "" if self.solution_type == _CONC_STR: solution_type += " Concentrations" if self.solution_type == _FLUX_STR: solution_type += " Fluxes" # Set plot options options = { "plot_function": plot_function, "grid": ("major", "x"), "title": "Time Profile for {0}{1}".format(self.id, solution_type), "xlabel": "Time", "ylabel": solution_type, "deviation": deviation, } # Get current axis and clear it ax = plt.gca() ax.cla() # Plot time profile ax = plot_time_profile(self, ax=ax, legend="right outside", **options) # Set figure size ax.get_figure().set_size_inches((6, 4))
[docs] def view_tiled_phase_portraits(self): """Generate a preview of the phase portraits for the solution. See :mod:`~mass.visualization` documentation for more information. Notes ----- Will clear and use the current axis (accessible via :func:`matplotlib.pyplot.gca`). """ _validate_visualization_packages("matplotlib") # Get current axis ax = plt.gca() ax.cla() # Set options options = { "title": ("Tiled Phase portraits for " + self.id, {"size": "large"}), "annotate_time_points": "endpoints", "annotate_time_points_color": ["r", "b"], "annotate_time_points_marker": ["o", "D"], "tile_xlabel_fontdict": {"size": "large"}, "tile_ylabel_fontdict": {"size": "large"}, "annotate_time_points_legend": "right outside", } # Plot with kwargs ax = plot_tiled_phase_portraits(self, ax=ax, **options) # Set figure size ax.get_figure().set_size_inches((7, 7))
[docs] def to_frame(self): """Return the stored solutions as a :class:`pandas.DataFrame`.""" # Ensure solutions are data points sols = dict( (k, v(self.time)) if self.interpolate else (k, v) for k, v in iteritems(self) ) # Make dataframe and set time as index try: df = pd.DataFrame.from_dict(sols) except ValueError: df = pd.DataFrame(itervalues(sols), index=iterkeys(sols)) df.index.name = "ID" else: df.index = pd.Series(self.time, name="Time") return df
[docs] def make_aggregate_solution( self, aggregate_id, equation, variables=None, parameters=None, update=True ): """Make a new aggregate variable and its solution from an equation. Parameters ---------- aggregate_id : str An identifier for the solution to be made. equation : str A string representing the equation of the new solution. variables : iterable or None Either an iterable of object identifiers or the objects themselves representing keys in the :class:`MassSolution` that are used as variables in equation. If ``None``, then all keys of the solution object are checked as variables, potentially resulting in lower performance time. parameters : dict or None A ``dict`` of additional parameters to use, where key:value pairs are the parameter identifiers and their numerical values. If ``None`` then it is assumed that there are no additional parameters in the equation. update : bool Whether to add the new solution into the :class:`MassSolution`. via the :meth:`~dict.update` method. Default is ``True``. Returns ------- solution : dict A ``dict`` containing where the key is the ``aggregate_id`` and the value is the newly created solution as the same type as the variable solutions Raises ------ SympifyError Raised if the ``equation_str`` could not be interpreted. """ if variables is None: variables = list(iterkeys(self)) else: variables = sorted([getattr(var, "_id", var) for var in variables]) invalid = [var for var in variables if var not in self] if invalid: raise ValueError("'{0!r}' not found in MassSolution".format(invalid)) local_syms = list(variables) if parameters is None: parameters = {} else: local_syms += list(parameters) equation = sympify(equation, locals={k: Symbol(k) for k in local_syms}) equation = lambdify(args=variables, expr=equation.subs(parameters)) if self.time is not None: values = array( [self[k](self.time) if self.interpolate else self[k] for k in variables] ) else: values = array([self[k] for k in variables]) solution = equation(*values) # # Return solution as type in the MassSolution if self.interpolate and self.time is not None: solution = interp1d( self.time, solution, kind="cubic", fill_value="extrapolate" ) elif solution.size == 1: solution = solution.item() solution = {aggregate_id: solution} # Update MassSolution if d if update: self.update(solution) try: self.initial_values[aggregate_id] = equation( *array([self.initial_values[k] for k in variables]) ) except KeyError: # Not all initial values have been set pass return solution
[docs] def __getattribute__(self, name): """Override of default :func:`getattr` to enable attribute accessors. Warnings -------- This method is intended for internal use only. """ if name in self: return self[name] else: return super(MassSolution, self).__getattribute__(name)
[docs] def __dir__(self): """Override of default :func:`dir` to include solution accessors. Warnings -------- This method is intended for internal use only. """ return list(iterkeys(self)) + super(MassSolution, self).__dir__()
__all__ = ("MassSolution",)