Source code for utopya.eval.plotmanager

"""Implements a plotting framework based on dantro

In order to make the plotting framework specific to Utopia, this module derives
both from the dantro PlotManager and some PlotCreator classes.
"""

import contextlib
import copy
import importlib
import logging
import os
import sys
from typing import Dict, Union

import dantro
import dantro._import_tools
import dantro.plot.creators
import dantro.plot.utils
import dantro.plot_mngr

from ..model_registry import ModelInfoBundle
from ._plot_func_resolver import PlotFuncResolver
from .plotcreators import (
    MultiversePlotCreator,
    PyPlotCreator,
    UniversePlotCreator,
)

log = logging.getLogger(__name__)

# -----------------------------------------------------------------------------


[docs]class PlotManager(dantro.plot_mngr.PlotManager): """This is the Utopia-specific version of the dantro ``PlotManager``. It registers the Utopia-specific plot creators and allows for custom interface specifications, e.g. by preloading custom modules. """ CREATORS: Dict[str, type] = dict( base=dantro.plot.creators.BasePlotCreator, external=PyPlotCreator, pyplot=PyPlotCreator, universe=UniversePlotCreator, multiverse=MultiversePlotCreator, ) """Supported plot creator classes""" MODEL_PLOTS_MODULE_NAME = "model_plots" """Name under which the model-specific plots are made importable""" PLOT_FUNC_RESOLVER: type = PlotFuncResolver """The custom plot function resolver type to use.""" # .........................................................................
[docs] def __init__( self, *args, _model_info_bundle: ModelInfoBundle = None, **kwargs ): """Sets up a PlotManager. This specialization of the :py:class:`dantro.plot_mngr.PlotManager` additionally stores some utopya-specific metadata in form of a :py:class:`~utopya.model_registry.info_bundle.ModelInfoBundle` that describes the model this PlotManager is used with. That information is then used to load some additional model-specific information once a creator is invoked. Furthermore, the :py:meth:`._preload_modules` method takes care to make model-, project-, or framework-specific plot functions available. Args: *args: Positional arguments passed to :py:class:`~dantro.plot_mngr.PlotManager`. _model_info_bundle (ModelInfoBundle, optional): The internally-used argument to pass model information to the plot manager. **kwargs: Keyword arguments passed on to :py:class:`~dantro.plot_mngr.PlotManager`. """ super().__init__(*args, **kwargs) self._model_info_bundle = copy.deepcopy(_model_info_bundle) self._preload_modules()
@property def common_out_dir(self) -> str: """The common output directory of all plots that were created with this plot manager instance. This uses the plot output paths stored in the plot information dict, specifically the ``target_dir`` entry. If there was no plot information yet, the return value will be empty. """ p = os.path.commonprefix([d["target_dir"] for d in self.plot_info]) if not os.path.exists(p): p = os.path.dirname(p) return p
[docs] def plot_from_cfg( self, *args, plots_cfg: Union[str, dict] = None, **kwargs ): """Thin wrapper around parent method that shows which plot configuration file will be used. """ log.hilight( "Now creating plots for '%s' model ...", self._model_info_bundle.model_name, ) if isinstance(plots_cfg, str): log.note("Plots configuration:\n %s\n", plots_cfg) elif plots_cfg is None: log.note( "Using default plots configuration:\n %s\n", self._model_info_bundle.paths.get("default_plots"), ) return super().plot_from_cfg(*args, plots_cfg=plots_cfg, **kwargs)
[docs] def _get_plot_func_resolver(self, **init_kwargs) -> PlotFuncResolver: """Instantiates the plot function resolver object. Additionally attaches the model info bundle to the resolver, such that it can use that information for plot function lookup. """ return self.PLOT_FUNC_RESOLVER( **init_kwargs, _model_info_bundle=self._model_info_bundle )
[docs] def _get_plot_creator( self, *args, **kwargs ) -> dantro.plot.creators.BasePlotCreator: """Sets up the BasePlotCreator and attaches a model information bundle to it such that this information is available downstream. """ creator = super()._get_plot_creator(*args, **kwargs) creator._model_info_bundle = copy.deepcopy(self._model_info_bundle) return creator
[docs] def _preload_modules(self): """Pre-loads the model-, project-, and framework-specific plot function modules. This allows to execute code (like registering model-specific dantro data operations) and have them available prior to the invocation of the creator and independently from the module that contains the plot function (which may be part of dantro, for instance). Uses :py:func:`dantro._import_tools.import_module_from_path` """ import_module_from_path = dantro._import_tools.import_module_from_path # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . # A simple exception handling context @contextlib.contextmanager def exception_handling(ExcType, scope: str): try: yield except ExcType as exc: _msg = ( f"{scope.title()}-specific plot module " "could not be imported!" ) if self.raise_exc: raise ExcType( f"{_msg}\n\nError was: {exc}\n\n" "For debugging, inspect the traceback. Disable debug " "mode to ignore exception and continue, even if this " "may cause errors during plotting." ) from exc log.warning(_msg) log.caution( "This may lead to errors during plotting. " "Enable debug mode to get a traceback." ) # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . mib = self._model_info_bundle _preloaded = [] if mib is not None: log.note("Pre-loading plot modules ...") mod_path = mib.paths.get("py_plots_dir") if mod_path and os.path.exists(mod_path): with exception_handling(ImportError, "model"): log.debug(" Loading model-specific plot module ...") _ms = f"{self.MODEL_PLOTS_MODULE_NAME}.{mib.model_name}" import_module_from_path( mod_path=mod_path, mod_str=_ms, ) _preloaded.append("model") # Also do this on the project and framework level # TODO Should make module name configurable separately! See #9 project = mib.project if ( project and project.paths.py_plots_dir and project.settings.preload_project_py_plots in (None, True) ): with exception_handling(ImportError, "project"): log.debug(" Loading project-specific plot module ...") dantro._import_tools.import_module_from_path( mod_path=project.paths.py_plots_dir, mod_str=f"{self.MODEL_PLOTS_MODULE_NAME}", ) _preloaded.append("project") if ( project and project.framework_project and project.settings.preload_framework_py_plots in (None, True) ): fw = project.framework_project if fw and fw.paths.py_plots_dir: with exception_handling(ImportError, "framework"): log.debug( " Loading framework-specific plot module ..." ) import_module_from_path( mod_path=fw.paths.py_plots_dir, mod_str=f"{self.MODEL_PLOTS_MODULE_NAME}", ) _preloaded.append("framework") if _preloaded: log.remark( " Pre-loaded plot modules of: %s", ", ".join(_preloaded) ) else: log.remark( " No `py_plots_dir` available or pre-loading deactivated." )