# -*- mode: python -*-
# =============================================================================
#  @@-COPYRIGHT-START-@@
#
#  Copyright (c) 2023-2023, Qualcomm Innovation Center, Inc. All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice,
#     this list of conditions and the following disclaimer.
#
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#
#  3. Neither the name of the copyright holder nor the names of its contributors
#     may be used to endorse or promote products derived from this software
#     without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
#
#  SPDX-License-Identifier: BSD-3-Clause
#
#  @@-COPYRIGHT-END-@@
# =============================================================================
""" Tool to visualize min and max activations/weights of quantized modules in a given model"""
import os
import torch
from bokeh.events import DocumentReady, Reset
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, TextInput, CustomJS, Range1d, HoverTool, CustomJSHover, Div, \
    BooleanFilter, CDSView, Spacer, DataTable, StringFormatter, TableColumn
from bokeh.models.tools import ResetTool
from bokeh.plotting import figure, save, curdoc
from aimet_torch.v2.quantsim import QuantizationSimModel
from aimet_torch.utils import get_ordered_list_of_modules
from aimet_torch.v2.quantization.base import QuantizerBase
from aimet_torch.v2.quantization.encoding_analyzer import _MinMaxObserver
[docs]def visualize_stats(sim: QuantizationSimModel, dummy_input, save_path: str = None) -> None:
    """Produces an interactive html to view the stats collected by each quantizer during calibration
    .. note::
        The QuantizationSimModel input is expected to have been calibrated before using this function. Stats will only
        be plotted for activations/parameters with quantizers containing calibration statistics.
        Currently, this tool is only compatible with quantizers containing :class:`MinMaxEncodingAnalyzer` encoding
        analyzers (i.e., :attr:`QuantScheme.post_training_tf` and :attr:`QuantScheme.training_range_learning_with_tf_init`
        quant schemes).
    Creates an interactive visualization of min and max activations/weights of all quantized modules in the input
    QuantSim object. The features include:
        - Adjustable threshold values to flag layers whose min or max activations/weights exceed the set thresholds
        - Tables containing names and ranges for layers exceeding threshold values
    Saves the visualization as a .html at the given path.
    Example:
        >>> sim = aimet_torch.v2.quantsim.QuantizationSimModel(model, dummy_input, quant_scheme=QuantScheme.post_training_tf)
        >>> with aimet_torch.v2.nn.compute_encodings(sim.model):
        ...     for data, _ in data_loader:
        ...         sim.model(data)
        ...
        >>> visualize_stats(sim, dummy_input, "./quant_stats_visualization.html")
    :param sim: Calibrated QuantizationSimModel
    :param dummy_input: Sample input used to trace the model
    :param save_path: Path for saving the visualization. Default is "./quant_stats_visualization.html"
    """
    if not isinstance(sim, QuantizationSimModel):
        raise TypeError(f"Expected type 'aimet_torch.v2.quantsim.QuantizationSimModel', got '{type(sim)}'.")
    check_path(save_path)
    # Flatten the quantized modules into an ordered list for easier indexing in the plots
    ordered_list = (get_ordered_list_of_modules(sim.model, dummy_input))
    namelist = []
    minlist = []
    maxlist = []
    for module in ordered_list:
        if isinstance(module[1], QuantizerBase):
            if isinstance(module[1].encoding_analyzer.observer, _MinMaxObserver):
                rng = module[1].encoding_analyzer.observer.get_stats()
                if (rng.min is not None) and (rng.max is not None):
                    namelist.append(module[0])
                    minlist.append(torch.min(rng.min).item())
                    maxlist.append(torch.max(rng.max).item())
            # TODO - Handle other quant schemes
    if len(namelist) == 0:
        raise RuntimeError(
            "No stats found to plot. Either there were no quantized modules, or calibration was not performed before calling this function, or observers of type other than _MinMaxObserver were used.")
    idx = list(range(len(namelist)))
    # Save an interactive bokeh plot as a standalone html in the specified directory with the specified name if provided
    if not save_path:
        save_path = "quant_stats_visualization.html"
    check_path(save_path)
    visualizer = QuantStatsVisualizer(idx, namelist, minlist, maxlist)
    visualizer.export_plot_as_html(save_path) 
def check_path(path: str):
    """ Function for sanity check on the given path """
    path_to_directory = os.path.dirname(path)
    if path_to_directory != '' and not os.path.exists(path_to_directory):
        raise NotADirectoryError(f"'{path_to_directory}' is not a directory.")
    if not path.endswith('.html'):
        raise ValueError("'save_path' must end with '.html'.")
