ria-toolkit-oss/src/ria_toolkit_oss/viz/onnx.py

592 lines
19 KiB
Python
Raw Normal View History

2025-10-20 12:16:30 -04:00
"""
ONNX model visualization utilities.
This module provides visualization functions for ONNX models following the same pattern
as other ria-toolkit-oss visualization modules.
"""
from pathlib import Path
import plotly.express as px
2025-10-20 14:44:51 -04:00
import plotly.graph_objects as go
2025-10-20 12:16:30 -04:00
from plotly.subplots import make_subplots
try:
import onnx
import onnx.helper
import onnx.numpy_helper
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
ONNX_AVAILABLE = True
except ImportError:
ONNX_AVAILABLE = False
def create_styled_error_figure(title: str, message: str, suggestion: str = None) -> go.Figure:
"""Create a professional error figure with Qoherent dark theme styling."""
fig = go.Figure()
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Create a clean, centered text display using Plotly's text formatting
main_text = f"<b style='color:#f56565;font-size:18px'>⚠️ {title}</b><br><br>"
main_text += f"<span style='color:#e2e8f0;font-size:14px'>{message}</span>"
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
if suggestion:
2025-10-20 14:44:51 -04:00
main_text += "<br><br><span style='color:#63b3ed;font-size:13px'>💡 <b>Suggestion:</b></span><br>"
2025-10-20 12:16:30 -04:00
main_text += f"<span style='color:#cbd5e0;font-size:12px'>{suggestion}</span>"
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Add the main text annotation
fig.add_annotation(
text=main_text,
2025-10-20 14:44:51 -04:00
xref="paper",
yref="paper",
x=0.5,
y=0.5,
xanchor="center",
yanchor="middle",
2025-10-20 12:16:30 -04:00
showarrow=False,
align="center",
borderwidth=2,
bordercolor="#4a5568",
bgcolor="#2d3748",
2025-10-20 14:44:51 -04:00
font=dict(family="Arial, sans-serif", size=14, color="#e2e8f0"),
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Update layout with dark theme
fig.update_layout(
title="",
height=400,
template="plotly_dark",
margin=dict(l=40, r=40, t=40, b=40),
plot_bgcolor="#1a202c",
paper_bgcolor="#1a202c",
2025-10-20 14:44:51 -04:00
font=dict(color="#e2e8f0"),
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Remove axes and grid
fig.update_xaxes(visible=False)
fig.update_yaxes(visible=False)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
return fig
def graph_structure(file_path: Path) -> go.Figure:
"""
Visualize the ONNX model graph structure showing nodes and connections.
Matches layout ID: graph_structure
"""
if not ONNX_AVAILABLE:
return create_styled_error_figure(
"ONNX Not Available",
"ONNX library is required for model analysis.",
"Install with: pip install onnx"
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
try:
# Load ONNX model
model = onnx.load(str(file_path))
graph = model.graph
nodes = graph.node
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
if len(nodes) == 0:
return create_styled_error_figure(
"Empty Model",
"This ONNX model contains no operators.",
"Please check if the model file is valid."
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Create network diagram data
node_info = []
for i, node in enumerate(nodes):
2025-10-20 14:44:51 -04:00
node_info.append(
{
"id": i,
"name": node.name or f"{node.op_type}_{i}",
"op_type": node.op_type,
"inputs": len(node.input),
"outputs": len(node.output),
}
)
2025-10-20 12:16:30 -04:00
# Create visualization
fig = go.Figure()
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Simple linear layout for now
x_positions = list(range(len(node_info)))
y_positions = [0] * len(node_info)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Add nodes as scatter points
2025-10-20 14:44:51 -04:00
fig.add_trace(
go.Scatter(
x=x_positions,
y=y_positions,
mode="markers+text",
marker=dict(
size=[min(max(info["inputs"] + info["outputs"] + 15, 20), 50) for info in node_info],
color=px.colors.qualitative.Set3[: len(node_info)],
opacity=0.8,
line=dict(width=2, color="white"),
),
text=[f"{info['op_type']}" for info in node_info],
textposition="middle center",
textfont=dict(size=10, color="white"),
hovertemplate="<b>%{text}</b><br>"
+ "Name: %{customdata[0]}<br>"
+ "Inputs: %{customdata[1]}<br>"
+ "Outputs: %{customdata[2]}<br>"
+ "<extra></extra>",
customdata=[[info["name"], info["inputs"], info["outputs"]] for info in node_info],
name="Operators",
)
)
2025-10-20 12:16:30 -04:00
# Add connecting lines
for i in range(len(node_info) - 1):
2025-10-20 14:44:51 -04:00
fig.add_trace(
go.Scatter(
x=[x_positions[i], x_positions[i + 1]],
y=[y_positions[i], y_positions[i + 1]],
mode="lines",
line=dict(color="gray", width=1, dash="dot"),
showlegend=False,
hoverinfo="skip",
)
)
2025-10-20 12:16:30 -04:00
fig.update_layout(
title={
2025-10-20 14:44:51 -04:00
"text": ("ONNX Graph Structure<br>"
f"<span style='font-size:14px; color:#a0a0a0;'>{len(nodes)} Operators</span>"),
"x": 0.5,
"xanchor": "center",
"font": {"size": 22},
2025-10-20 12:16:30 -04:00
},
xaxis_title="Execution Order",
yaxis_title="",
showlegend=False,
height=500,
template="plotly_dark",
yaxis=dict(showticklabels=False, showgrid=False),
xaxis=dict(showgrid=False),
2025-10-20 14:44:51 -04:00
margin=dict(l=50, r=50, t=80, b=50),
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
return fig
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
except Exception as e:
return create_styled_error_figure(
2025-10-20 14:44:51 -04:00
"Graph Analysis Error",
"Could not analyze ONNX model structure.",
2025-10-20 12:16:30 -04:00
f"Error: {str(e)}"
)
def operator_analysis(file_path: Path) -> go.Figure:
"""
Analyze the distribution and types of operators in the ONNX model.
Matches layout ID: operator_analysis
"""
if not ONNX_AVAILABLE:
return create_styled_error_figure(
"ONNX Not Available",
"ONNX library is required for operator analysis.",
"Install with: pip install onnx"
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
try:
model = onnx.load(str(file_path))
graph = model.graph
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Count operators
op_counts = {}
for node in graph.node:
op_type = node.op_type
op_counts[op_type] = op_counts.get(op_type, 0) + 1
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
if not op_counts:
return create_styled_error_figure(
"No Operators",
"This ONNX model contains no operators to analyze.",
2025-10-20 14:44:51 -04:00
"Please verify the model file is valid.",
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Sort by frequency
sorted_ops = sorted(op_counts.items(), key=lambda x: x[1], reverse=True)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Create pie chart and bar chart
fig = make_subplots(
2025-10-20 14:44:51 -04:00
rows=2,
cols=1,
2025-10-20 12:16:30 -04:00
subplot_titles=("Operator Distribution", "Operator Frequency"),
2025-10-20 14:44:51 -04:00
specs=[[{"type": "pie"}], [{"type": "bar"}]],
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Pie chart for operator distribution
op_names, op_values = zip(*sorted_ops) if sorted_ops else ([], [])
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.add_trace(
go.Pie(
labels=list(op_names),
values=list(op_values),
textinfo="label+percent",
textposition="auto",
2025-10-20 14:44:51 -04:00
showlegend=False,
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=1,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Bar chart for frequency
fig.add_trace(
go.Bar(
x=list(op_names),
y=list(op_values),
2025-10-20 14:44:51 -04:00
marker_color=px.colors.qualitative.Set3[: len(op_names)],
showlegend=False,
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=2,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.update_layout(
title={
2025-10-20 14:44:51 -04:00
"text": ("ONNX Operator Analysis<br>"
f"<span style='font-size:14px; color:#a0a0a0;'>{len(op_counts)} Unique Types</span>"),
"x": 0.5,
"xanchor": "center",
"font": {"size": 22},
2025-10-20 12:16:30 -04:00
},
height=700,
2025-10-20 14:44:51 -04:00
template="plotly_dark",
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
return fig
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
except Exception as e:
return create_styled_error_figure(
"Operator Analysis Error",
2025-10-20 14:44:51 -04:00
"Could not analyze ONNX operators.",
2025-10-20 12:16:30 -04:00
f"Error: {str(e)}"
)
def model_metadata(file_path: Path) -> go.Figure:
"""
Display comprehensive metadata about the ONNX model.
Matches layout ID: model_metadata
"""
if not ONNX_AVAILABLE:
return create_styled_error_figure(
"ONNX Not Available",
"ONNX library is required for metadata analysis.",
"Install with: pip install onnx"
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
try:
model = onnx.load(str(file_path))
graph = model.graph
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Calculate basic statistics
total_nodes = len(graph.node)
total_inputs = len(graph.input)
total_outputs = len(graph.output)
total_initializers = len(graph.initializer)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Calculate parameter count
total_params = 0
for initializer in graph.initializer:
try:
tensor = onnx.numpy_helper.to_array(initializer)
total_params += tensor.size
2025-10-20 14:44:51 -04:00
except Exception:
2025-10-20 12:16:30 -04:00
pass # Skip if tensor can't be loaded
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Get model file size
file_size_mb = file_path.stat().st_size / (1024 * 1024)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Create metadata display
fig = make_subplots(
2025-10-20 14:44:51 -04:00
rows=2,
cols=2,
2025-10-20 12:16:30 -04:00
subplot_titles=("Model Size", "Architecture", "Inputs/Outputs", "Parameters"),
2025-10-20 14:44:51 -04:00
specs=[[{"type": "indicator"}, {"type": "bar"}], [{"type": "table"}, {"type": "indicator"}]],
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Model size indicator
fig.add_trace(
go.Indicator(
mode="number+gauge",
value=file_size_mb,
2025-10-20 14:44:51 -04:00
title={"text": "Model Size (MB)"},
number={"suffix": " MB", "valueformat": ".2f"},
2025-10-20 12:16:30 -04:00
gauge={
2025-10-20 14:44:51 -04:00
"axis": {"range": [0, max(100, file_size_mb * 1.5)]},
"bar": {"color": "darkblue"},
"steps": [
{"range": [0, 10], "color": "lightgreen"},
{"range": [10, 50], "color": "yellow"},
{"range": [50, 100], "color": "orange"},
],
},
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=1,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Architecture components
arch_data = ["Nodes", "Inputs", "Outputs", "Initializers"]
arch_values = [total_nodes, total_inputs, total_outputs, total_initializers]
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.add_trace(
go.Bar(
x=arch_data,
y=arch_values,
2025-10-20 14:44:51 -04:00
marker_color=["blue", "green", "orange", "red"],
2025-10-20 12:16:30 -04:00
showlegend=False
),
2025-10-20 14:44:51 -04:00
row=1,
col=2,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# I/O Table
io_data = []
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Add input info
for inp in graph.input[:5]: # Limit to first 5
shape = "Unknown"
dtype = "Unknown"
if inp.type and inp.type.tensor_type:
# Get shape
if inp.type.tensor_type.shape:
2025-10-20 14:44:51 -04:00
dims = [str(d.dim_value) if d.dim_value > 0 else "?" for d in inp.type.tensor_type.shape.dim]
2025-10-20 12:16:30 -04:00
shape = f"[{', '.join(dims)}]"
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Get data type
elem_type = inp.type.tensor_type.elem_type
2025-10-20 14:44:51 -04:00
type_map = {
1: "float32",
2: "uint8",
3: "int8",
6: "int32",
7: "int64",
9: "bool",
10: "float16",
11: "double",
}
dtype = type_map.get(elem_type, f"type_{elem_type}")
io_data.append(["Input", inp.name[:20], shape, dtype])
2025-10-20 12:16:30 -04:00
# Add output info
for out in graph.output[:5]: # Limit to first 5
shape = "Unknown"
dtype = "Unknown"
if out.type and out.type.tensor_type:
if out.type.tensor_type.shape:
2025-10-20 14:44:51 -04:00
dims = [str(d.dim_value) if d.dim_value > 0 else "?" for d in out.type.tensor_type.shape.dim]
2025-10-20 12:16:30 -04:00
shape = f"[{', '.join(dims)}]"
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
elem_type = out.type.tensor_type.elem_type
2025-10-20 14:44:51 -04:00
type_map = {
1: "float32",
2: "uint8",
3: "int8",
6: "int32",
7: "int64",
9: "bool",
10: "float16",
11: "double",
}
dtype = type_map.get(elem_type, f"type_{elem_type}")
io_data.append(["Output", out.name[:20], shape, dtype])
2025-10-20 12:16:30 -04:00
if io_data:
fig.add_trace(
go.Table(
header=dict(
2025-10-20 14:44:51 -04:00
values=["Type", "Name", "Shape", "Data Type"],
fill_color="lightblue",
align="left"
2025-10-20 12:16:30 -04:00
),
cells=dict(
values=list(zip(*io_data)),
2025-10-20 14:44:51 -04:00
fill_color="white",
align="left"
),
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=2,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Parameters indicator
fig.add_trace(
go.Indicator(
mode="number",
value=total_params,
2025-10-20 14:44:51 -04:00
title={"text": "Total Parameters"},
number={"suffix": "M", "valueformat": ".2f"},
number_font_size=30,
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=2,
col=2,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.update_layout(
title={
2025-10-20 14:44:51 -04:00
"text": ("ONNX Model Metadata<br>"
f"<span style='font-size:14px; color:#a0a0a0;'>{total_params/1e6:.2f}M Parameters</span>"),
"x": 0.5,
"xanchor": "center",
"font": {"size": 22},
2025-10-20 12:16:30 -04:00
},
height=600,
template="plotly_dark",
2025-10-20 14:44:51 -04:00
showlegend=False,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
return fig
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
except Exception as e:
return create_styled_error_figure(
"Metadata Analysis Error",
2025-10-20 14:44:51 -04:00
"Could not extract ONNX model metadata.",
2025-10-20 12:16:30 -04:00
f"Error: {str(e)}"
)
def performance_metrics(file_path: Path) -> go.Figure:
"""
Display performance and computational metrics for the ONNX model.
Matches layout ID: performance_metrics
"""
if not ONNX_AVAILABLE:
return create_styled_error_figure(
"ONNX Not Available",
"ONNX library is required for performance analysis.",
2025-10-20 14:44:51 -04:00
"Install with: pip install onnx",
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
try:
model = onnx.load(str(file_path))
graph = model.graph
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Calculate metrics
model_size_bytes = file_path.stat().st_size
model_size_mb = model_size_bytes / (1024 * 1024)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Count parameters
total_params = 0
for initializer in graph.initializer:
try:
tensor = onnx.numpy_helper.to_array(initializer)
total_params += tensor.size
2025-10-20 14:44:51 -04:00
except Exception:
2025-10-20 12:16:30 -04:00
pass
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Estimate memory usage (rough approximation)
param_memory_mb = (total_params * 4) / (1024 * 1024) # Assume float32
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Count operations by complexity
2025-10-20 14:44:51 -04:00
compute_ops = ["Conv", "MatMul", "Gemm", "LSTM", "GRU"]
efficient_ops = ["Relu", "Add", "Mul", "BatchNormalization", "Dropout"]
compute_count = sum(1 for node in graph.node if any(op in node.op_type for op in compute_ops))
efficient_count = sum(1 for node in graph.node if any(op in node.op_type for op in efficient_ops))
2025-10-20 12:16:30 -04:00
total_ops = len(graph.node)
other_count = total_ops - compute_count - efficient_count
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Create performance dashboard
fig = make_subplots(
2025-10-20 14:44:51 -04:00
rows=2,
cols=2,
2025-10-20 12:16:30 -04:00
subplot_titles=("Model Efficiency", "Memory Usage", "Operation Types", "Complexity Score"),
2025-10-20 14:44:51 -04:00
specs=[[{"type": "bar"}, {"type": "bar"}], [{"type": "pie"}, {"type": "indicator"}]],
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Model efficiency metrics
efficiency_metrics = ["Model Size (MB)", "Parameters (M)", "Total Ops"]
2025-10-20 14:44:51 -04:00
efficiency_values = [model_size_mb, total_params / 1e6, total_ops]
2025-10-20 12:16:30 -04:00
fig.add_trace(
go.Bar(
x=efficiency_metrics,
y=efficiency_values,
2025-10-20 14:44:51 -04:00
marker_color=["blue", "green", "orange"],
2025-10-20 12:16:30 -04:00
showlegend=False
),
2025-10-20 14:44:51 -04:00
row=1,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Memory usage
memory_types = ["Parameters", "Est. Inference"]
memory_values = [param_memory_mb, param_memory_mb * 2] # Rough estimate
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.add_trace(
go.Bar(
x=memory_types,
y=memory_values,
2025-10-20 14:44:51 -04:00
marker_color=["purple", "red"],
2025-10-20 12:16:30 -04:00
showlegend=False
),
2025-10-20 14:44:51 -04:00
row=1,
col=2,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Operation types pie chart
fig.add_trace(
go.Pie(
2025-10-20 14:44:51 -04:00
labels=["Compute Ops", "Efficient Ops", "Other Ops"],
2025-10-20 12:16:30 -04:00
values=[compute_count, efficient_count, other_count],
2025-10-20 14:44:51 -04:00
marker_colors=["red", "green", "gray"],
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=2,
col=1,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
# Complexity score (simple heuristic)
complexity_score = min(100, (model_size_mb * 10 + total_params / 1e6 * 20 + compute_count))
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=complexity_score,
2025-10-20 14:44:51 -04:00
title={"text": "Complexity Score"},
2025-10-20 12:16:30 -04:00
gauge={
2025-10-20 14:44:51 -04:00
"axis": {"range": [0, 100]},
"bar": {
"color": "darkred" if complexity_score > 70 else "orange" if complexity_score > 40 else "green"
},
"steps": [
{"range": [0, 40], "color": "lightgreen"},
{"range": [40, 70], "color": "yellow"},
{"range": [70, 100], "color": "lightcoral"},
],
},
2025-10-20 12:16:30 -04:00
),
2025-10-20 14:44:51 -04:00
row=2,
col=2,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
fig.update_layout(
title={
2025-10-20 14:44:51 -04:00
"text": ("ONNX Performance Metrics<br>"
f"<span style='font-size:14px; color:#a0a0a0;'>"
f"Complexity Score: {complexity_score:.0f}/100</span>"),
"x": 0.5,
"xanchor": "center",
"font": {"size": 22},
2025-10-20 12:16:30 -04:00
},
height=600,
template="plotly_dark",
2025-10-20 14:44:51 -04:00
showlegend=False,
2025-10-20 12:16:30 -04:00
)
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
return fig
2025-10-20 14:44:51 -04:00
2025-10-20 12:16:30 -04:00
except Exception as e:
return create_styled_error_figure(
"Performance Analysis Error",
2025-10-20 14:44:51 -04:00
"Could not analyze ONNX model performance.",
2025-10-20 12:16:30 -04:00
f"Error: {str(e)}"
2025-10-20 14:44:51 -04:00
)