Source code for sierra.core.graphs.summary_line

# Copyright 2018 John Harwell, All rights reserved.
#
# SPDX-License-Identifier: MIT
#
"""
Linegraph for summarizing the results of a :term:`Batch Experiment`.

Graphs one datapoint per :term:`Experiment`.
"""

# Core packages
import typing as tp
import pathlib
import logging

# 3rd party packages
import polars as pl
import holoviews as hv

# Project packages
from sierra.core import config, utils, storage, models
from . import pathset, graphutils

_logger = logging.getLogger(__name__)


[docs] def generate( # noqa: PLR0913,PLR0917 pathset: pathset.PathSet, input_stem: str, output_stem: str, medium: str, title: str, xlabel: str, ylabel: str, backend: str, legend: list[str], xticks: list[float], stats_center: str, stats_spread: str, *, xticklabels: tp.Optional[list[str]] = None, large_text: bool = False, logyscale: bool = False, ) -> bool: """Generate a linegraph from a :term:`Batch Summary Data` file. Possibly shows the 95% confidence interval or box and whisker plots, according to configuration. Attributes: paths: Set of run-time tree paths for the batch experiment. input_stem: Stem of the :term:`Batch Summary Data` file to generate a graph from. output_fpath: The absolute path to the output image file to save generated graph to. title: Graph title. xlabel: X-label for graph. ylabel: Y-label for graph. backend: The holoviews backend to use. xticks: The xticks for the graph. xticklabels: The xtick labels for the graph (can be different than the xticks; e.g., if the xticxs are 1-10 for categorical data, then then labels would be the categories). large_text: Should the labels, ticks, and titles be large, or regular size? legend: Legend for graph. logyscale: Should the Y axis be in the log2 domain ? stats_spread: The type of spread statistics to include on the graph (from ``--spread``). stats_center: The measure of centeral tendency to use as the main data input. (from ``--center``). model_root: The absolute path to the ``models/`` directory for the batch experiment. """ hv.extension(backend, inline=False, logo=False) ofile_ext = graphutils.ofile_ext(backend) input_fpath = pathset.input_root / ( input_stem + config.STATS[stats_center].spreads["none"].exts[stats_center] ) output_fpath = pathset.output_root / f"SM-{output_stem}.{ofile_ext}" if not utils.path_exists(input_fpath): _logger.debug( "Not generating <batchroot>/%s: <batchroot>/%s does not exist", output_fpath.relative_to(pathset.batchroot), input_fpath.relative_to(pathset.batchroot), ) return False text_size = ( config.GRAPHS["text_size_large"] if large_text else config.GRAPHS["text_size_small"] ) df = storage.df_read(input_fpath, medium) # Column 0 is the 'Experiment ID' index, which we don't want included as # a vdim cols = df.columns[1:] df = df.with_columns(pl.Series("xticks", xticks)) # Convert to pandas for HoloViews compatibility df_pd = df.to_pandas() dataset = hv.Dataset(data=df_pd.reset_index(), kdims=["xticks"], vdims=cols) assert len(df) == len( xticks ), "Length mismatch between xticks,# data points: {} vs {}".format( len(xticks), len(df) ) model_info = _read_model_info(pathset.model_root, input_stem, medium, xticks) # Add statistics according to configuration stat_dfs = graphutils.read_spread_stats( stats_center, stats_spread, pathset.input_root, input_stem, medium ) vdim_color = graphutils.build_color_map(dataset, legend) plot = graphutils.plot_stats( dataset, stats_center, stats_spread, stat_dfs, backend=backend, vdim_color=vdim_color, ) # Add legend plot.opts(legend_position="bottom") # Plot lines after stats so they show on top plot *= _plot_lines(dataset, model_info, legend, backend) # Add X,Y labels plot.opts(ylabel=ylabel, xlabel=xlabel) # Configure ticks (must be last so not overwritten by what you get from # plotting the lines) plot = _plot_ticks(plot, logyscale, xticks, xticklabels) # Set fontsizes plot.opts( fontsize={ "title": text_size["title"], "labels": text_size["xyz_label"], "ticks": text_size["tick_label"], "legend": text_size["legend_label"], }, ) # Add title plot.opts(title=title) graphutils.plot_save(plot, output_fpath, backend) _logger.debug( "Graph written to <batchroot>/%s", output_fpath.relative_to(pathset.batchroot) ) return True
def _plot_lines( dataset: hv.Dataset, model_info: models.ModelInfo, legend: list[str], backend: str, ) -> hv.NdOverlay: # Plot the curve(s) plot = hv.Overlay( [ hv.Curve( dataset, kdims=dataset.kdims[0], vdims=vdim, label=legend[dataset.vdims.index(vdim)], ) for vdim in dataset.vdims ] ) # Plot the points for each curve plot *= hv.Overlay( [hv.Points((dataset[dataset.kdims[0]], dataset[v])) for v in dataset.vdims] ) if model_info.dataset: # TODO: This currently only works for a single model being put onto a # summary line graph. curve_style = tp.cast(dict[str, tp.Any], config.GRAPHS["curve_dashed"]) plot *= hv.Overlay( [ hv.Curve( model_info.dataset, model_info.dataset.kdims[0], vdim.name, label=model_info.legend[model_info.dataset.vdims.index(vdim)], ).opts(**curve_style[backend]) for vdim in model_info.dataset.vdims ] ) # Plot the points for each curve plot *= hv.Overlay( [ hv.Points( ( model_info.dataset[model_info.dataset.kdims[0]], model_info.dataset[v], ) ) for v in model_info.dataset.vdims if len(model_info.dataset[v]) <= 50 ] ) return plot def _plot_ticks( plot: hv.NdOverlay, logyscale: bool, xticks: list[float], xticklabels: tp.Optional[list[str]], ) -> hv.NdOverlay: if logyscale: plot.opts(logy=True) # For ordered, qualitative data if xticklabels is not None: plot.opts(xticks=list(zip(xticks, xticklabels)), xrotation=90) return plot # 2024/09/13 [JRH]: The union is for compatability with type checkers in # python {3.8,3.11}. def _read_model_info( model_root: tp.Optional[pathlib.Path], input_stem: str, medium: str, xticks: list[float], ) -> models.ModelInfo: if model_root is None: return models.ModelInfo() _logger.trace("Model root='%s'", model_root) exts = config.MODELS_EXT modelf = model_root / (input_stem + exts["model"]) legendf = model_root / (input_stem + exts["legend"]) if not utils.path_exists(modelf): _logger.trace( "No model file=<batch_model_root>/%s found", modelf.relative_to(model_root), ) return models.ModelInfo() info = models.ModelInfo() df = storage.df_read(modelf, medium) # Column 0 is the 'Experiment ID' index, which we don't want included as # a vdim cols = df.columns[1:] df = df.with_columns(pl.Series("xticks", xticks)) # Convert to pandas for HoloViews compatibility df_pd = df.to_pandas() info.dataset = hv.Dataset(data=df_pd.reset_index(), kdims=["xticks"], vdims=cols) with utils.utf8open(legendf, "r") as f: info.legend = f.read().splitlines() _logger.trace( "Loaded model='%s',legend='%s'", modelf.relative_to(model_root), legendf.relative_to(model_root), ) return info __all__ = ["generate"]