class DataSources:
    """
    Class to hold the Bokeh ColumnDataSource objects needed in the visualization.
    """
    def __init__(self,
                 idx:list,
                 namelist:list,
                 minlist:list,
                 maxlist:list,
                 p: figure,
                 default_values: dict,
                 ):
        self.data_source = ColumnDataSource(
            data=dict(idx=idx, namelist=namelist, minlist=minlist, maxlist=maxlist))
        self.default_values_source = ColumnDataSource(
            data=dict(default_ymax=[default_values['default_ymax']],
                      default_ymin=[default_values['default_ymin']],
                      default_maxclip=[default_values['default_maxclip']],
                      default_minclip=[default_values['default_minclip']],
                      default_xmax=[default_values['default_xmax']],
                      default_xmin=[default_values['default_xmin']]))
        self.limits_source = ColumnDataSource(
            data=dict(ymax=[default_values['default_ymax']], ymin=[default_values['default_ymin']],
                      xmin=[p.x_range.start], xmax=[p.x_range.end],
                      minclip=[default_values['default_minclip']],
                      maxclip=[default_values['default_maxclip']]))
        self.min_marker_source = ColumnDataSource(
            data=dict(x=[], y=[], names=[], min_activations=[], max_activations=[], fmt_min_activations=[],
                      fmt_max_activations=[]))
        self.max_marker_source = ColumnDataSource(
            data=dict(x=[], y=[], names=[], min_activations=[], max_activations=[], fmt_min_activations=[],
                      fmt_max_activations=[]))
class TableObjects:
    """
    Class for holding various objects related to the table elements in the visualization.
    """
    def __init__(self, datasources: DataSources):
        self.min_name_filter = BooleanFilter()
        self.max_name_filter = BooleanFilter()
        self.min_name_view = CDSView(filter=self.min_name_filter)
        self.max_name_view = CDSView(filter=self.max_name_filter)
        min_columns = [
            TableColumn(field="names", title="Layer Name",
                        formatter=StringFormatter(font_style="bold"), width=400),
            TableColumn(field="fmt_min_activations", title="Min Activation", width=100),
            TableColumn(field="fmt_max_activations", title="Max Activation", width=100),
        ]
        self.min_data_table = DataTable(source=datasources.min_marker_source, view=self.min_name_view, columns=min_columns,
                                        editable=True,
                                        sortable=True, selectable="checkbox", width=800,
                                        index_position=-1, index_header="row index", index_width=60,
                                        )
        max_columns = [
            TableColumn(field="names", title="Layer Name",
                        formatter=StringFormatter(font_style="bold"), width=400),
            TableColumn(field="fmt_min_activations", title="Min Activation", width=100),
            TableColumn(field="fmt_max_activations", title="Max Activation", width=100),
        ]
        self.max_data_table = DataTable(source=datasources.max_marker_source, view=self.max_name_view, columns=max_columns,
                                        editable=True,
                                        sortable=True, selectable="checkbox", width=800,
                                        index_position=-1, index_header="row index", index_width=60,
                                        )
class InputWidgets:
    """
    Class to hold various input widgets.
    """
    def __init__(self, default_values: dict):
        self.ymin_input = TextInput(value=str(default_values['default_ymin']),
                                    title="Enter lower display limit of the plot")
        self.ymax_input = TextInput(value=str(default_values['default_ymax']),
                                    title="Enter upper display limit of the plot")
        self.minclip_input = TextInput(value=str(default_values['default_minclip']),
                                       title="Enter lower threshold value for activations/weights")
        self.maxclip_input = TextInput(value=str(default_values['default_maxclip']),
                                       title="Enter upper threshold value for activations/weights")
        self.min_name_input = TextInput(value="", title="Enter name filter")
        self.max_name_input = TextInput(value="", title="Enter name filter")
class CustomCallbacks:
    """
    Class to hold Custom JavaScript Callbacks for interactivity in the visualization.
    """
    def __init__(self):
        self.limit_change_callback = None
        self.reset_callback = None
        self.min_name_filter_callback = None
        self.max_name_filter_callback = None
