From c86564fb2f4821650fd19e1b4ddb427ac2e277eb Mon Sep 17 00:00:00 2001
From: Philip Colangelo
Date: Wed, 11 Dec 2024 17:55:43 -0500
Subject: [PATCH] Code to validate report file - robust pixmap handling -
better pixmap quality - copy png instead of grab() - scale loading gif -
multimodel report support - recompiled gui with pyside6.8.1
---
setup.py | 2 +-
src/digest/dialog.py | 14 +-
src/digest/histogramchartwidget.py | 2 +-
src/digest/main.py | 61 +-
src/digest/model_class/digest_model.py | 10 +-
src/digest/model_class/digest_onnx_model.py | 52 +-
src/digest/model_class/digest_report_model.py | 47 +-
src/digest/modelsummary.py | 8 +
src/digest/multi_model_analysis.py | 104 ++-
src/digest/multi_model_selection_page.py | 59 +-
src/digest/resource_rc.py | 36 +-
src/digest/ui/freezeinputs_ui.py | 2 +-
src/digest/ui/huggingface_page_ui.py | 2 +-
src/digest/ui/mainwindow_ui.py | 698 +++++++-----------
src/digest/ui/modelsummary.ui | 43 +-
src/digest/ui/modelsummary_ui.py | 41 +-
src/digest/ui/multimodelanalysis_ui.py | 2 +-
src/digest/ui/multimodelselection_page.ui | 63 +-
src/digest/ui/multimodelselection_page_ui.py | 57 +-
src/digest/ui/nodessummary_ui.py | 2 +-
test/resnet18_reports/resnet18_heatmap.png | Bin 103019 -> 127496 bytes
test/resnet18_reports/resnet18_report.yaml | 7 +-
test/test_gui.py | 2 +-
23 files changed, 680 insertions(+), 634 deletions(-)
diff --git a/setup.py b/setup.py
index ca21f4a..b2ad16d 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
setup(
name="digestai",
- version="1.0.0",
+ version="1.1.0",
description="Model analysis toolkit",
author="Philip Colangelo, Daniel Holanda",
packages=find_packages(where="src"),
diff --git a/src/digest/dialog.py b/src/digest/dialog.py
index d2f834e..ae9986d 100644
--- a/src/digest/dialog.py
+++ b/src/digest/dialog.py
@@ -125,13 +125,23 @@ class WarnDialog(QDialog):
def __init__(self, warning_message: str, parent=None):
super().__init__(parent)
- self.setWindowTitle("Warning Message")
+
self.setWindowIcon(QIcon(":/assets/images/digest_logo_500.jpg"))
+
+ self.setWindowTitle("Warning Message")
+ self.setWindowFlags(Qt.WindowType.Dialog)
self.setMinimumWidth(300)
+ self.setWindowModality(Qt.WindowModality.WindowModal)
+
layout = QVBoxLayout()
# Application Version
- layout.addWidget(QLabel("Something went wrong"))
+ layout.addWidget(QLabel("Warning"))
layout.addWidget(QLabel(warning_message))
+
+ ok_button = QPushButton("OK")
+ ok_button.clicked.connect(self.accept) # Close dialog when clicked
+ layout.addWidget(ok_button)
+
self.setLayout(layout)
diff --git a/src/digest/histogramchartwidget.py b/src/digest/histogramchartwidget.py
index 97d5f16..9dbe557 100644
--- a/src/digest/histogramchartwidget.py
+++ b/src/digest/histogramchartwidget.py
@@ -140,7 +140,7 @@ def __init__(self, *args, **kwargs):
super(StackedHistogramWidget, self).__init__(*args, **kwargs)
self.plot_widget = pg.PlotWidget()
- self.plot_widget.setMaximumHeight(150)
+ self.plot_widget.setMaximumHeight(200)
plot_item = self.plot_widget.getPlotItem()
if plot_item:
plot_item.setContentsMargins(0, 0, 0, 0)
diff --git a/src/digest/main.py b/src/digest/main.py
index fe46bf0..dfdab4a 100644
--- a/src/digest/main.py
+++ b/src/digest/main.py
@@ -3,6 +3,7 @@
import os
import sys
+import shutil
import argparse
from datetime import datetime
from typing import Dict, Tuple, Optional, Union
@@ -33,7 +34,7 @@
QMenu,
)
from PySide6.QtGui import QDragEnterEvent, QDropEvent, QPixmap, QMovie, QIcon, QFont
-from PySide6.QtCore import Qt
+from PySide6.QtCore import Qt, QSize
from digest.dialog import StatusDialog, InfoDialog, WarnDialog, ProgressDialog
from digest.thread import StatsThread, SimilarityThread
@@ -309,9 +310,9 @@ def update_cards(
digest_model: DigestModel,
unique_id: str,
):
- self.digest_models[unique_id].model_flops = digest_model.model_flops
+ self.digest_models[unique_id].flops = digest_model.flops
self.digest_models[unique_id].node_type_flops = digest_model.node_type_flops
- self.digest_models[unique_id].model_parameters = digest_model.model_parameters
+ self.digest_models[unique_id].parameters = digest_model.parameters
self.digest_models[unique_id].node_type_parameters = (
digest_model.node_type_parameters
)
@@ -326,10 +327,10 @@ def update_cards(
isinstance(widget, modelSummary)
and widget.digest_model.unique_id == unique_id
):
- if digest_model.model_flops is None:
+ if digest_model.flops is None:
flops_str = "--"
else:
- flops_str = format(digest_model.model_flops, ",")
+ flops_str = format(digest_model.flops, ",")
# Set up the pie chart
pie_chart_labels, pie_chart_data = zip(
@@ -390,10 +391,20 @@ def update_similarity_widget(
break
if completed_successfully and isinstance(widget, modelSummary) and png_filepath:
- widget_width = widget.ui.similarityWidget.width()
- widget.ui.similarityImg.setPixmap(
- QPixmap(png_filepath).scaledToWidth(widget_width)
+ widget.load_gif.stop()
+ widget.ui.similarityImg.clear()
+ widget_width = widget.ui.similarityImg.width()
+
+ pixmap = QPixmap(png_filepath)
+ aspect_ratio = pixmap.width() / pixmap.height()
+ target_height = int(widget_width / aspect_ratio)
+ pixmap_scaled = pixmap.scaled(
+ QSize(widget_width, target_height),
+ Qt.AspectRatioMode.KeepAspectRatio,
+ Qt.TransformationMode.SmoothTransformation,
)
+
+ widget.ui.similarityImg.setPixmap(pixmap_scaled)
widget.ui.similarityImg.setText("")
widget.ui.similarityImg.setCursor(Qt.CursorShape.PointingHandCursor)
@@ -429,7 +440,8 @@ def update_similarity_widget(
widget.ui.similarityCorrelation.setText(text)
elif isinstance(widget, modelSummary):
# Remove animation and set text to failing message
- widget.ui.similarityImg.setMovie(QMovie())
+ widget.load_gif.stop()
+ widget.ui.similarityImg.clear()
widget.ui.similarityImg.setText("Failed to perform similarity analysis")
else:
print(
@@ -666,10 +678,6 @@ def load_onnx(self, filepath: str):
self.ui.singleModelWidget.show()
progress.step()
- movie = QMovie(":/assets/gifs/load.gif")
- model_summary.ui.similarityImg.setMovie(movie)
- movie.start()
-
# Start similarity Analysis
# Note: Should only be started after the model tab has been created
png_tmp_path = os.path.join(self.temp_dir.name, model_id)
@@ -716,6 +724,16 @@ def load_report(self, filepath: str):
digest_model = DigestReportModel(filepath)
+ if not digest_model.is_valid:
+ progress.close()
+ invalid_yaml_dialog = StatusDialog(
+ title="Warning",
+ status_message=f"YAML file {filepath} is not a valid digest report",
+ )
+ invalid_yaml_dialog.show()
+
+ return
+
model_id = digest_model.unique_id
# There is no sense in offering to save the report
@@ -739,9 +757,7 @@ def load_report(self, filepath: str):
model_summary.ui.modelFilename.setText(filepath)
model_summary.ui.generatedDate.setText(datetime.now().strftime("%B %d, %Y"))
- model_summary.ui.parameters.setText(
- format(digest_model.model_parameters, ",")
- )
+ model_summary.ui.parameters.setText(format(digest_model.parameters, ","))
node_type_counts = digest_model.node_type_counts
if len(node_type_counts) < 15:
@@ -751,7 +767,6 @@ def load_report(self, filepath: str):
model_summary.ui.opHistogramChart.bar_spacing = bar_spacing
model_summary.ui.opHistogramChart.set_data(node_type_counts)
-
model_summary.ui.nodes.setText(str(sum(node_type_counts.values())))
progress.step()
@@ -962,13 +977,13 @@ def save_reports(self):
)
digest_model.save_node_type_counts_csv_report(node_type_filepath)
- # Save the similarity image
- similarity_png = self.model_similarity_report[
+ # Save (copy) the similarity image
+ png_file_path = self.model_similarity_thread[
digest_model.unique_id
- ].enlarged_image_label.grab()
- similarity_png.save(
- os.path.join(save_directory, f"{model_name}_heatmap.png"), "PNG"
- )
+ ].png_filepath
+ png_save_path = os.path.join(save_directory, f"{model_name}_heatmap.png")
+ if png_file_path and os.path.exists(png_file_path):
+ shutil.copy(png_file_path, png_save_path)
# Save the text report
txt_report_filepath = os.path.join(save_directory, f"{model_name}_report.txt")
diff --git a/src/digest/model_class/digest_model.py b/src/digest/model_class/digest_model.py
index 3c2fe12..9064184 100644
--- a/src/digest/model_class/digest_model.py
+++ b/src/digest/model_class/digest_model.py
@@ -94,15 +94,15 @@ def __init__(self, *args, **kwargs):
class DigestModel(ABC):
- def __init__(self, filepath: str, model_name: str):
+ def __init__(self, filepath: str, model_name: str, model_type: SupportedModelTypes):
# Public members exposed to the API
self.unique_id: str = str(uuid4())
self.filepath: Optional[str] = filepath
self.model_name: str = model_name
- self.model_type: Optional[SupportedModelTypes] = None
+ self.model_type: SupportedModelTypes = model_type
self.node_type_counts: NodeTypeCounts = NodeTypeCounts()
- self.model_flops: Optional[int] = None
- self.model_parameters: int = 0
+ self.flops: Optional[int] = None
+ self.parameters: int = 0
self.node_type_flops: Dict[str, int] = {}
self.node_type_parameters: Dict[str, int] = {}
self.node_data = NodeData()
@@ -118,7 +118,7 @@ def get_node_shape_counts(self) -> NodeShapeCounts:
return tensor_shape_counter
@abstractmethod
- def parse_model_nodes(self, *args) -> None:
+ def parse_model_nodes(self, *args, **kwargs) -> None:
pass
@abstractmethod
diff --git a/src/digest/model_class/digest_onnx_model.py b/src/digest/model_class/digest_onnx_model.py
index c8b5af3..35aad1d 100644
--- a/src/digest/model_class/digest_onnx_model.py
+++ b/src/digest/model_class/digest_onnx_model.py
@@ -1,7 +1,7 @@
# Copyright(C) 2024 Advanced Micro Devices, Inc. All rights reserved.
import os
-from typing import List, Dict, Optional, Tuple, Union, cast
+from typing import List, Dict, Optional, Tuple, cast
from datetime import datetime
from collections import OrderedDict
import yaml
@@ -11,7 +11,6 @@
from digest.model_class.digest_model import (
DigestModel,
SupportedModelTypes,
- NodeTypeCounts,
NodeInfo,
TensorData,
TensorInfo,
@@ -27,7 +26,7 @@ def __init__(
model_name: str = "",
save_proto: bool = True,
) -> None:
- super().__init__(onnx_filepath, model_name)
+ super().__init__(onnx_filepath, model_name, SupportedModelTypes.ONNX)
self.model_type = SupportedModelTypes.ONNX
@@ -185,11 +184,11 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
# Initialze to zero so we can accumulate. Set to None during the
# model FLOPs calculation if it errors out.
- self.model_flops = 0
+ self.flops = 0
# Check to see if the model inputs have any dynamic shapes
if onnx_utils.get_dynamic_input_dims(onnx_model):
- self.model_flops = None
+ self.flops = None
try:
onnx_model, _ = onnx_utils.optimize_onnx_model(onnx_model)
@@ -199,7 +198,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
)
except Exception as e: # pylint: disable=broad-except
print(f"ONNX utils: {str(e)}")
- self.model_flops = None
+ self.flops = None
# If the ONNX model contains one of the following unsupported ops, then this
# function will return None since the FLOP total is expected to be incorrect
@@ -250,7 +249,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
if all(isinstance(dim, int) for dim in input_tensor.shape):
input_parameters = int(np.prod(np.array(input_tensor.shape)))
node_info.parameters += input_parameters
- self.model_parameters += input_parameters
+ self.parameters += input_parameters
self.node_type_parameters[node.op_type] = (
self.node_type_parameters.get(node.op_type, 0)
+ input_parameters
@@ -267,7 +266,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
self.node_data[node.name] = node_info
if node.op_type in unsupported_ops:
- self.model_flops = None
+ self.flops = None
node_info.flops = None
try:
@@ -288,7 +287,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
isinstance(dim, int) for dim in input_a
) or not isinstance(input_b[-1], int):
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
node_info.flops = int(
@@ -307,7 +306,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
isinstance(dim, int) for dim in input_b
):
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
node_info.flops = int(
@@ -325,7 +324,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
isinstance(dim, int) for dim in w_shape
):
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
mm_dims = [
@@ -371,7 +370,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
if not all(isinstance(dim, int) for dim in x_shape):
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
x_shape_ints = cast(List[int], x_shape)
@@ -458,7 +457,7 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
if not all(isinstance(dim, int) for dim in x_shape):
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
x_shape_ints = cast(List[int], x_shape)
@@ -498,12 +497,12 @@ def parse_model_nodes(self, onnx_model: onnx.ModelProto) -> None:
except IndexError as err:
print(f"Error parsing node {node.name}: {err}")
node_info.flops = None
- self.model_flops = None
+ self.flops = None
continue
# Update the model level flops count
- if node_info.flops is not None and self.model_flops is not None:
- self.model_flops += node_info.flops
+ if node_info.flops is not None and self.flops is not None:
+ self.flops += node_info.flops
# Update the node type flops count
self.node_type_flops[node.op_type] = (
@@ -523,7 +522,8 @@ def save_yaml_report(self, filepath: str) -> None:
yaml_data = {
"report_date": report_date,
- "onnx_file": self.filepath,
+ "model_type": self.model_type.value,
+ "model_file": self.filepath,
"model_name": self.model_name,
"model_version": self.model_version,
"graph_name": self.graph_name,
@@ -533,8 +533,8 @@ def save_yaml_report(self, filepath: str) -> None:
"opset": self.opset,
"import_list": dict(self.imports),
"graph_nodes": sum(self.node_type_counts.values()),
- "model_parameters": self.model_parameters,
- "model_flops": self.model_flops,
+ "parameters": self.parameters,
+ "flops": self.flops,
"node_type_counts": dict(self.node_type_counts),
"node_type_flops": dict(self.node_type_flops),
"node_type_parameters": self.node_type_parameters,
@@ -555,6 +555,7 @@ def save_text_report(self, filepath: str) -> None:
with open(filepath, "w", encoding="utf-8") as f_p:
f_p.write(f"Report created on {report_date}\n")
+ f_p.write(f"Model type: {self.model_type.name}\n")
if self.filepath:
f_p.write(f"ONNX file: {self.filepath}\n")
f_p.write(f"Name of the model: {self.model_name}\n")
@@ -569,9 +570,9 @@ def save_text_report(self, filepath: str) -> None:
f_p.write("\n")
f_p.write(f"Total graph nodes: {sum(self.node_type_counts.values())}\n")
- f_p.write(f"Number of parameters: {self.model_parameters}\n")
- if self.model_flops:
- f_p.write(f"Number of FLOPs: {self.model_flops}\n")
+ f_p.write(f"Number of parameters: {self.parameters}\n")
+ if self.flops:
+ f_p.write(f"Number of FLOPs: {self.flops}\n")
f_p.write("\n")
table_op_intensity = PrettyTable()
@@ -582,7 +583,7 @@ def save_text_report(self, filepath: str) -> None:
[
op_type,
count,
- 100.0 * float(count) / float(self.model_flops),
+ 100.0 * float(count) / float(self.flops),
]
)
@@ -647,8 +648,3 @@ def save_text_report(self, filepath: str) -> None:
f_p.write("Output Tensor(s) Information:\n")
f_p.write(output_table.get_string())
f_p.write("\n\n")
-
- def get_node_type_counts(self) -> Union[NodeTypeCounts, None]:
- if not self.node_type_counts and self.model_proto:
- self.node_type_counts = onnx_utils.get_node_type_counts(self.model_proto)
- return self.node_type_counts if self.node_type_counts else None
diff --git a/src/digest/model_class/digest_report_model.py b/src/digest/model_class/digest_report_model.py
index 5027ee4..4478285 100644
--- a/src/digest/model_class/digest_report_model.py
+++ b/src/digest/model_class/digest_report_model.py
@@ -49,12 +49,18 @@ def __init__(
self.model_type = SupportedModelTypes.REPORT
+ self.is_valid = self.validate_yaml(report_filepath)
+
+ if not self.is_valid:
+ print(f"The yaml file {report_filepath} is not a valid digest report.")
+ return
+
self.model_data = OrderedDict()
with open(report_filepath, "r", encoding="utf-8") as yaml_f:
self.model_data = yaml.safe_load(yaml_f)
model_name = self.model_data["model_name"]
- super().__init__(report_filepath, model_name)
+ super().__init__(report_filepath, model_name, SupportedModelTypes.REPORT)
self.similarity_heatmap_path: Optional[str] = None
self.node_data = NodeData()
@@ -106,8 +112,8 @@ def __init__(
self.node_data[node_name] = node_info
# Unpack the model type agnostic values
- self.model_flops = self.model_data["model_flops"]
- self.model_parameters = self.model_data["model_parameters"]
+ self.flops = self.model_data["flops"]
+ self.parameters = self.model_data["parameters"]
self.node_type_flops = self.model_data["node_type_flops"]
self.node_type_parameters = self.model_data["node_type_parameters"]
self.node_type_counts = self.model_data["node_type_counts"]
@@ -125,6 +131,41 @@ def __init__(
}
)
+ def validate_yaml(self, report_file_path: str) -> bool:
+ """Check that the provided yaml file is indeed a Digest Report file."""
+ expected_keys = [
+ "report_date",
+ "model_file",
+ "model_type",
+ "model_name",
+ "flops",
+ "node_type_flops",
+ "node_type_parameters",
+ "node_type_counts",
+ "input_tensors",
+ "output_tensors",
+ ]
+ try:
+ with open(report_file_path, "r", encoding="utf-8") as file:
+ yaml_content = yaml.safe_load(file)
+
+ if not isinstance(yaml_content, dict):
+ print("Error: YAML content is not a dictionary")
+ return False
+
+ for key in expected_keys:
+ if key not in yaml_content:
+ # print(f"Error: Missing required key '{key}'")
+ return False
+
+ return True
+ except yaml.YAMLError as _:
+ # print(f"Error parsing YAML file: {e}")
+ return False
+ except IOError as _:
+ # print(f"Error reading file: {e}")
+ return False
+
def parse_model_nodes(self) -> None:
"""There are no model nodes to parse"""
diff --git a/src/digest/modelsummary.py b/src/digest/modelsummary.py
index 5aa43c9..a92b756 100644
--- a/src/digest/modelsummary.py
+++ b/src/digest/modelsummary.py
@@ -7,6 +7,8 @@
# pylint: disable=no-name-in-module
from PySide6.QtWidgets import QWidget
+from PySide6.QtGui import QMovie
+from PySide6.QtCore import QSize
from onnx import ModelProto
@@ -37,6 +39,12 @@ def __init__(
self.model_proto: Optional[ModelProto] = None
model_name: str = digest_model.model_name if digest_model.model_name else ""
+ self.load_gif = QMovie(":/assets/gifs/load.gif")
+ # We set the size of the GIF to half the original
+ self.load_gif.setScaledSize(QSize(214, 120))
+ self.ui.similarityImg.setMovie(self.load_gif)
+ self.load_gif.start()
+
# There is no freezing if the model is not ONNX
self.ui.freezeButton.setVisible(False)
self.freeze_inputs: Optional[FreezeInputs] = None
diff --git a/src/digest/multi_model_analysis.py b/src/digest/multi_model_analysis.py
index e63de50..6848403 100644
--- a/src/digest/multi_model_analysis.py
+++ b/src/digest/multi_model_analysis.py
@@ -52,11 +52,18 @@ def __init__(
self.global_node_shape_counter: NodeShapeCounts = defaultdict(Counter)
# Holds the data for all models statistics
- self.global_model_data: Dict[str, Dict[str, Union[int, None]]] = {}
+ self.global_model_data: Dict[str, Dict[str, Union[int, str, None]]] = {}
progress = ProgressDialog("", len(model_list), self)
- header_labels = ["Model", "Opset", "Total Nodes", "Parameters", "FLOPs"]
+ header_labels = [
+ "Model Name",
+ "Model Type",
+ "Opset",
+ "Total Nodes",
+ "Parameters",
+ "FLOPs",
+ ]
self.ui.dataTable.setRowCount(len(model_list))
self.ui.dataTable.setColumnCount(len(header_labels))
self.ui.dataTable.setHorizontalHeaderLabels(header_labels)
@@ -64,24 +71,27 @@ def __init__(
for row, model in enumerate(model_list):
- if not isinstance(model, DigestOnnxModel):
- continue
-
item = QTableWidgetItem(str(model.model_name))
self.ui.dataTable.setItem(row, 0, item)
- item = QTableWidgetItem(str(model.opset))
+ item = QTableWidgetItem(str(model.model_type.name))
self.ui.dataTable.setItem(row, 1, item)
- item = QTableWidgetItem(str(len(model.node_data)))
+ if isinstance(model, DigestOnnxModel):
+ item = QTableWidgetItem(str(model.opset))
+ elif isinstance(model, DigestReportModel):
+ item = QTableWidgetItem(str(model.model_data.get("opset", "NA")))
self.ui.dataTable.setItem(row, 2, item)
- item = QTableWidgetItem(str(model.model_parameters))
+ item = QTableWidgetItem(str(len(model.node_data)))
self.ui.dataTable.setItem(row, 3, item)
- item = QTableWidgetItem(str(model.model_flops))
+ item = QTableWidgetItem(str(model.parameters))
self.ui.dataTable.setItem(row, 4, item)
+ item = QTableWidgetItem(str(model.flops))
+ self.ui.dataTable.setItem(row, 5, item)
+
self.ui.dataTable.resizeColumnsToContents()
self.ui.dataTable.resizeRowsToContents()
@@ -93,41 +103,59 @@ def __init__(
if digest_model.model_name is None:
digest_model.model_name = f"model_{i}"
- if not isinstance(digest_model, DigestOnnxModel):
- continue
-
- if digest_model.model_proto:
- dynamic_input_dims = onnx_utils.get_dynamic_input_dims(
- digest_model.model_proto
- )
- if dynamic_input_dims:
- print(
- "Found the following non-static input dims in your model. "
- "It is recommended to make all dims static before generating reports."
+ if isinstance(digest_model, DigestOnnxModel):
+ opset = digest_model.opset
+ if digest_model.model_proto:
+ dynamic_input_dims = onnx_utils.get_dynamic_input_dims(
+ digest_model.model_proto
)
- for dynamic_shape in dynamic_input_dims:
- print(f"dim: {dynamic_shape}")
+ if dynamic_input_dims:
+ print(
+ "Found the following non-static input dims in your model. "
+ "It is recommended to make all dims static before generating reports."
+ )
+ for dynamic_shape in dynamic_input_dims:
+ print(f"dim: {dynamic_shape}")
+
+ elif isinstance(digest_model, DigestReportModel):
+ opset = digest_model.model_data.get("opset", "")
# Update the global model dictionary
- if digest_model.model_name in self.global_model_data:
+ if digest_model.unique_id in self.global_model_data:
print(
- f"Warning! {digest_model.model_name} has already been processed, "
+ f"Warning! {digest_model.model_name} with id "
+ f"{digest_model.unique_id} has already been processed, "
"skipping the duplicate model."
)
+ continue
- self.global_model_data[digest_model.model_name] = {
- "opset": digest_model.opset,
- "parameters": digest_model.model_parameters,
- "flops": digest_model.model_flops,
+ self.global_model_data[digest_model.unique_id] = {
+ "model_name": digest_model.model_name,
+ "model_type": digest_model.model_type.name,
+ "opset": opset,
+ "parameters": digest_model.parameters,
+ "flops": digest_model.flops,
}
- node_type_counter[digest_model.model_name] = (
- digest_model.get_node_type_counts()
+ # Here we are creating a name that is a combination of the model name
+ # and the model type.
+ node_type_counter_key = (
+ f"{digest_model.model_name}-{digest_model.model_type.value}"
)
+ if node_type_counter_key in node_type_counter:
+ print(
+ f"Warning! {digest_model.model_name} with model type "
+ f"{digest_model.model_type.value} has already been added to "
+ "to the stacked histogram, skipping."
+ )
+ continue
+
+ node_type_counter[node_type_counter_key] = digest_model.node_type_counts
+
# Update global data structure for node type counter
self.global_node_type_counter.update(
- node_type_counter[digest_model.model_name]
+ node_type_counter[node_type_counter_key]
)
node_shape_counts = digest_model.get_node_shape_counts()
@@ -258,10 +286,18 @@ def save_reports(self):
) as csvfile:
writer = csv.writer(csvfile)
rows = [
- [model, data["opset"], data["parameters"], data["flops"]]
- for model, data in self.global_model_data.items()
+ [
+ data["model_name"],
+ data["model_type"],
+ data["opset"],
+ data["parameters"],
+ data["flops"],
+ ]
+ for _, data in self.global_model_data.items()
]
- writer.writerow(["Model", "Opset", "Parameters", "FLOPs"])
+ writer.writerow(
+ ["Model Name", "Model Type", "Opset", "Parameters", "FLOPs"]
+ )
writer.writerows(rows)
if save_individual_reports or save_multi_reports:
diff --git a/src/digest/multi_model_selection_page.py b/src/digest/multi_model_selection_page.py
index 42863a1..ddf2e90 100644
--- a/src/digest/multi_model_selection_page.py
+++ b/src/digest/multi_model_selection_page.py
@@ -93,8 +93,10 @@ def __init__(
self.ui.warningLabel.hide()
self.item_model = QStandardItemModel()
self.item_model.itemChanged.connect(self.update_num_selected_label)
- self.ui.selectAllBox.setCheckState(Qt.CheckState.Checked)
- self.ui.selectAllBox.stateChanged.connect(self.update_list_view_items)
+ self.ui.radioAll.setChecked(True)
+ self.ui.radioAll.toggled.connect(self.update_list_view_items)
+ self.ui.radioONNX.toggled.connect(self.update_list_view_items)
+ self.ui.radioReports.toggled.connect(self.update_list_view_items)
self.ui.selectFolderBtn.clicked.connect(self.openFolder)
self.ui.duplicateLabel.hide()
self.ui.modelListView.setModel(self.item_model)
@@ -178,10 +180,20 @@ def update_num_selected_label(self):
self.ui.openAnalysisBtn.setEnabled(False)
def update_list_view_items(self):
- state = self.ui.selectAllBox.checkState()
+ radio_all_state = self.ui.radioAll.isChecked()
+ radio_onnx_state = self.ui.radioONNX.isChecked()
+ radio_reports_state = self.ui.radioReports.isChecked()
for row in range(self.item_model.rowCount()):
item = self.item_model.item(row)
- item.setCheckState(state)
+ value = item.data(Qt.ItemDataRole.DisplayRole)
+ if radio_all_state:
+ item.setCheckState(Qt.CheckState.Checked)
+ elif os.path.splitext(value)[-1] == ".onnx" and radio_onnx_state:
+ item.setCheckState(Qt.CheckState.Checked)
+ elif os.path.splitext(value)[-1] == ".yaml" and radio_reports_state:
+ item.setCheckState(Qt.CheckState.Checked)
+ else:
+ item.setCheckState(Qt.CheckState.Unchecked)
def set_directory(self, directory: str):
"""
@@ -197,15 +209,30 @@ def set_directory(self, directory: str):
return
progress = ProgressDialog("Searching Directory for ONNX Files", 0, self)
+
onnx_file_list = list(
glob.glob(os.path.join(directory, "**/*.onnx"), recursive=True)
)
+ onnx_file_list = [os.path.normpath(model_file) for model_file in onnx_file_list]
+
+ yaml_file_list = list(
+ glob.glob(os.path.join(directory, "**/*.yaml"), recursive=True)
+ )
+ yaml_file_list = [os.path.normpath(model_file) for model_file in yaml_file_list]
+
+ # Filter out YAML files that are not valid reports
+ report_file_list = []
+ for yaml_file in yaml_file_list:
+ digest_report = DigestReportModel(yaml_file)
+ if digest_report.is_valid:
+ report_file_list.append(yaml_file)
+
+ total_num_models = len(onnx_file_list) + len(report_file_list)
- onnx_file_list = [os.path.normpath(onnx_file) for onnx_file in onnx_file_list]
serialized_models_paths: defaultdict[bytes, List[str]] = defaultdict(list)
progress.close()
- progress = ProgressDialog("Loading ONNX Models", len(onnx_file_list), self)
+ progress = ProgressDialog("Loading Models", total_num_models, self)
memory_limit_percentage = 90
models_loaded = 0
@@ -215,7 +242,9 @@ def set_directory(self, directory: str):
break
try:
models_loaded += 1
- model = onnx.load(filepath, load_external_data=False)
+ if os.path.splitext(filepath)[-1] == ".onnx":
+ model = onnx.load(filepath, load_external_data=False)
+ serialized_models_paths[model.SerializeToString()].append(filepath)
dialog_msg = (
"Warning: System RAM has exceeded the threshold of "
f"{memory_limit_percentage}%. No further models will be loaded. "
@@ -226,7 +255,7 @@ def set_directory(self, directory: str):
parent=self,
):
self.update_warning_label(
- f"Loaded only {models_loaded - 1} out of {len(onnx_file_list)} models "
+ f"Loaded only {models_loaded - 1} out of {total_num_models} models "
f"as memory consumption has reached {memory_limit_percentage}% of "
"system memory. Preventing further loading of models."
)
@@ -237,15 +266,13 @@ def set_directory(self, directory: str):
break
else:
self.ui.warningLabel.hide()
- serialized_models_paths[model.SerializeToString()].append(filepath)
+
except DecodeError as error:
print(f"Error decoding model {filepath}: {error}")
progress.close()
- progress = ProgressDialog(
- "Processing ONNX Models", len(serialized_models_paths), self
- )
+ progress = ProgressDialog("Processing Models", total_num_models, self)
num_duplicates = 0
self.item_model.clear()
@@ -269,6 +296,12 @@ def set_directory(self, directory: str):
item.setCheckState(Qt.CheckState.Checked)
self.item_model.appendRow(item)
+ for path in report_file_list:
+ item = QStandardItem(path)
+ item.setCheckable(True)
+ item.setCheckState(Qt.CheckState.Checked)
+ self.item_model.appendRow(item)
+
progress.close()
if num_duplicates:
@@ -284,7 +317,7 @@ def set_directory(self, directory: str):
self.update_num_selected_label()
self.update_message_label(
- f"Found a total of {len(onnx_file_list)} ONNX files. "
+ f"Found a total of {total_num_models} model files. "
"Right click a model below "
"to open it up in the model summary view."
)
diff --git a/src/digest/resource_rc.py b/src/digest/resource_rc.py
index cf29584..59afc50 100644
--- a/src/digest/resource_rc.py
+++ b/src/digest/resource_rc.py
@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
-# Created by: The Resource Compiler for Qt version 6.8.0
+# Created by: The Resource Compiler for Qt version 6.8.1
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -19676,39 +19676,39 @@
\x00\x00\x000\x00\x02\x00\x00\x00\x03\x00\x00\x00\x05\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00B\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01\x93Ju\xc2>\
+\x00\x00\x01\x93K\x85\xbd\xbb\
\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x01\x86\x02\
-\x00\x00\x01\x93Ju\xc2A\
+\x00\x00\x01\x93K\x85\xbd\xbb\
\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x01Open (Ctrl-O)
",
- None,
- )
- )
- # endif // QT_CONFIG(tooltip)
+ MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"DigestAI", None))
+#if QT_CONFIG(tooltip)
+ self.openFileBtn.setToolTip(QCoreApplication.translate("MainWindow", u"
Open (Ctrl-O)
", None))
+#endif // QT_CONFIG(tooltip)
self.openFileBtn.setText("")
- # if QT_CONFIG(shortcut)
- self.openFileBtn.setShortcut(
- QCoreApplication.translate("MainWindow", "Ctrl+O", None)
- )
- # endif // QT_CONFIG(shortcut)
- # if QT_CONFIG(tooltip)
- self.openFolderBtn.setToolTip(
- QCoreApplication.translate(
- "MainWindow",
- "Multi-Model Analysis
",
- None,
- )
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.openFileBtn.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
+#endif // QT_CONFIG(shortcut)
+#if QT_CONFIG(tooltip)
+ self.openFolderBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Multi-Model Analysis
", None))
+#endif // QT_CONFIG(tooltip)
self.openFolderBtn.setText("")
- # if QT_CONFIG(tooltip)
- self.huggingfaceBtn.setToolTip(
- QCoreApplication.translate("MainWindow", "Huggingface", None)
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.huggingfaceBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Huggingface", None))
+#endif // QT_CONFIG(tooltip)
self.huggingfaceBtn.setText("")
- # if QT_CONFIG(tooltip)
- self.summaryBtn.setToolTip(
- QCoreApplication.translate("MainWindow", "Summary", None)
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.summaryBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Summary", None))
+#endif // QT_CONFIG(tooltip)
self.summaryBtn.setText("")
- # if QT_CONFIG(tooltip)
- self.saveBtn.setToolTip(
- QCoreApplication.translate("MainWindow", "Save Report (Ctrl-S)", None)
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.saveBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Save Report (Ctrl-S)", None))
+#endif // QT_CONFIG(tooltip)
self.saveBtn.setText("")
- # if QT_CONFIG(shortcut)
- self.saveBtn.setShortcut(
- QCoreApplication.translate("MainWindow", "Ctrl+S", None)
- )
- # endif // QT_CONFIG(shortcut)
- # if QT_CONFIG(tooltip)
- self.nodesListBtn.setToolTip(
- QCoreApplication.translate("MainWindow", "Node List", None)
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.saveBtn.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
+#endif // QT_CONFIG(shortcut)
+#if QT_CONFIG(tooltip)
+ self.nodesListBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Node List", None))
+#endif // QT_CONFIG(tooltip)
self.nodesListBtn.setText("")
- # if QT_CONFIG(shortcut)
- self.nodesListBtn.setShortcut(
- QCoreApplication.translate("MainWindow", "Ctrl+S", None)
- )
- # endif // QT_CONFIG(shortcut)
- # if QT_CONFIG(tooltip)
- self.subgraphBtn.setToolTip(
- QCoreApplication.translate("MainWindow", "Subgraph", None)
- )
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(shortcut)
+ self.nodesListBtn.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
+#endif // QT_CONFIG(shortcut)
+#if QT_CONFIG(tooltip)
+ self.subgraphBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Subgraph", None))
+#endif // QT_CONFIG(tooltip)
self.subgraphBtn.setText("")
- # if QT_CONFIG(tooltip)
- self.infoBtn.setToolTip(QCoreApplication.translate("MainWindow", "Info", None))
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.infoBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Info", None))
+#endif // QT_CONFIG(tooltip)
self.infoBtn.setText("")
- # if QT_CONFIG(tooltip)
- self.exitBtn.setToolTip(QCoreApplication.translate("MainWindow", "Exit", None))
- # endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.exitBtn.setToolTip(QCoreApplication.translate("MainWindow", u"Exit", None))
+#endif // QT_CONFIG(tooltip)
self.exitBtn.setText("")
self.Logo.setText("")
- # if QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
self.tabWidget.setToolTip("")
- # endif // QT_CONFIG(tooltip)
- self.tabWidget.setTabText(
- self.tabWidget.indexOf(self.tab),
- QCoreApplication.translate("MainWindow", "Tab 1", None),
- )
+#endif // QT_CONFIG(tooltip)
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QCoreApplication.translate("MainWindow", u"Tab 1", None))
self.subgraphIcon.setText("")
- self.comingSoonLabel.setText(
- QCoreApplication.translate(
- "MainWindow",
- 'Coming soon...
',
- None,
- )
- )
-
+ self.comingSoonLabel.setText(QCoreApplication.translate("MainWindow", u"Coming soon...
", None))
# retranslateUi
+
diff --git a/src/digest/ui/modelsummary.ui b/src/digest/ui/modelsummary.ui
index 180fed4..d0ea5ca 100644
--- a/src/digest/ui/modelsummary.ui
+++ b/src/digest/ui/modelsummary.ui
@@ -6,8 +6,8 @@
0
0
- 980
- 687
+ 1061
+ 837
@@ -153,9 +153,9 @@ border-top-right-radius: 10px;
0
- 0
+ -558
991
- 1453
+ 1443
@@ -244,7 +244,7 @@ QFrame:hover {
6
- -
+
-
@@ -271,7 +271,7 @@ QFrame:hover {
- -
+
-
@@ -667,20 +667,32 @@ QFrame:hover {
-
-
+
0
0
+
+
+ 300
+ 500
+
+
-
-
+
-
-
+
0
0
+
+
+ 0
+ 0
+
+
16777215
@@ -690,6 +702,9 @@ QFrame:hover {
Loading...
+
+ false
+
Qt::AlignmentFlag::AlignCenter
@@ -834,7 +849,7 @@ QFrame:hover {
-
-
+
0
0
@@ -861,7 +876,7 @@ QFrame:hover {
-
-
-
+
-
QLabel {
@@ -875,7 +890,7 @@ QFrame:hover {
- -
+
-
@@ -975,7 +990,7 @@ QScrollBar::handle:vertical {
- -
+
-
@@ -1218,7 +1233,7 @@ QScrollBar::handle:vertical {
- -
+
-
diff --git a/src/digest/ui/modelsummary_ui.py b/src/digest/ui/modelsummary_ui.py
index 1102e3a..3f3b290 100644
--- a/src/digest/ui/modelsummary_ui.py
+++ b/src/digest/ui/modelsummary_ui.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'modelsummary.ui'
##
-## Created by: Qt User Interface Compiler version 6.8.0
+## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -29,7 +29,7 @@ class Ui_modelSummary(object):
def setupUi(self, modelSummary):
if not modelSummary.objectName():
modelSummary.setObjectName(u"modelSummary")
- modelSummary.resize(980, 687)
+ modelSummary.resize(1061, 837)
sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -115,7 +115,7 @@ def setupUi(self, modelSummary):
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
- self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 991, 1453))
+ self.scrollAreaWidgetContents.setGeometry(QRect(0, -558, 991, 1443))
self.scrollAreaWidgetContents.setStyleSheet(u"background-color: black;")
self.verticalLayout_20 = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout_20.setObjectName(u"verticalLayout_20")
@@ -178,7 +178,7 @@ def setupUi(self, modelSummary):
self.opsetLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.opsetLabel.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
- self.verticalLayout_5.addWidget(self.opsetLabel, 0, Qt.AlignmentFlag.AlignHCenter)
+ self.verticalLayout_5.addWidget(self.opsetLabel)
self.opsetVersion = QLabel(self.opsetFrame)
self.opsetVersion.setObjectName(u"opsetVersion")
@@ -192,7 +192,7 @@ def setupUi(self, modelSummary):
self.opsetVersion.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.opsetVersion.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse|Qt.TextInteractionFlag.TextSelectableByKeyboard|Qt.TextInteractionFlag.TextSelectableByMouse)
- self.verticalLayout_5.addWidget(self.opsetVersion, 0, Qt.AlignmentFlag.AlignHCenter)
+ self.verticalLayout_5.addWidget(self.opsetVersion)
self.horizontalLayout_2.addWidget(self.opsetFrame)
@@ -397,18 +397,24 @@ def setupUi(self, modelSummary):
self.secondRowChartsLayout.setContentsMargins(-1, 20, -1, -1)
self.similarityWidget = QWidget(self.scrollAreaWidgetContents)
self.similarityWidget.setObjectName(u"similarityWidget")
- sizePolicy.setHeightForWidth(self.similarityWidget.sizePolicy().hasHeightForWidth())
- self.similarityWidget.setSizePolicy(sizePolicy)
+ sizePolicy6.setHeightForWidth(self.similarityWidget.sizePolicy().hasHeightForWidth())
+ self.similarityWidget.setSizePolicy(sizePolicy6)
+ self.similarityWidget.setMinimumSize(QSize(300, 500))
self.placeholderWidget = QVBoxLayout(self.similarityWidget)
self.placeholderWidget.setObjectName(u"placeholderWidget")
self.similarityImg = ClickableLabel(self.similarityWidget)
self.similarityImg.setObjectName(u"similarityImg")
- sizePolicy.setHeightForWidth(self.similarityImg.sizePolicy().hasHeightForWidth())
- self.similarityImg.setSizePolicy(sizePolicy)
+ sizePolicy7 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ sizePolicy7.setHorizontalStretch(0)
+ sizePolicy7.setVerticalStretch(0)
+ sizePolicy7.setHeightForWidth(self.similarityImg.sizePolicy().hasHeightForWidth())
+ self.similarityImg.setSizePolicy(sizePolicy7)
+ self.similarityImg.setMinimumSize(QSize(0, 0))
self.similarityImg.setMaximumSize(QSize(16777215, 16777215))
+ self.similarityImg.setScaledContents(False)
self.similarityImg.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.placeholderWidget.addWidget(self.similarityImg, 0, Qt.AlignmentFlag.AlignHCenter)
+ self.placeholderWidget.addWidget(self.similarityImg)
self.similarityCorrelationStatic = QLabel(self.similarityWidget)
self.similarityCorrelationStatic.setObjectName(u"similarityCorrelationStatic")
@@ -446,11 +452,8 @@ def setupUi(self, modelSummary):
self.flopsPieChart = PieChartWidget(self.scrollAreaWidgetContents)
self.flopsPieChart.setObjectName(u"flopsPieChart")
- sizePolicy7 = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Preferred)
- sizePolicy7.setHorizontalStretch(0)
- sizePolicy7.setVerticalStretch(0)
- sizePolicy7.setHeightForWidth(self.flopsPieChart.sizePolicy().hasHeightForWidth())
- self.flopsPieChart.setSizePolicy(sizePolicy7)
+ sizePolicy6.setHeightForWidth(self.flopsPieChart.sizePolicy().hasHeightForWidth())
+ self.flopsPieChart.setSizePolicy(sizePolicy6)
self.flopsPieChart.setMinimumSize(QSize(300, 500))
self.secondRowChartsLayout.addWidget(self.flopsPieChart)
@@ -474,7 +477,7 @@ def setupUi(self, modelSummary):
" background: transparent;\n"
"}")
- self.inputsLayout.addWidget(self.inputsLabel, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.inputsLayout.addWidget(self.inputsLabel)
self.inputsTable = QTableWidget(self.scrollAreaWidgetContents)
if (self.inputsTable.columnCount() < 4):
@@ -543,7 +546,7 @@ def setupUi(self, modelSummary):
self.inputsTable.verticalHeader().setVisible(False)
self.inputsTable.verticalHeader().setHighlightSections(True)
- self.inputsLayout.addWidget(self.inputsTable, 0, Qt.AlignmentFlag.AlignVCenter)
+ self.inputsLayout.addWidget(self.inputsTable)
self.thirdRowInputsLayout.addLayout(self.inputsLayout)
@@ -580,7 +583,7 @@ def setupUi(self, modelSummary):
self.freezeButton.setIcon(icon)
self.freezeButton.setIconSize(QSize(32, 32))
- self.thirdRowInputsLayout.addWidget(self.freezeButton, 0, Qt.AlignmentFlag.AlignTop)
+ self.thirdRowInputsLayout.addWidget(self.freezeButton)
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
@@ -770,7 +773,7 @@ def setupUi(self, modelSummary):
self.modelProtoTable.verticalHeader().setMinimumSectionSize(20)
self.modelProtoTable.verticalHeader().setDefaultSectionSize(20)
- self.verticalLayout_3.addWidget(self.modelProtoTable, 0, Qt.AlignmentFlag.AlignRight)
+ self.verticalLayout_3.addWidget(self.modelProtoTable)
self.importsLabel = QLabel(self.sidePaneFrame)
self.importsLabel.setObjectName(u"importsLabel")
diff --git a/src/digest/ui/multimodelanalysis_ui.py b/src/digest/ui/multimodelanalysis_ui.py
index 54aa6d6..b9da242 100644
--- a/src/digest/ui/multimodelanalysis_ui.py
+++ b/src/digest/ui/multimodelanalysis_ui.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'multimodelanalysis.ui'
##
-## Created by: Qt User Interface Compiler version 6.8.0
+## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
diff --git a/src/digest/ui/multimodelselection_page.ui b/src/digest/ui/multimodelselection_page.ui
index c5d12f8..0071b47 100644
--- a/src/digest/ui/multimodelselection_page.ui
+++ b/src/digest/ui/multimodelselection_page.ui
@@ -52,7 +52,7 @@
-
-
-
+
-
@@ -68,7 +68,7 @@
- -
+
-
false
@@ -128,7 +128,7 @@
- Warning: The chosen folder contains more than MAX_ONNX_MODELS
+ Warning
2
@@ -141,7 +141,60 @@
-
-
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 33
+
+
+
+ false
+
+
+
+
+
+ All
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 33
+
+
+
+ false
+
+
+
+
+
+ ONNX
+
+
+
+ -
+
0
@@ -161,7 +214,7 @@
- Select All
+ Reports
diff --git a/src/digest/ui/multimodelselection_page_ui.py b/src/digest/ui/multimodelselection_page_ui.py
index e6acb66..79ed6a6 100644
--- a/src/digest/ui/multimodelselection_page_ui.py
+++ b/src/digest/ui/multimodelselection_page_ui.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'multimodelselection_page.ui'
##
-## Created by: Qt User Interface Compiler version 6.8.0
+## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,9 +15,9 @@
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
-from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QHBoxLayout,
- QLabel, QListView, QListWidget, QListWidgetItem,
- QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout,
+from PySide6.QtWidgets import (QAbstractItemView, QApplication, QHBoxLayout, QLabel,
+ QListView, QListWidget, QListWidgetItem, QPushButton,
+ QRadioButton, QSizePolicy, QSpacerItem, QVBoxLayout,
QWidget)
class Ui_MultiModelSelection(object):
@@ -59,7 +59,7 @@ def setupUi(self, MultiModelSelection):
self.selectFolderBtn.setSizePolicy(sizePolicy)
self.selectFolderBtn.setStyleSheet(u"")
- self.horizontalLayout_2.addWidget(self.selectFolderBtn, 0, Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
+ self.horizontalLayout_2.addWidget(self.selectFolderBtn)
self.openAnalysisBtn = QPushButton(MultiModelSelection)
self.openAnalysisBtn.setObjectName(u"openAnalysisBtn")
@@ -68,7 +68,7 @@ def setupUi(self, MultiModelSelection):
self.openAnalysisBtn.setSizePolicy(sizePolicy)
self.openAnalysisBtn.setStyleSheet(u"")
- self.horizontalLayout_2.addWidget(self.openAnalysisBtn, 0, Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
+ self.horizontalLayout_2.addWidget(self.openAnalysisBtn)
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
@@ -104,15 +104,36 @@ def setupUi(self, MultiModelSelection):
self.horizontalLayout_3 = QHBoxLayout()
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
- self.selectAllBox = QCheckBox(MultiModelSelection)
- self.selectAllBox.setObjectName(u"selectAllBox")
- sizePolicy.setHeightForWidth(self.selectAllBox.sizePolicy().hasHeightForWidth())
- self.selectAllBox.setSizePolicy(sizePolicy)
- self.selectAllBox.setMinimumSize(QSize(0, 33))
- self.selectAllBox.setAutoFillBackground(False)
- self.selectAllBox.setStyleSheet(u"")
-
- self.horizontalLayout_3.addWidget(self.selectAllBox)
+ self.radioAll = QRadioButton(MultiModelSelection)
+ self.radioAll.setObjectName(u"radioAll")
+ sizePolicy.setHeightForWidth(self.radioAll.sizePolicy().hasHeightForWidth())
+ self.radioAll.setSizePolicy(sizePolicy)
+ self.radioAll.setMinimumSize(QSize(0, 33))
+ self.radioAll.setAutoFillBackground(False)
+ self.radioAll.setStyleSheet(u"")
+ self.radioAll.setChecked(True)
+
+ self.horizontalLayout_3.addWidget(self.radioAll)
+
+ self.radioONNX = QRadioButton(MultiModelSelection)
+ self.radioONNX.setObjectName(u"radioONNX")
+ sizePolicy.setHeightForWidth(self.radioONNX.sizePolicy().hasHeightForWidth())
+ self.radioONNX.setSizePolicy(sizePolicy)
+ self.radioONNX.setMinimumSize(QSize(0, 33))
+ self.radioONNX.setAutoFillBackground(False)
+ self.radioONNX.setStyleSheet(u"")
+
+ self.horizontalLayout_3.addWidget(self.radioONNX)
+
+ self.radioReports = QRadioButton(MultiModelSelection)
+ self.radioReports.setObjectName(u"radioReports")
+ sizePolicy.setHeightForWidth(self.radioReports.sizePolicy().hasHeightForWidth())
+ self.radioReports.setSizePolicy(sizePolicy)
+ self.radioReports.setMinimumSize(QSize(0, 33))
+ self.radioReports.setAutoFillBackground(False)
+ self.radioReports.setStyleSheet(u"")
+
+ self.horizontalLayout_3.addWidget(self.radioReports)
self.numSelectedLabel = QLabel(MultiModelSelection)
self.numSelectedLabel.setObjectName(u"numSelectedLabel")
@@ -184,8 +205,10 @@ def retranslateUi(self, MultiModelSelection):
self.selectFolderBtn.setText(QCoreApplication.translate("MultiModelSelection", u"Select Folder", None))
self.openAnalysisBtn.setText(QCoreApplication.translate("MultiModelSelection", u"Open Analysis", None))
self.infoLabel.setText("")
- self.warningLabel.setText(QCoreApplication.translate("MultiModelSelection", u"Warning: The chosen folder contains more than MAX_ONNX_MODELS", None))
- self.selectAllBox.setText(QCoreApplication.translate("MultiModelSelection", u"Select All", None))
+ self.warningLabel.setText(QCoreApplication.translate("MultiModelSelection", u"Warning", None))
+ self.radioAll.setText(QCoreApplication.translate("MultiModelSelection", u"All", None))
+ self.radioONNX.setText(QCoreApplication.translate("MultiModelSelection", u"ONNX", None))
+ self.radioReports.setText(QCoreApplication.translate("MultiModelSelection", u"Reports", None))
self.numSelectedLabel.setText(QCoreApplication.translate("MultiModelSelection", u"0 selected models", None))
self.duplicateLabel.setText(QCoreApplication.translate("MultiModelSelection", u"The following models were found to be duplicates and have been deselected from the list on the left.", None))
# retranslateUi
diff --git a/src/digest/ui/nodessummary_ui.py b/src/digest/ui/nodessummary_ui.py
index 7efc69d..e0e400c 100644
--- a/src/digest/ui/nodessummary_ui.py
+++ b/src/digest/ui/nodessummary_ui.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'nodessummary.ui'
##
-## Created by: Qt User Interface Compiler version 6.8.0
+## Created by: Qt User Interface Compiler version 6.8.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
diff --git a/test/resnet18_reports/resnet18_heatmap.png b/test/resnet18_reports/resnet18_heatmap.png
index e2ae67d2b40cb8dda4977947b3396ad3ed021969..1fb614ec18fa878876dba1eddfbb4680ed19714a 100644
GIT binary patch
literal 127496
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