diff --git a/.travis.yml b/.travis.yml index dc7560a79..bcbee6197 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,6 @@ matrix: env: TOXENV=py36 - python: '3.7' env: TOXENV=py37 - - python: '3.8' - env: TOXENV=py38 branches: only: diff --git a/cameo/core/strain_design.py b/cameo/core/strain_design.py index 82476bf36..71a72f724 100644 --- a/cameo/core/strain_design.py +++ b/cameo/core/strain_design.py @@ -23,7 +23,6 @@ from cameo.core.result import Result from cameo.core.target import EnsembleTarget, Target -from cameo.visualization.plotting import plotter from gnomic import Genotype @@ -180,7 +179,7 @@ def __add__(self, other): def _repr_html_(self): return self.data_frame._repr_html_() - def plot(self, grid=None, width=None, height=None, title=None, *args, **kwargs): + def plot(self, plotter, grid=None, width=None, height=None, title=None, *args, **kwargs): if title is None: title = "Target frequency plot for %s result" % self.__method_name__ diff --git a/cameo/flux_analysis/analysis.py b/cameo/flux_analysis/analysis.py index 5502aa8c0..42a506d52 100644 --- a/cameo/flux_analysis/analysis.py +++ b/cameo/flux_analysis/analysis.py @@ -29,10 +29,9 @@ from cobra.util import fix_objective_as_constraint, get_context from cobra.exceptions import OptimizationError from numpy import trapz -from sympy import S from optlang.interface import UNBOUNDED, OPTIMAL +from optlang.symbolics import Zero -import cameo from cameo import config from cameo.core.result import Result from cameo.flux_analysis.util import remove_infeasible_cycles, fix_pfba_as_constraint @@ -40,7 +39,6 @@ from cameo.ui import notice from cameo.util import partition, _BIOMASS_RE_ from cameo.core.utils import get_reaction_for -from cameo.visualization.plotting import plotter logger = logging.getLogger(__name__) @@ -203,8 +201,9 @@ def flux_variability_analysis(model, reactions=None, fraction_of_optimum=0., pfb Returns ------- - pandas.DataFrame - Pandas DataFrame containing the results of the flux variability analysis. + FluxVariabilityResult + FluxVariabilityResult with DataFrame data_frame property containing the results of the flux variability + analysis. """ if view is None: @@ -357,7 +356,7 @@ def _flux_variability_analysis(model, reactions=None): fva_sol = OrderedDict() lb_flags = dict() with model: - model.objective = S.Zero + model.objective = Zero model.objective.direction = 'min' for reaction in reactions: @@ -760,7 +759,7 @@ def __init__(self, phase_plane, variable_ids, objective, def data_frame(self): return pandas.DataFrame(self._phase_plane) - def plot(self, grid=None, width=None, height=None, title=None, axis_font_size=None, palette=None, + def plot(self, plotter, grid=None, width=None, height=None, title=None, axis_font_size=None, palette=None, points=None, points_colors=None, estimate='flux', **kwargs): """plot phenotypic phase plane result @@ -882,7 +881,7 @@ def __init__(self, data_frame, *args, **kwargs): def data_frame(self): return self._data_frame - def plot(self, index=None, grid=None, width=None, height=None, title=None, palette=None, **kwargs): + def plot(self, plotter, index=None, grid=None, width=None, height=None, title=None, palette=None, **kwargs): if index is None: index = self.data_frame.index[0:10] fva_result = self.data_frame.loc[index] diff --git a/cameo/flux_analysis/simulation.py b/cameo/flux_analysis/simulation.py index a083ea487..8f2c66f98 100644 --- a/cameo/flux_analysis/simulation.py +++ b/cameo/flux_analysis/simulation.py @@ -48,8 +48,8 @@ add = Add._from_args mul = Mul._from_args -NegativeOne = sympy.singleton.S.NegativeOne -One = sympy.singleton.S.One +NegativeOne = sympy.S.NegativeOne +One = sympy.S.One FloatOne = sympy.Float(1) RealNumber = sympy.RealNumber diff --git a/cameo/strain_design/deterministic/flux_variability_based.py b/cameo/strain_design/deterministic/flux_variability_based.py index 1f07705a6..a171f16e3 100644 --- a/cameo/strain_design/deterministic/flux_variability_based.py +++ b/cameo/strain_design/deterministic/flux_variability_based.py @@ -37,7 +37,6 @@ from cobra import Reaction, Metabolite from cobra.util import fix_objective_as_constraint -from cameo.visualization.plotting import plotter from cameo import config from cameo.ui import notice @@ -633,7 +632,7 @@ def plot(self, index=None, variables=None, grid=None, width=None, height=None, t else: self._plot_production_envelope(title=title, grid=grid, width=width, height=height) - def _plot_flux_variability_analysis(self, index, variables=None, title=None, + def _plot_flux_variability_analysis(self, plotter, index, variables=None, title=None, width=None, height=None, palette=None, grid=None): if variables is None: variables = self.reference_fva.index[0:10] @@ -1008,7 +1007,7 @@ class FSEOFResult(StrainDesignMethodResult): __method_name__ = "FSEOF" - def plot(self, grid=None, width=None, height=None, title=None, *args, **kwargs): + def plot(self, plotter, grid=None, width=None, height=None, title=None, *args, **kwargs): if title is None: title = "FSEOF fluxes" diff --git a/cameo/strain_design/deterministic/linear_programming.py b/cameo/strain_design/deterministic/linear_programming.py index 8663df352..6c3137f97 100644 --- a/cameo/strain_design/deterministic/linear_programming.py +++ b/cameo/strain_design/deterministic/linear_programming.py @@ -41,7 +41,6 @@ from cameo.flux_analysis.simulation import fba from cameo.flux_analysis.structural import find_coupled_reactions_nullspace from cameo.util import reduce_reaction_set, decompose_reaction_groups -from cameo.visualization.plotting import plotter logger = logging.getLogger(__name__) @@ -385,7 +384,7 @@ def display_on_map(self, index=0, map_name=None, palette="YlGnBu"): fluxes = fba(self._model) fluxes.display_on_map(map_name=map_name, palette=palette) - def plot(self, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): + def plot(self, plotter, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): wt_production = phenotypic_phase_plane(self._model, objective=self._target, variables=[self._biomass.id]) with self._model: for ko in self.data_frame.loc[index, "reactions"]: diff --git a/cameo/strain_design/heuristic/evolutionary_based.py b/cameo/strain_design/heuristic/evolutionary_based.py index 1ceeda88f..ab8878a1a 100644 --- a/cameo/strain_design/heuristic/evolutionary_based.py +++ b/cameo/strain_design/heuristic/evolutionary_based.py @@ -38,7 +38,6 @@ from cameo.strain_design.heuristic.evolutionary.processing import process_reaction_knockout_solution, \ process_gene_knockout_solution, process_reaction_swap_solution from cameo.util import TimeMachine -from cameo.visualization.plotting import plotter from cameo.core.utils import get_reaction_for __all__ = ["OptGene"] @@ -294,7 +293,7 @@ def display_on_map(self, index=0, map_name=None, palette="YlGnBu"): fluxes = self._simulation_method(self._model, **self._simulation_kwargs) fluxes.display_on_map(map_name=map_name, palette=palette) - def plot(self, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): + def plot(self, plotter, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): wt_production = phenotypic_phase_plane(self._model, objective=self._target, variables=[self._biomass]) with self._model: for ko in self.data_frame.loc[index, "reactions"]: @@ -483,7 +482,7 @@ def display_on_map(self, index=0, map_name=None, palette="YlGnBu"): fluxes = self._simulation_method(self._model, **self._simulation_kwargs) fluxes.display_on_map(map_name=map_name, palette=palette) - def plot(self, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): + def plot(self, plotter, index=0, grid=None, width=None, height=None, title=None, palette=None, **kwargs): wt_production = phenotypic_phase_plane(self._model, objective=self._target, variables=[self._biomass]) with self._model: for ko in self.data_frame.loc[index, "reactions"]: diff --git a/cameo/strain_design/pathway_prediction/pathway_predictor.py b/cameo/strain_design/pathway_prediction/pathway_predictor.py index aa211d87a..bc36fd816 100644 --- a/cameo/strain_design/pathway_prediction/pathway_predictor.py +++ b/cameo/strain_design/pathway_prediction/pathway_predictor.py @@ -39,7 +39,6 @@ from cameo.data import metanetx from cameo.strain_design.pathway_prediction import util from cameo.util import TimeMachine -from cameo.visualization.plotting import plotter __all__ = ['PathwayPredictor'] @@ -172,7 +171,9 @@ def plot(self, grid=None, width=None, height=None, title=None): # TODO: small pathway visualizations would be great. raise NotImplementedError - def plot_production_envelopes(self, model, objective=None, title=None): + def plot_production_envelopes( + self, plotter, model, objective=None, title=None + ): rows = int(ceil(len(self.pathways) / 2.0)) title = "Production envelops for %s" % self.pathways[0].product.name if title is None else title grid = plotter.grid(n_rows=rows, title=title) diff --git a/cameo/visualization/plotting/__init__.py b/cameo/visualization/plotting/__init__.py index b84fa1039..a46aea2a3 100644 --- a/cameo/visualization/plotting/__init__.py +++ b/cameo/visualization/plotting/__init__.py @@ -11,68 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import - -from warnings import warn - -from cameo.visualization.plotting.abstract import AbstractPlotter - -__all__ = ["plotter"] - -_engine = None -_engines = {} - -try: - from cameo.visualization.plotting.with_bokeh import BokehPlotter - - _engine = BokehPlotter() if _engine is None else _engine - _engines["bokeh"] = BokehPlotter -except ImportError: - pass - -try: - from cameo.visualization.plotting.with_plotly import PlotlyPlotter - - _engine = PlotlyPlotter(mode='offline') if _engine is None else _engine - _engines["plotly"] = PlotlyPlotter -except ImportError: - pass - -try: - from cameo.visualization.plotting.with_plotly import GGPlotPlotter - - _engine = GGPlotPlotter() if _engine is None else _engine - _engines["ggplot"] = GGPlotPlotter -except ImportError: - pass - -if _engine is None: - warn("Cannot import any plotting library. Please install one of 'plotly', 'bokeh' or 'ggplot' if you want to" - " use any plotting function.") - _engine = AbstractPlotter() - - -class _plotting: - def __init__(self, engine): - self.__dict__['_engine'] = engine - - def __getattr__(self, item): - if item not in ["_engine", "engine"]: - return getattr(self.__dict__['_engine'], item) - else: - return self.__dict__['_engine'] - - def __setattr__(self, key, item): - if key not in ["_engine", "engine"]: - raise KeyError(key) - else: - if isinstance(item, str): - item = _engines[item]() - - if not isinstance(item, AbstractPlotter): - raise AssertionError("Invalid engine %s" % item) - - self.__dict__['_engine'] = item - - -plotter = _plotting(_engine) diff --git a/cameo/visualization/plotting/with_bokeh.py b/cameo/visualization/plotting/with_bokeh.py deleted file mode 100644 index 5ba09a697..000000000 --- a/cameo/visualization/plotting/with_bokeh.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright 2016 Novo Nordisk Foundation Center for Biosustainability, DTU. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import absolute_import - -from bokeh.charts import Line, Bar -from bokeh.models import FactorRange -from bokeh.layouts import gridplot -from bokeh.plotting import figure, show - -from cameo.util import partition, inheritdocstring, in_ipnb -from cameo.visualization.plotting.abstract import AbstractPlotter - - -class BokehPlotter(AbstractPlotter, metaclass=inheritdocstring): - def __init__(self, **options): - if in_ipnb(): - from bokeh.io import output_notebook - output_notebook(hide_banner=True) - super(BokehPlotter, self).__init__(**options) - - def _add_fva_bars(self, plot, factors, dataframe, height, step, color, strain): - alpha = self.get_option('alpha') - - left = dataframe.ub.tolist() - right = dataframe.lb.tolist() - bottom = [(i + 1.5) + height for i in range(len(factors))] - top = [(i + 1.5) + height + step for i in range(len(factors))] - - plot.quad(top=top, bottom=bottom, left=left, right=right, color=color, legend=strain, fill_alpha=alpha) - - def flux_variability_analysis(self, dataframe, grid=None, width=None, height=None, title=None, - palette=None, x_axis_label=None, y_axis_label=None): - palette = self.get_option('palette') if palette is None else palette - width = self.get_option('width') if width is None else width - strains = dataframe["strain"].unique() - factors = dataframe['reaction'].unique().tolist() - x_range = [min(dataframe.lb) - 5, max(dataframe.ub) + 5] - - width, height = self.golden_ratio(width, height) - - plot = figure(title=title, plot_width=width, plot_height=height, - x_range=x_range, y_range=FactorRange(factors=[""] + factors + [""]), - min_border=5) - - plot.line([0, 0], [0, len(factors) + 3], line_color="black", line_width=1.5, line_alpha=1) - - n = len(strains) - step = 1.0 / float(len(strains)) - for strain, i, color in zip(strains, range(n), self._palette(palette, n)): - _dataframe = dataframe[dataframe["strain"] == strain] - self._add_fva_bars(plot, factors, _dataframe, i * step, step, color, strain) - - if x_axis_label: - plot.xaxis.axis_label = x_axis_label - if y_axis_label: - plot.yaxis.axis_label = y_axis_label - - return plot - - def _add_production_envelope(self, plot, dataframe, strain, color=None): - patch_alpha = self.get_option('alpha') - ub = dataframe["ub"].values.tolist() - lb = dataframe["lb"].values.tolist() - var = dataframe["value"].values - - x = [v for v in var] + [v for v in reversed(var)] - y = [v for v in lb] + [v for v in reversed(ub)] - - plot.patch(x=x, y=y, fill_color=color, fill_alpha=patch_alpha, legend=strain) - - if "label" in dataframe.columns: - plot.text(dataframe["label"].values, var, ub) - - plot.line(var, ub, color=color) - plot.line(var, lb, color=color) - if ub[-1] != lb[-1]: - plot.line((var[-1], var[-1]), (ub[-1], lb[-1]), color=color) - - def production_envelope(self, dataframe, grid=None, width=None, height=None, title=None, points=None, - points_colors=None, palette='RdYlBu', x_axis_label=None, y_axis_label=None): - strains = dataframe["strain"].unique() - palette = self.get_option('palette') if palette is None else palette - width = self.get_option('width') if width is None else width - if not height: - width, height = self.golden_ratio(width, height) - - plot = figure(title=title, plot_width=width, plot_height=height) - for strain, color in zip(strains, self._palette(palette, len(strains))): - _dataframe = dataframe[dataframe["strain"] == strain] - self._add_production_envelope(plot, _dataframe, strain, color=color) - - if points is not None: - plot.scatter(*zip(*points), color="green" if points_colors is None else points_colors) - - if x_axis_label: - plot.xaxis.axis_label = x_axis_label - if y_axis_label: - plot.yaxis.axis_label = y_axis_label - - if grid is not None: - grid.append(plot) - return grid - - return plot - - def line(self, dataframe, width=None, height=None, palette=None, title="Line", - x_axis_label=None, y_axis_label=None, grid=None): - - palette = self.get_option('palette') if palette is None else palette - width = self.get_option('width') if width is None else width - - if not height: - width, height = self.golden_ratio(width, height) - - palette = self._palette(palette, len(dataframe.index)) - - line = Line(dataframe.T, legend="top_right", color=palette, ylabel=y_axis_label, xlabel=y_axis_label, - title=title, width=width, height=height) - - if grid is not None: - grid.append(line) - return grid - - return line - - def frequency(self, dataframe, width=None, height=None, palette=None, title="Frequency plot", - x_axis_label=None, y_axis_label="Frequency", grid=None): - palette = self.get_option('palette') if palette is None else palette - width = self.get_option('width') if width is None else width - - if not height: - width, height = self.golden_ratio(width, height) - - palette = self._palette(palette, len(dataframe.index)) - - bar = Bar(dataframe, color=palette, values='frequency', ylabel=y_axis_label, xlabel=x_axis_label, - title=title, width=width, height=height) - - if grid is not None: - grid.append(bar) - return grid - - return bar - - - @property - def _display(self): - return show - - @staticmethod - def _make_grid(grid): - return gridplot(children=partition(grid.plots, grid.n_rows), - name=grid.title) diff --git a/cameo/visualization/plotting/with_ggplot.py b/cameo/visualization/plotting/with_ggplot.py deleted file mode 100644 index f80416761..000000000 --- a/cameo/visualization/plotting/with_ggplot.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2015 Novo Nordisk Foundation Center for Biosustainability, DTU. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import absolute_import - -from math import ceil - -from warnings import warn -from ggplot import scale_colour_manual, geom_area, geom_tile, scale_x_continuous, scale_y_continuous, aes, facet_grid - -from cameo.util import in_ipnb, inheritdocstring -from cameo.visualization.plotting import AbstractPlotter - - -class GGPlotPlotter(AbstractPlotter, metaclass=inheritdocstring): - def __init__(self, **options): - warn("ggplot interface is under construction...") - - super(GGPlotPlotter, self).__init__(**options) - - def production_envelope(self, dataframe, grid=None, width=None, height=None, title=None, points=None, - points_colors=None, palette=None, x_axis_label=None, y_axis_label=None): - - palette = self.get_option('palette') if palette is None else palette - width = self.get_option('width') if width is None else width - colors = self._palette(palette, len(dataframe.strain.unique())) - - plot = aes(data=dataframe, ymin="lb", ymax="ub", x="value", color=scale_colour_manual(colors)) + geom_area() - if title: - plot += geom_tile(title) - if x_axis_label: - plot += scale_x_continuous(name=x_axis_label) - if y_axis_label: - plot += scale_y_continuous(name=y_axis_label) - - return plot - - def flux_variability_analysis(self, dataframe, grid=None, width=None, height=None, title=None, palette=None, - x_axis_label=None, y_axis_label=None): - - return aes(data=dataframe, ) - - @property - def _display(self): - if in_ipnb(): - from IPython.display import display - return display - - @staticmethod - def _make_grid(grid): - columns = ceil(grid.n_rows / len(grid.plots())) - return grid.plot[0] + facet_grid(grid.n_rows, columns, scales="fixed") diff --git a/cameo/visualization/plotting/with_plotly.py b/cameo/visualization/plotting/with_plotly.py index b4c47d88c..046910a8f 100644 --- a/cameo/visualization/plotting/with_plotly.py +++ b/cameo/visualization/plotting/with_plotly.py @@ -16,7 +16,7 @@ import math -import plotly.graph_objs as go +import plotly.graph_objects as go from plotly import tools from cameo.util import zip_repeat, in_ipnb, inheritdocstring, partition diff --git a/setup.py b/setup.py index c6b9d1a39..dd5905cc5 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ extra_requirements = { 'docs': ['Sphinx>=1.3.5', 'numpydoc>=0.5'], - 'plotly': ['plotly>=3.0.0'], + 'plotly': ['plotly>=4.12.0'], 'bokeh': ['bokeh<=0.12.1'], 'jupyter': ['jupyter>=1.0.0', 'ipywidgets>=4.1.1'], 'test': ['pytest', 'pytest-cov', 'pytest-benchmark'], diff --git a/tests/test_visualization.py b/tests/test_visualization.py new file mode 100644 index 000000000..5e0b8f351 --- /dev/null +++ b/tests/test_visualization.py @@ -0,0 +1,29 @@ +# Copyright 2019 Novo Nordisk Foundation Center for Biosustainability, DTU. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from cameo.flux_analysis.analysis import phenotypic_phase_plane +from cameo.visualization.plotting.with_plotly import PlotlyPlotter + +plotter = PlotlyPlotter() + + +def test_ppp_plotly(model): + """Test if at least it doesn't raise an exception.""" + production_envelope = phenotypic_phase_plane( + model, + variables=[model.reactions.Biomass_Ecoli_core_N_lp_w_fsh_GAM_rp__Nmet2], + objective=model.metabolites.succ_e, + ) + # pass a grid argument so that it doesn't open a browser tab + production_envelope.plot(plotter, height=400, grid=[]) diff --git a/tox.ini b/tox.ini index 63d1d3cb6..83cc306a7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ deps = pytest pytest-cov pytest-benchmark + plotly whitelist_externals = obabel setenv =