class QuantStatsVisualizer:
    """
    Class for constructing the visualization with functionality to export the plot as
    :param idx: List with indexing for the ordered list of quantized modules.
    :param namelist: List containing names of the ordered list of quantized modules.
    :param minlist: List containing min activations of the ordered list of quantized modules.
    :param maxlist: List containing max activations of the ordered list of quantized modules.
    """
    def __init__(self, idx: list, namelist: list, minlist: list, maxlist: list):
        self.idx = idx
        self.namelist = namelist
        self.minlist = minlist
        self.maxlist = maxlist
        self.p = None
        self.default_values = dict()
    def _add_plot_lines(self, datasources: DataSources):
        self.p.segment(x0='xmin', x1='xmax', y0='ymin', y1='ymin', line_width=4, line_color='black', source=datasources.limits_source)
        self.p.segment(x0='xmin', x1='xmax', y0='ymax', y1='ymax', line_width=4, line_color='black', source=datasources.limits_source)
        self.p.segment(x0='xmin', x1='xmax', y0='minclip', y1='minclip', line_width=2, line_color='black',
                  line_dash='dashed',
                  source=datasources.limits_source)
        self.p.segment(x0='xmin', x1='xmax', y0='maxclip', y1='maxclip', line_width=2, line_color='black',
                  line_dash='dashed',
                  source=datasources.limits_source)
        self.p.line('idx', 'maxlist', source=datasources.data_source, legend_label="Max Activation", line_width=2, line_color="red")
        self.p.line('idx', 'minlist', source=datasources.data_source, legend_label="Min Activation", line_width=2, line_color="blue")
    def _add_min_max_markers(self, datasources: DataSources):
        min_markers = self.p.circle_x('x', 'y', source=datasources.min_marker_source, size=10, color='orange', line_color="navy")
        max_markers = self.p.circle_x('x', 'y', source=datasources.max_marker_source, size=10, color='orange', line_color="navy")
        return  min_markers, max_markers
    @staticmethod
    def _get_min_max_hovertools(min_markers, max_markers):
        format_code = """
                    if (Math.abs(value) < 1e-3 || Math.abs(value) > 1e3) {
                    return value.toExponential(2);
                    } else {
                    return value.toFixed(2);
                    }
                """
        format_hover = CustomJSHover(code=format_code)
        min_hover = HoverTool(renderers=[min_markers], tooltips=[
            ("Name", "@names"),
            ("Max Activation", "@max_activations{custom}"),
            ("Min Activation", "@min_activations{custom}"),
        ], formatters={
            "@min_activations": format_hover,
            "@max_activations": format_hover,
        })
        max_hover = HoverTool(renderers=[max_markers], tooltips=[
            ("Name", "@names"),
            ("Max Activation", "@max_activations{custom}"),
            ("Min Activation", "@min_activations{custom}"),
        ], formatters={
            "@min_activations": format_hover,
            "@max_activations": format_hover,
        })
        return min_hover, max_hover
    def _define_callbacks(self, datasources, tableobjects, inputwidgets):
        customcallbacks = CustomCallbacks()
        customcallbacks.limit_change_callback = CustomJS(args=dict(limits_source=datasources.limits_source,
                                                   data_source=datasources.data_source,
                                                   min_marker_source=datasources.min_marker_source,
                                                   max_marker_source=datasources.max_marker_source,
                                                   ymax_input=inputwidgets.ymax_input,
                                                   ymin_input=inputwidgets.ymin_input,
                                                   maxclip_input=inputwidgets.maxclip_input,
                                                   minclip_input=inputwidgets.minclip_input,
                                                   plot=self.p,
                                                   min_name_filter=tableobjects.min_name_filter,
                                                   max_name_filter=tableobjects.max_name_filter,
                                                   ), code="""
                // Function to adaptively format numerical values in scientific notation
                // if they are large in magnitude
                function formatValue(value) {
                    if (Math.abs(value) < 1e-3 || Math.abs(value) > 1e3) {
                        return value.toExponential(2);
                    } else {
                        return value.toFixed(2);
                    }
                }
                
                // Reading values from input widgets and setting plot y axis range  
                const limits_data = limits_source.data;
                limits_data['ymax'] = [parseFloat(ymax_input.value)];
                limits_data['ymin'] = [parseFloat(ymin_input.value)];
                plot.y_range.start = limits_data['ymin'][0];
                plot.y_range.end = limits_data['ymax'][0];
                limits_data['maxclip'] = [parseFloat(maxclip_input.value)];
                limits_data['minclip'] = [parseFloat(minclip_input.value)];
                
                // Updating the min and max marker sources
                const activation_data = data_source.data;
                const idx = activation_data['idx'];
                const minlist = activation_data['minlist'];
                const maxlist = activation_data['maxlist'];
                const namelist = activation_data['namelist'];
                const min_marker_x = [];
                const min_marker_y = [];
                const min_marker_names = [];
                const min_marker_min_activations = [];
                const min_marker_max_activations = [];
                const min_marker_fmt_min_activations = [];
                const min_marker_fmt_max_activations = [];
                for (let i = 0; i < idx.length; i++) {
                    if (minlist[i] < limits_data['minclip'][0]) {
                        min_marker_x.push(idx[i]);
                        min_marker_y.push(limits_data['minclip'][0]);
                        min_marker_names.push(namelist[i]);
                        min_marker_min_activations.push(minlist[i]);
                        min_marker_max_activations.push(maxlist[i]);
                        min_marker_fmt_min_activations.push(formatValue(minlist[i]));
                        min_marker_fmt_max_activations.push(formatValue(maxlist[i]));
                    }
                }
                min_marker_source.data['x'] = min_marker_x;
                min_marker_source.data['y'] = min_marker_y;
                min_marker_source.data['names'] = min_marker_names;
                min_marker_source.data['min_activations'] = min_marker_min_activations;
                min_marker_source.data['max_activations'] = min_marker_max_activations;
                min_marker_source.data['fmt_min_activations'] = min_marker_fmt_min_activations;
                min_marker_source.data['fmt_max_activations'] = min_marker_fmt_max_activations;
                min_name_filter.booleans = new Array(min_marker_names.length).fill(true);
                const max_marker_x = [];
                const max_marker_y = [];
                const max_marker_names = [];
                const max_marker_min_activations = [];
                const max_marker_max_activations = [];
                const max_marker_fmt_min_activations = [];
                const max_marker_fmt_max_activations = [];
                for (let i = 0; i < idx.length; i++) {
                    if (maxlist[i] > limits_data['maxclip'][0]) {
                        max_marker_x.push(idx[i]);
                        max_marker_y.push(limits_data['maxclip'][0]);
                        max_marker_names.push(namelist[i]);
                        max_marker_min_activations.push(minlist[i]);
                        max_marker_max_activations.push(maxlist[i]);
                        max_marker_fmt_min_activations.push(formatValue(minlist[i]));
                        max_marker_fmt_max_activations.push(formatValue(maxlist[i]));
                    }
                }
                max_marker_source.data['x'] = max_marker_x;
                max_marker_source.data['y'] = max_marker_y;
                max_marker_source.data['names'] = max_marker_names;
                max_marker_source.data['min_activations'] = max_marker_min_activations;
                max_marker_source.data['max_activations'] = max_marker_max_activations;
                max_marker_source.data['fmt_min_activations'] = max_marker_fmt_min_activations;
                max_marker_source.data['fmt_max_activations'] = max_marker_fmt_max_activations;
                max_name_filter.booleans = new Array(max_marker_names.length).fill(true);
                
                // Emitting the changes made to ColumnDataSources
                limits_source.change.emit();
                min_marker_source.change.emit();
                max_marker_source.change.emit();
            """)
        customcallbacks.reset_callback = CustomJS(args=dict(limits_source=datasources.limits_source,
                                            data_source=datasources.data_source,
                                            default_values_source=datasources.default_values_source,
                                            min_marker_source=datasources.min_marker_source,
                                            max_marker_source=datasources.max_marker_source,
                                            ymax_input=inputwidgets.ymax_input,
                                            ymin_input=inputwidgets.ymin_input,
                                            maxclip_input=inputwidgets.maxclip_input,
                                            minclip_input=inputwidgets.minclip_input,
                                            plot=self.p,
                                            min_name_filter=tableobjects.min_name_filter,
                                            max_name_filter=tableobjects.max_name_filter,
                                            ), code="""
                // Function to adaptively format numerical values in scientific notation
                // if they are large in magnitude
                function formatValue(value) {
                    if (Math.abs(value) < 1e-3 || Math.abs(value) > 1e3) {
                        return value.toExponential(2);
                    } else {
                        return value.toFixed(2);
                    }
                }
                
                // Resetting the limits source with default values
                limits_source.data['ymax'] = default_values_source.data['default_ymax'];
                limits_source.data['ymin'] = default_values_source.data['default_ymin'];
                limits_source.data['xmax'] = default_values_source.data['default_xmax'];
                limits_source.data['xmin'] = default_values_source.data['default_xmin'];
                limits_source.data['maxclip'] = default_values_source.data['default_maxclip'];
                limits_source.data['minclip'] = default_values_source.data['default_minclip'];
                const limits_data = limits_source.data;
                // Resetting the plot ranges
                plot.y_range.start = limits_data['ymin'][0];
                plot.y_range.end = limits_data['ymax'][0];
                plot.x_range.start = limits_data['xmin'][0];
                plot.x_range.end = limits_data['xmax'][0];
                // Resetting the input widget values
                ymax_input.value = limits_data['ymax'][0].toString();
                ymin_input.value = limits_data['ymin'][0].toString();
                maxclip_input.value = limits_data['maxclip'][0].toString();
                minclip_input.value = limits_data['minclip'][0].toString();
                
                // Updating the min and max marker sources
                const activation_data = data_source.data;
                const idx = activation_data['idx'];
                const minlist = activation_data['minlist'];
                const maxlist = activation_data['maxlist'];
                const namelist = activation_data['namelist'];
                const min_marker_x = [];
                const min_marker_y = [];
                const min_marker_names = [];
                const min_marker_min_activations = [];
                const min_marker_max_activations = [];
                const min_marker_fmt_min_activations = [];
                const min_marker_fmt_max_activations = [];
                for (let i = 0; i < idx.length; i++) {
                    if (minlist[i] < limits_data['minclip'][0]) {
                        min_marker_x.push(idx[i]);
                        min_marker_y.push(limits_data['minclip'][0]);
                        min_marker_names.push(namelist[i]);
                        min_marker_min_activations.push(minlist[i]);
                        min_marker_max_activations.push(maxlist[i]);
                        min_marker_fmt_min_activations.push(formatValue(minlist[i]));
                        min_marker_fmt_max_activations.push(formatValue(maxlist[i]));
                    }
                }
                min_marker_source.data['x'] = min_marker_x;
                min_marker_source.data['y'] = min_marker_y;
                min_marker_source.data['names'] = min_marker_names;
                min_marker_source.data['min_activations'] = min_marker_min_activations;
                min_marker_source.data['max_activations'] = min_marker_max_activations;
                min_marker_source.data['fmt_min_activations'] = min_marker_fmt_min_activations;
                min_marker_source.data['fmt_max_activations'] = min_marker_fmt_max_activations;
                min_name_filter.booleans = new Array(min_marker_names.length).fill(true);
                const max_marker_x = [];
                const max_marker_y = [];
                const max_marker_names = [];
                const max_marker_min_activations = [];
                const max_marker_max_activations = [];
                const max_marker_fmt_min_activations = [];
                const max_marker_fmt_max_activations = [];
                for (let i = 0; i < idx.length; i++) {
                    if (maxlist[i] > limits_data['maxclip'][0]) {
                        max_marker_x.push(idx[i]);
                        max_marker_y.push(limits_data['maxclip'][0]);
                        max_marker_names.push(namelist[i]);
                        max_marker_min_activations.push(minlist[i]);
                        max_marker_max_activations.push(maxlist[i]);
                        max_marker_fmt_min_activations.push(formatValue(minlist[i]));
                        max_marker_fmt_max_activations.push(formatValue(maxlist[i]));
                    }
                }
                max_marker_source.data['x'] = max_marker_x;
                max_marker_source.data['y'] = max_marker_y;
                max_marker_source.data['names'] = max_marker_names;
                max_marker_source.data['min_activations'] = max_marker_min_activations;
                max_marker_source.data['max_activations'] = max_marker_max_activations;
                max_marker_source.data['fmt_min_activations'] = max_marker_fmt_min_activations;
                max_marker_source.data['fmt_max_activations'] = max_marker_fmt_max_activations;
                max_name_filter.booleans = new Array(max_marker_names.length).fill(true);
                
                // Emitting the changes made to ColumnDataSources
                limits_source.change.emit();
                min_marker_source.change.emit();
                max_marker_source.change.emit();
            """)
        customcallbacks.min_name_filter_callback = CustomJS(args=dict(marker_source=datasources.min_marker_source,
                                                      text_filter=tableobjects.min_name_filter,
                                                      ),
                                            code="""
                // Filter all names having entered pattern as a substring
                text_filter.booleans = Array.from(marker_source.data['names']).map(t => t.includes(cb_obj.value));
                marker_source.change.emit();
            """)
        customcallbacks.max_name_filter_callback = CustomJS(args=dict(marker_source=datasources.max_marker_source,
                                                      text_filter=tableobjects.max_name_filter,
                                                      ),
                                            code="""
                // Filter all names having entered pattern as a substring
                text_filter.booleans = Array.from(marker_source.data['names']).map(t => t.includes(cb_obj.value));
                marker_source.change.emit();
            """)
        return customcallbacks
    def _attach_callbacks(self, inputwidgets, customcallbacks):
        self.p.js_on_event(Reset, customcallbacks.reset_callback)
        inputwidgets.ymax_input.js_on_change('value', customcallbacks.limit_change_callback)
        inputwidgets.ymin_input.js_on_change('value', customcallbacks.limit_change_callback)
        inputwidgets.maxclip_input.js_on_change('value', customcallbacks.limit_change_callback)
        inputwidgets.minclip_input.js_on_change('value', customcallbacks.limit_change_callback)
        inputwidgets.min_name_input.js_on_change("value", customcallbacks.min_name_filter_callback)
        inputwidgets.max_name_input.js_on_change("value", customcallbacks.max_name_filter_callback)
    def _create_layout(self, inputwidgets, tableobjects):
        heading_1 = Div(text="<h2>Quant Stats Visualizer</h2>")
        heading_2 = Div(text="<h2>List of layers with Min activation/weight lesser than lower threshold</h2>")
        heading_3 = Div(text="<h2>List of layers with Max activation/weight higher than upper threshold</h2>")
        sp1 = Spacer(width=50, height=40)
        row1 = row(inputwidgets.ymin_input, inputwidgets.ymax_input)
        row2 = row(inputwidgets.minclip_input, inputwidgets.maxclip_input)
        inputs1 = column(row1, row2)
        layout = column(heading_1, inputs1, sp1, self.p,
                        row(column(heading_2, inputwidgets.min_name_input, tableobjects.min_data_table),
                            column(heading_3, inputwidgets.max_name_input, tableobjects.max_data_table)))
        return layout
    def export_plot_as_html(self, save_path: str) -> None:
        """
        Method for constructing the visualization and saving it to the given path.
        :param save_path: Path for saving the visualization.
        """
        curdoc().theme = 'light_minimal'
        self.p = figure(width=700,
               height=400,
               title="Min Max Activations/Weights of quantized modules for given model",
               x_axis_label="Layer index",
               y_axis_label="Activation/Weight",
               tools="pan,wheel_zoom,box_zoom")
        # Defining the default values of plotting parameters
        self.default_values['default_ymax'] = 1e5
        self.default_values['default_ymin'] = -1e5
        self.default_values['default_xmax'] = len(self.idx) - 1
        self.default_values['default_xmin'] = 0
        self.default_values['default_maxclip'] = self.default_values['default_ymax'] / 2
        self.default_values['default_minclip'] = self.default_values['default_ymin'] / 2
        self.p.x_range = Range1d(0, len(self.idx))
        self.p.y_range = Range1d(self.default_values['default_ymax'], self.default_values['default_ymin'])
        # Creating and adding a reset tool
        rt = ResetTool()
        self.p.add_tools(rt)
        # Defining Bokeh ColumnDataSources
        datasources = DataSources(idx=self.idx,
                                  namelist=self.namelist,
                                  minlist=self.minlist,
                                  maxlist=self.maxlist,
                                  p=self.p,
                                  default_values=self.default_values,
                                  )
        # Creating plot objects
        self._add_plot_lines(datasources)
        # Marker points to see which layers cross the thresholds
        min_markers, max_markers = self._add_min_max_markers(datasources)
        # Defining a hover functionality to see layer details on hovering on the marker points
        min_hover, max_hover = self._get_min_max_hovertools(min_markers, max_markers)
        self.p.add_tools(min_hover, max_hover)
        # Defining the table objects and name filter views
        tableobjects = TableObjects(datasources)
        # Creating the input widgets
        inputwidgets = InputWidgets(self.default_values)
        # Defining Custom JavaScript callbacks
        customcallbacks = self._define_callbacks(datasources, tableobjects, inputwidgets)
        # Attach events to corresponding callbacks
        curdoc().js_on_event(DocumentReady, customcallbacks.reset_callback)
        self._attach_callbacks(inputwidgets, customcallbacks)
        # Define the formatting
        layout = self._create_layout(inputwidgets, tableobjects)
        # Save as standalone html
        save(layout, save_path)