Source code for chipscopy.api.ibert.eye_scan.plotter

# Copyright 2021 Xilinx, 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.

import csv
import math
import re
from math import exp, log
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional

from chipscopy.utils.printer import printer

try:
    import kaleido
    import plotly as px
    import plotly.graph_objs as go

    _plotly_available = True
except ImportError:
    _plotly_available = False

try:
    from IPython import get_ipython
    from IPython.display import Image, display

    _jupyter_available = True
except ImportError:
    get_ipython = None
    Image = None
    display = None
    _jupyter_available = False

from chipscopy.api.ibert.aliases import EYE_SCAN_HORZ_RANGE

if TYPE_CHECKING:  # pragma: no cover
    from chipscopy.api.ibert.eye_scan import EyeScan


def check_for_plotly():
    if not _plotly_available:
        raise ImportError(
            f"Plotting packages not installed! Please run 'pip install chipscopy[core-addons]'"
        )


def check_for_jupyter():
    if not _jupyter_available:
        raise ImportError(
            f"Jupyter packages not installed! Please run 'pip install chipscopy[jupyter]'"
        )


def is_running_notebook():
    try:
        check_for_jupyter()
        shell = get_ipython().__class__.__name__
    except ImportError:
        shell = None
    except NameError:
        shell = None
    if shell == "ZMQInteractiveShell":
        return True
    else:
        return False


[docs]class EyeScanPlot: """ Container for eye scan plot """ def __init__(self, eye_scan): self.eye_scan: EyeScan = eye_scan """Link to the `EyeScan` object""" self.fig: go.Figure = None """Plotly `Figure` object""" self._x: List[float] = list() self._y: List[int] = list() self._z: List[List[float]] = list() def _clear_out_data(self): self._x, self._y, self._z = list(), list(), list() def _generate(self, *, title=""): if title == "": title = f"{self.eye_scan.rx.handle} ({self.eye_scan.name})" self._clear_out_data() if self.eye_scan.scan_data.processed is None: raise RuntimeError( f"No plot data received from server! " f"This might be because the scan did not finish successfully." ) xs, ys = set(), set() for point in self.eye_scan.scan_data.processed.scan_points: xs.add(point[0]) ys.add(point[1]) self._x = sorted(xs) self._y = sorted(ys) # This will be needed later on to generate the legend min_ber, max_ber, plot_z_to_ber = -1234, -1234, list() for y in self._y: row_vals = list() z_hovertext_row = list() for x in self._x: ber = self.eye_scan.scan_data.processed.scan_points[(x, y)].ber val = log(ber, 10) hovertext = format(pow(10, val), ".2e") z_hovertext_row.append(hovertext) row_vals.append(val) curr_min = min(row_vals) if min_ber == -1234 or curr_min < min_ber: min_ber = curr_min curr_max = max(row_vals) if max_ber == -1234 or curr_max > max_ber: max_ber = curr_max self._z.append(row_vals) plot_z_to_ber.append(z_hovertext_row) extracted_data = re.match( r"^(.*) UI to (.*) UI$", self.eye_scan.scan_data.all_params[EYE_SCAN_HORZ_RANGE] ) max_horz_range_ui, min_horz_range_ui = ( float(extracted_data.group(1)), float(extracted_data.group(2)), ) max_horz_range_codes, min_horz_range_codes = max(self._x), min(self._y) for index, x_in_codes in enumerate(self._x): self._x[index] = min_horz_range_ui + ( ((x_in_codes - min_horz_range_codes) * (max_horz_range_ui - min_horz_range_ui)) / (max_horz_range_codes - min_horz_range_codes) ) # Generate BER to z mapping for colorbar a.k.a legend colorbar_number_of_ticks: int = math.floor(min_ber) incr_factor = (max_ber - min_ber) / (colorbar_number_of_ticks - 1) color_bar_tick_text = list() color_bar_tick_values = list() # for x_factor in range(colorbar_number_of_ticks): # val = min_ber + (x_factor * incr_factor) # color_bar_tick_values.append(val) # tick_text = format(pow(base, val) if base != 0 else exp(val), ".2e") # color_bar_tick_text.append(tick_text) for i in range(-1, math.floor(min_ber) - 1, -1): color_bar_tick_text.append(format(pow(10, i), ".0e")) color_bar_tick_values.append(i) contour = go.Contour( x=self._x, y=self._y, z=self._z, line=dict(smoothing=0.85, width=0), colorbar=dict( nticks=abs(math.floor(min_ber)), ticks="outside", tickmode="array", tickvals=color_bar_tick_values, ticktext=color_bar_tick_text, title=dict(text="BER"), ), contours=dict(start=-1, end=math.floor(min_ber), size=0.5), colorscale="portland", hovertext=plot_z_to_ber, hoverinfo="x+y+text", ) x_axis_options = dict(zeroline=False, title="UI", mirror=True) y_axis_options = dict(zeroline=False, title="Voltage (Codes)", mirror=True) layout = go.Layout(xaxis=x_axis_options, yaxis=y_axis_options) self.fig = go.Figure(layout=layout, data=contour) self.fig.update_layout(title=dict(text=title))
[docs] def show(self, display_type: str = "automatic", *, title: str = ""): """ Displays the plot in the browser Args: display_type: image format to show. Options are automatic, static, dynamic. Default is 'automatic' title: title to display in plot """ check_for_plotly() self._generate(title=title) if display_type == "automatic": if is_running_notebook(): display_type = "static" else: display_type = "dynamic" if display_type == "dynamic": self.fig.show() elif display_type == "static": check_for_jupyter() image_bytes = self.fig.to_image(format="png") ipython_image = Image(image_bytes) display(ipython_image) else: raise ValueError("show: display_type must be automatic, static, or dynamic")
[docs] def save( self, path: str = ".", file_name: str = None, *, file_format: str = "svg" ) -> Optional[str]: """ Save plot to a file Args: path: **(Optional)** Location where file should be saved. file_name: **(Optional)** Name of the file file_format: **(Optional)** File format. Default is `SVG` Returns: Path of the saved plot """ check_for_plotly() output_path = Path(path) if file_name is None: file_name = f"{self.eye_scan.name}" output_path = output_path.joinpath(f"{file_name}.{file_format}") self._generate() self.fig.write_image(str(output_path.resolve()), height=1080, width=1920, scale=1) printer(f"Saved eye scan plot to {str(output_path.resolve())}", level="info") return str(output_path.resolve())
class EyeScanImporter: # pragma: no cover @staticmethod def load_from_vivado_csv(file_path: Path = None): if file_path is None: file_path = Path( "C:/Users/araju/Documents/Pycharm Projects/chipscopy/chipscopy/api/" "ibert/scan/plots/vivado_eye_scan.csv" ) def normalize_codes_to_ui(data, min_range, max_range): min_element, max_element = min(data), max(data) for index, value in enumerate(data): data[index] = min_range + ( ((value - min_element) * (max_range - min_range)) / (max_element - min_element) ) x_axis, y_axis, scan_data = list(), list(), list() with file_path.open("r") as csv_file: csv_file_contents = csv.reader(csv_file) for row in csv_file_contents: if row[0] == "Horizontal Range": split = row[1].split() min_value_range, max_value_range = (float(split[0]), float(split[-2])) elif row[0] == "Scan Start": for index, row in enumerate(csv_file_contents): if row[0] == "Scan End": break if not x_axis: x_axis = [float(value) for value in row[1:]] normalize_codes_to_ui(x_axis, min_value_range, max_value_range) continue y_axis.append(float(row[0])) scan_data.append([log(float(value)) for value in row[1:]]) return x_axis, y_axis, scan_data