diff --git a/src/caret_analyze/plot/histogram/__init__.py b/src/caret_analyze/plot/histogram/__init__.py index 83306a1da..fb8e1ad6d 100644 --- a/src/caret_analyze/plot/histogram/__init__.py +++ b/src/caret_analyze/plot/histogram/__init__.py @@ -14,8 +14,12 @@ from .histogram import ResponseTimeHistPlot from .histogram_factory import ResponseTimeHistPlotFactory +from .histogram_plot import HistogramPlot +from .histogram_plot_factory import HistogramPlotFactory __all__ = [ 'ResponseTimeHistPlot', - 'ResponseTimeHistPlotFactory' + 'ResponseTimeHistPlotFactory', + 'HistogramPlotFactory', + 'HistogramPlot' ] diff --git a/src/caret_analyze/plot/histogram/histogram_plot.py b/src/caret_analyze/plot/histogram/histogram_plot.py new file mode 100644 index 000000000..b22d1d72b --- /dev/null +++ b/src/caret_analyze/plot/histogram/histogram_plot.py @@ -0,0 +1,118 @@ +# Copyright 2021 Research Institute of Systems Planning, Inc. +# +# 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 annotations + +from collections.abc import Sequence + +from bokeh.plotting import Figure + +from caret_analyze.record import Frequency, Latency, Period + +import pandas as pd + +from ..plot_base import PlotBase +from ..visualize_lib import VisualizeLibInterface +from ...exceptions import UnsupportedTypeError +from ...runtime import CallbackBase, Communication + +MetricsTypes = Frequency | Latency | Period +HistTypes = CallbackBase | Communication + + +class HistogramPlot(PlotBase): + """Class that provides API for histogram data.""" + + def __init__( + self, + metrics: list[MetricsTypes], + visualize_lib: VisualizeLibInterface, + target_objects: Sequence[HistTypes], + data_type: str + ) -> None: + self._metrics = metrics + self._visualize_lib = visualize_lib + self._target_objects = target_objects + self._data_type = data_type + + def to_dataframe( + self, + xaxis_type: str + ) -> pd.DataFrame: + """ + Get data in pandas DataFrame format. + + Parameters + ---------- + xaxis_type : str + Type of time for timestamp. + + Returns + ------- + pd.DataFrame + + """ + raise NotImplementedError() + + def figure( + self, + xaxis_type: str | None = None, + ywheel_zoom: bool | None = None, + full_legends: bool | None = None + ) -> Figure: + """ + Get a histogram graph for each object using the bokeh library. + + Parameters + ---------- + xaxis_type : str + Type of x-axis of the line graph to be plotted. + "system_time", "index", or "sim_time" can be specified, by default "system_time". + ywheel_zoom : bool + If True, the drawn graph can be expanded in the y-axis direction + by the mouse wheel, by default True. + full_legends : bool + If True, all legends are drawn + even if the number of legends exceeds the threshold, by default False. + + Returns + ------- + bokeh.plotting.Figure + + Raises + ------ + UnsupportedTypeError + Argument xaxis_type is not "system_time", "index", or "sim_time". + + """ + # Set default value + xaxis_type = xaxis_type or 'system_time' + ywheel_zoom = ywheel_zoom if ywheel_zoom is not None else True + full_legends = full_legends if full_legends is not None else False + + # Validate + self._validate_xaxis_type(xaxis_type) + + return self._visualize_lib.histogram( + self._metrics, + self._target_objects, + self._data_type + ) + + def _validate_xaxis_type(self, xaxis_type: str) -> None: + if xaxis_type not in ['system_time', 'sim_time', 'index']: + raise UnsupportedTypeError( + f'Unsupported xaxis_type. xaxis_type = {xaxis_type}. ' + 'supported xaxis_type: [system_time/sim_time/index]' + ) diff --git a/src/caret_analyze/plot/histogram/histogram_plot_factory.py b/src/caret_analyze/plot/histogram/histogram_plot_factory.py new file mode 100644 index 000000000..ef254b1bd --- /dev/null +++ b/src/caret_analyze/plot/histogram/histogram_plot_factory.py @@ -0,0 +1,95 @@ +# Copyright 2021 Research Institute of Systems Planning, Inc. +# +# 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 annotations + +from collections.abc import Sequence + +from caret_analyze.record import Frequency, Latency, Period + +from .histogram_plot import HistogramPlot +from ..visualize_lib import VisualizeLibInterface +from ...common import type_check_decorator +from ...exceptions import UnsupportedTypeError +from ...runtime import CallbackBase, Communication + +MetricsType = Frequency | Latency | Period +HistTypes = CallbackBase | Communication + + +class HistogramPlotFactory: + """Factory class to create an instance of HistogramPlot.""" + + @staticmethod + @type_check_decorator + def create_instance( + target_objects: Sequence[HistTypes], + metrics_name: str, + visualize_lib: VisualizeLibInterface + ) -> HistogramPlot: + """ + Create an instance of HistogramPlot. + + Parameters + ---------- + target_objects : Sequence[HistTypes] + HistTypes = CallbackBase | Communication + metrics_name : str + Metrics for HistogramPlot data. + supported metrics: [frequency/latency/period] + visualize_lib : VisualizeLibInterface + Instance of VisualizeLibInterface used for visualization. + + Returns + ------- + HistogramPlot + + Raises + ------ + UnsupportedTypeError + Argument metrics is not "frequency", "latency", or "period". + + """ + if metrics_name == 'frequency': + metrics_frequency: list[MetricsType] =\ + [Frequency(target_object.to_records()) for target_object in target_objects] + return HistogramPlot( + metrics_frequency, + visualize_lib, + target_objects, + metrics_name + ) + elif metrics_name == 'latency': + metrics_latency: list[MetricsType] =\ + [Latency(target_object.to_records()) for target_object in target_objects] + return HistogramPlot( + metrics_latency, + visualize_lib, + target_objects, + metrics_name + ) + elif metrics_name == 'period': + metrics_period: list[MetricsType] =\ + [Period(target_object.to_records()) for target_object in target_objects] + return HistogramPlot( + metrics_period, + visualize_lib, + target_objects, + metrics_name + ) + else: + raise UnsupportedTypeError( + 'Unsupported metrics specified. ' + 'Supported metrics: [frequency/latency/period]' + ) diff --git a/src/caret_analyze/plot/plot_facade.py b/src/caret_analyze/plot/plot_facade.py index a3c2815d5..e7bc871ae 100644 --- a/src/caret_analyze/plot/plot_facade.py +++ b/src/caret_analyze/plot/plot_facade.py @@ -18,7 +18,7 @@ from logging import getLogger from .callback_scheduling import CallbackSchedulingPlot, CallbackSchedulingPlotFactory -from .histogram import ResponseTimeHistPlot, ResponseTimeHistPlotFactory +from .histogram import HistogramPlotFactory, ResponseTimeHistPlot, ResponseTimeHistPlotFactory from .message_flow import MessageFlowPlot, MessageFlowPlotFactory from .plot_base import PlotBase from .stacked_bar import StackedBarPlot, StackedBarPlotFactory @@ -30,6 +30,7 @@ logger = getLogger(__name__) TimeSeriesTypes = CallbackBase | Communication | Publisher | Subscription | Path +HistTypes = CallbackBase | Communication CallbackSchedTypes = (Application | Executor | Path | Node | CallbackGroup | Collection[CallbackGroup]) @@ -63,6 +64,35 @@ def parse_collection_or_unpack( return parsed_target_objects +def parse_collection_or_unpack_for_hist( + target_arg: tuple[Collection[HistTypes]] | tuple[HistTypes, ...] +) -> list[HistTypes]: + """ + Parse target argument. + + To address both cases where the target argument is passed in collection type + or unpacked, this function converts them to the same list format. + + Parameters + ---------- + target_arg : tuple[Collection[HistTypes]] | tuple[HistTypes, ...] + Target objects. + + Returns + ------- + list[HistTypes] + + """ + parsed_target_objects: list[HistTypes] + if isinstance(target_arg[0], Collection): + assert len(target_arg) == 1 + parsed_target_objects = list(target_arg[0]) + else: # Unpacked case + parsed_target_objects = list(target_arg) # type: ignore + + return parsed_target_objects + + class Plot: """Facade class for plot.""" @@ -300,3 +330,81 @@ def create_message_flow_plot( target_path, visualize_lib, granularity, treat_drop_as_delay, lstrip_s, rstrip_s ) return plot + + @staticmethod + def create_frequency_histogram_plot( + *target_objects: HistTypes + ) -> PlotBase: + """ + Get frequency histogram plot instance. + + Parameters + ---------- + target_objects : Collection[HistTypes] + HistTypes = CallbackBase | Communication + Instances that are the sources of the plotting. + This also accepts multiple inputs by unpacking. + + Returns + ------- + PlotBase + + """ + visualize_lib = VisualizeLibFactory.create_instance() + plot = HistogramPlotFactory.create_instance( + parse_collection_or_unpack_for_hist(target_objects), + 'frequency', visualize_lib + ) + return plot + + @staticmethod + def create_latency_histogram_plot( + *target_objects: HistTypes + ) -> PlotBase: + """ + Get latency histogram plot instance. + + Parameters + ---------- + target_objects : Collection[HistTypes] + HistTypes = CallbackBase | Communication + Instances that are the sources of the plotting. + This also accepts multiple inputs by unpacking. + + Returns + ------- + PlotBase + + """ + visualize_lib = VisualizeLibFactory.create_instance() + plot = HistogramPlotFactory.create_instance( + parse_collection_or_unpack_for_hist(target_objects), + 'latency', visualize_lib + ) + return plot + + @staticmethod + def create_period_histogram_plot( + *target_objects: HistTypes + ) -> PlotBase: + """ + Get period histogram plot instance. + + Parameters + ---------- + target_objects : Collection[HistTypes] + HistTypes = CallbackBase | Communication + Instances that are the sources of the plotting. + This also accepts multiple inputs by unpacking. + + Returns + ------- + PlotBase + + """ + visualize_lib = VisualizeLibFactory.create_instance() + plot = HistogramPlotFactory.create_instance( + parse_collection_or_unpack_for_hist(target_objects), + 'period', visualize_lib + ) + return plot diff --git a/src/caret_analyze/plot/visualize_lib/bokeh/bokeh.py b/src/caret_analyze/plot/visualize_lib/bokeh/bokeh.py index e50b4ce05..29b00487f 100644 --- a/src/caret_analyze/plot/visualize_lib/bokeh/bokeh.py +++ b/src/caret_analyze/plot/visualize_lib/bokeh/bokeh.py @@ -17,18 +17,28 @@ from collections.abc import Sequence from logging import getLogger +from bokeh.models import GlyphRenderer, HoverTool + from bokeh.plotting import Figure +from caret_analyze.record import Frequency, Latency, Period + +from numpy import histogram + from .callback_scheduling import BokehCallbackSched from .message_flow import BokehMessageFlow from .response_time_hist import BokehResponseTimeHist from .stacked_bar import BokehStackedBar from .timeseries import BokehTimeSeries +from .util import ColorSelectorFactory, LegendManager from ..visualize_lib_interface import VisualizeLibInterface from ...metrics_base import MetricsBase from ....runtime import CallbackBase, CallbackGroup, Communication, Path, Publisher, Subscription TimeSeriesTypes = CallbackBase | Communication | (Publisher | Subscription) | Path +MetricsTypes = Frequency | Latency | Period +HistTypes = CallbackBase | Communication + logger = getLogger(__name__) @@ -37,7 +47,7 @@ class Bokeh(VisualizeLibInterface): """Class that visualizes data using Bokeh library.""" def __init__(self) -> None: - pass + self._legend_items: list[tuple[str, list[GlyphRenderer]]] = [] def response_time_hist( self, @@ -163,3 +173,49 @@ def timeseries( """ timeseries = BokehTimeSeries(metrics, xaxis_type, ywheel_zoom, full_legends, case) return timeseries.create_figure() + + def histogram( + self, + metrics: list[MetricsTypes], + target_objects: Sequence[HistTypes], + data_type: str + ) -> Figure: + legend_manager = LegendManager() + if data_type == 'frequency': + x_label = data_type + ' [Hz]' + elif data_type in ['period', 'latency']: + x_label = data_type + ' [ms]' + else: + raise NotImplementedError() + plot = Figure( + title=data_type, x_axis_label=x_label, y_axis_label='Probability', plot_width=800 + ) + data_list: list[list[int]] = [ + [_ for _ in m.to_records().get_column_series(data_type) if _ is not None] + for m in metrics + ] + color_selector = ColorSelectorFactory.create_instance('unique') + if data_type in ['period', 'latency']: + data_list = [[_ *10**(-6) for _ in data] for data in data_list] + max_value = max( + max([max_len for max_len in data_list if len(max_len)], key=lambda x: max(x)) + ) + min_value = min( + min([min_len for min_len in data_list if len(min_len)], key=lambda x: min(x)) + ) + for hist_type, target_object in zip(data_list, target_objects): + hist, bins = histogram(hist_type, 20, (min_value, max_value), density=True) + quad = plot.quad(top=hist, bottom=0, + left=bins[:-1], right=bins[1:], + line_color='white', alpha=0.5, + color=color_selector.get_color()) + legend_manager.add_legend(target_object, quad) + hover = HoverTool( + tooltips=[(x_label, '@left'), ('Probability', '@top')], renderers=[quad] + ) + plot.add_tools(hover) + + legends = legend_manager.create_legends(20, False, location='top_right') + for legend in legends: + plot.add_layout(legend, 'right') + return plot diff --git a/src/caret_analyze/plot/visualize_lib/visualize_lib_interface.py b/src/caret_analyze/plot/visualize_lib/visualize_lib_interface.py index b2c18a326..5bc1f4ecc 100644 --- a/src/caret_analyze/plot/visualize_lib/visualize_lib_interface.py +++ b/src/caret_analyze/plot/visualize_lib/visualize_lib_interface.py @@ -20,9 +20,12 @@ from bokeh.plotting import Figure from ..metrics_base import MetricsBase +from ...record import Frequency, Latency, Period from ...runtime import CallbackBase, CallbackGroup, Communication, Path, Publisher, Subscription TimeSeriesTypes = CallbackBase | Communication | (Publisher | Subscription) +MetricsTypes = Frequency | Latency | Period +HistTypes = CallbackBase | Communication class VisualizeLibInterface(metaclass=ABCMeta): @@ -87,3 +90,11 @@ def stacked_bar( case: str, ) -> Figure: raise NotImplementedError() + + def histogram( + self, + metrics: list[MetricsTypes], + target_objects: Sequence[HistTypes], + data_type: str + ) -> Figure: + raise NotImplementedError() diff --git a/src/caret_analyze/runtime/path.py b/src/caret_analyze/runtime/path.py index 0de8aaedb..6afe4e734 100644 --- a/src/caret_analyze/runtime/path.py +++ b/src/caret_analyze/runtime/path.py @@ -91,11 +91,7 @@ def _to_rename_rule( old_columns: Sequence[str], new_columns: Sequence[str], ): - return { - old_column: new_column - for old_column, new_column - in zip(old_columns, new_columns) - } + return dict(zip(old_columns, new_columns)) class RecordsMerged: