haoline 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haoline/.streamlit/config.toml +10 -0
- haoline/__init__.py +248 -0
- haoline/analyzer.py +935 -0
- haoline/cli.py +2712 -0
- haoline/compare.py +811 -0
- haoline/compare_visualizations.py +1564 -0
- haoline/edge_analysis.py +525 -0
- haoline/eval/__init__.py +131 -0
- haoline/eval/adapters.py +844 -0
- haoline/eval/cli.py +390 -0
- haoline/eval/comparison.py +542 -0
- haoline/eval/deployment.py +633 -0
- haoline/eval/schemas.py +833 -0
- haoline/examples/__init__.py +15 -0
- haoline/examples/basic_inspection.py +74 -0
- haoline/examples/compare_models.py +117 -0
- haoline/examples/hardware_estimation.py +78 -0
- haoline/format_adapters.py +1001 -0
- haoline/formats/__init__.py +123 -0
- haoline/formats/coreml.py +250 -0
- haoline/formats/gguf.py +483 -0
- haoline/formats/openvino.py +255 -0
- haoline/formats/safetensors.py +273 -0
- haoline/formats/tflite.py +369 -0
- haoline/hardware.py +2307 -0
- haoline/hierarchical_graph.py +462 -0
- haoline/html_export.py +1573 -0
- haoline/layer_summary.py +769 -0
- haoline/llm_summarizer.py +465 -0
- haoline/op_icons.py +618 -0
- haoline/operational_profiling.py +1492 -0
- haoline/patterns.py +1116 -0
- haoline/pdf_generator.py +265 -0
- haoline/privacy.py +250 -0
- haoline/pydantic_models.py +241 -0
- haoline/report.py +1923 -0
- haoline/report_sections.py +539 -0
- haoline/risks.py +521 -0
- haoline/schema.py +523 -0
- haoline/streamlit_app.py +2024 -0
- haoline/tests/__init__.py +4 -0
- haoline/tests/conftest.py +123 -0
- haoline/tests/test_analyzer.py +868 -0
- haoline/tests/test_compare_visualizations.py +293 -0
- haoline/tests/test_edge_analysis.py +243 -0
- haoline/tests/test_eval.py +604 -0
- haoline/tests/test_format_adapters.py +460 -0
- haoline/tests/test_hardware.py +237 -0
- haoline/tests/test_hardware_recommender.py +90 -0
- haoline/tests/test_hierarchical_graph.py +326 -0
- haoline/tests/test_html_export.py +180 -0
- haoline/tests/test_layer_summary.py +428 -0
- haoline/tests/test_llm_patterns.py +540 -0
- haoline/tests/test_llm_summarizer.py +339 -0
- haoline/tests/test_patterns.py +774 -0
- haoline/tests/test_pytorch.py +327 -0
- haoline/tests/test_report.py +383 -0
- haoline/tests/test_risks.py +398 -0
- haoline/tests/test_schema.py +417 -0
- haoline/tests/test_tensorflow.py +380 -0
- haoline/tests/test_visualizations.py +316 -0
- haoline/universal_ir.py +856 -0
- haoline/visualizations.py +1086 -0
- haoline/visualize_yolo.py +44 -0
- haoline/web.py +110 -0
- haoline-0.3.0.dist-info/METADATA +471 -0
- haoline-0.3.0.dist-info/RECORD +70 -0
- haoline-0.3.0.dist-info/WHEEL +4 -0
- haoline-0.3.0.dist-info/entry_points.txt +5 -0
- haoline-0.3.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Copyright (c) 2025 HaoLine Contributors
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Hierarchical Graph View for visualization.
|
|
6
|
+
|
|
7
|
+
Task 5.7: Creates collapsible, multi-level graph representations
|
|
8
|
+
for LLM-scale models where flat visualization would be unreadable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .analyzer import GraphInfo
|
|
20
|
+
from .patterns import Block
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class HierarchicalNode:
|
|
25
|
+
"""
|
|
26
|
+
A node in the hierarchical graph that can contain children.
|
|
27
|
+
|
|
28
|
+
Task 5.7.1: Create HierarchicalNode with children.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
name: str
|
|
33
|
+
node_type: str # "op", "block", "layer", "model"
|
|
34
|
+
op_type: str | None = None # ONNX op type for leaf nodes
|
|
35
|
+
|
|
36
|
+
# Hierarchy
|
|
37
|
+
children: list[HierarchicalNode] = field(default_factory=list)
|
|
38
|
+
parent_id: str | None = None
|
|
39
|
+
depth: int = 0
|
|
40
|
+
|
|
41
|
+
# State
|
|
42
|
+
is_collapsed: bool = True # Task 5.7.3
|
|
43
|
+
is_repeated: bool = False
|
|
44
|
+
repeat_count: int = 1 # Task 5.7.5: xN notation
|
|
45
|
+
|
|
46
|
+
# Aggregated stats (Task 5.7.4)
|
|
47
|
+
total_flops: int = 0
|
|
48
|
+
total_params: int = 0
|
|
49
|
+
total_memory_bytes: int = 0
|
|
50
|
+
node_count: int = 1
|
|
51
|
+
|
|
52
|
+
# Inputs/outputs for edges
|
|
53
|
+
inputs: list[str] = field(default_factory=list)
|
|
54
|
+
outputs: list[str] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
# Metadata
|
|
57
|
+
attributes: dict = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
def is_leaf(self) -> bool:
|
|
60
|
+
"""Check if this is a leaf node (no children)."""
|
|
61
|
+
return len(self.children) == 0
|
|
62
|
+
|
|
63
|
+
def get_display_name(self) -> str:
|
|
64
|
+
"""Get display name with repeat notation."""
|
|
65
|
+
if self.repeat_count > 1:
|
|
66
|
+
return f"{self.name} x{self.repeat_count}"
|
|
67
|
+
return self.name
|
|
68
|
+
|
|
69
|
+
def collapse(self) -> None:
|
|
70
|
+
"""Collapse this node."""
|
|
71
|
+
self.is_collapsed = True
|
|
72
|
+
|
|
73
|
+
def expand(self) -> None:
|
|
74
|
+
"""Expand this node."""
|
|
75
|
+
self.is_collapsed = False
|
|
76
|
+
|
|
77
|
+
def toggle(self) -> None:
|
|
78
|
+
"""Toggle collapsed state."""
|
|
79
|
+
self.is_collapsed = not self.is_collapsed
|
|
80
|
+
|
|
81
|
+
def aggregate_stats(self) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Aggregate statistics from children.
|
|
84
|
+
|
|
85
|
+
Task 5.7.4: Aggregate stats for collapsed blocks.
|
|
86
|
+
"""
|
|
87
|
+
if self.is_leaf():
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.total_flops = 0
|
|
91
|
+
self.total_params = 0
|
|
92
|
+
self.total_memory_bytes = 0
|
|
93
|
+
self.node_count = 0
|
|
94
|
+
|
|
95
|
+
for child in self.children:
|
|
96
|
+
child.aggregate_stats()
|
|
97
|
+
self.total_flops += child.total_flops
|
|
98
|
+
self.total_params += child.total_params
|
|
99
|
+
self.total_memory_bytes += child.total_memory_bytes
|
|
100
|
+
self.node_count += child.node_count
|
|
101
|
+
|
|
102
|
+
# Apply repeat multiplier
|
|
103
|
+
if self.repeat_count > 1:
|
|
104
|
+
self.total_flops *= self.repeat_count
|
|
105
|
+
self.total_memory_bytes *= self.repeat_count
|
|
106
|
+
self.node_count *= self.repeat_count
|
|
107
|
+
# Params are shared, don't multiply
|
|
108
|
+
|
|
109
|
+
def to_dict(self, include_children: bool = True) -> dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Convert to dictionary for JSON export.
|
|
112
|
+
|
|
113
|
+
Task 5.7.6: JSON export for visualization.
|
|
114
|
+
"""
|
|
115
|
+
result: dict[str, Any] = {
|
|
116
|
+
"id": self.id,
|
|
117
|
+
"name": self.name,
|
|
118
|
+
"display_name": self.get_display_name(),
|
|
119
|
+
"node_type": self.node_type,
|
|
120
|
+
"op_type": self.op_type,
|
|
121
|
+
"depth": self.depth,
|
|
122
|
+
"is_collapsed": self.is_collapsed,
|
|
123
|
+
"is_repeated": self.is_repeated,
|
|
124
|
+
"repeat_count": self.repeat_count,
|
|
125
|
+
"total_flops": self.total_flops,
|
|
126
|
+
"total_params": self.total_params,
|
|
127
|
+
"total_memory_bytes": self.total_memory_bytes,
|
|
128
|
+
"node_count": self.node_count,
|
|
129
|
+
"inputs": self.inputs,
|
|
130
|
+
"outputs": self.outputs,
|
|
131
|
+
"attributes": self.attributes,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if include_children and self.children:
|
|
135
|
+
result["children"] = [c.to_dict(include_children) for c in self.children]
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class HierarchicalGraph:
|
|
142
|
+
"""Complete hierarchical graph representation."""
|
|
143
|
+
|
|
144
|
+
root: HierarchicalNode
|
|
145
|
+
nodes_by_id: dict[str, HierarchicalNode] = field(default_factory=dict)
|
|
146
|
+
total_nodes: int = 0
|
|
147
|
+
depth: int = 0
|
|
148
|
+
|
|
149
|
+
def get_node(self, node_id: str) -> HierarchicalNode | None:
|
|
150
|
+
"""Get node by ID."""
|
|
151
|
+
return self.nodes_by_id.get(node_id)
|
|
152
|
+
|
|
153
|
+
def collapse_all(self) -> None:
|
|
154
|
+
"""Collapse all non-leaf nodes."""
|
|
155
|
+
for node in self.nodes_by_id.values():
|
|
156
|
+
if not node.is_leaf():
|
|
157
|
+
node.collapse()
|
|
158
|
+
|
|
159
|
+
def expand_all(self) -> None:
|
|
160
|
+
"""Expand all nodes."""
|
|
161
|
+
for node in self.nodes_by_id.values():
|
|
162
|
+
node.expand()
|
|
163
|
+
|
|
164
|
+
def expand_to_depth(self, max_depth: int) -> None:
|
|
165
|
+
"""Expand nodes up to a certain depth."""
|
|
166
|
+
for node in self.nodes_by_id.values():
|
|
167
|
+
if node.depth <= max_depth:
|
|
168
|
+
node.expand()
|
|
169
|
+
else:
|
|
170
|
+
node.collapse()
|
|
171
|
+
|
|
172
|
+
def get_visible_nodes(self) -> list[HierarchicalNode]:
|
|
173
|
+
"""Get nodes visible given current collapse state."""
|
|
174
|
+
visible: list[HierarchicalNode] = []
|
|
175
|
+
self._collect_visible(self.root, visible)
|
|
176
|
+
return visible
|
|
177
|
+
|
|
178
|
+
def _collect_visible(self, node: HierarchicalNode, visible: list[HierarchicalNode]) -> None:
|
|
179
|
+
"""Recursively collect visible nodes."""
|
|
180
|
+
visible.append(node)
|
|
181
|
+
if not node.is_collapsed:
|
|
182
|
+
for child in node.children:
|
|
183
|
+
self._collect_visible(child, visible)
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> dict:
|
|
186
|
+
"""Export to dictionary."""
|
|
187
|
+
return {
|
|
188
|
+
"root": self.root.to_dict(),
|
|
189
|
+
"total_nodes": self.total_nodes,
|
|
190
|
+
"depth": self.depth,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
def to_json(self, indent: int | None = 2) -> str:
|
|
194
|
+
"""Export to JSON string."""
|
|
195
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class HierarchicalGraphBuilder:
|
|
199
|
+
"""
|
|
200
|
+
Build hierarchical graph from flat ONNX graph and detected blocks.
|
|
201
|
+
|
|
202
|
+
Task 5.7.2: Convert Blocks to HierarchicalNodes.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, logger: logging.Logger | None = None):
|
|
206
|
+
self.logger = logger or logging.getLogger("haoline.hierarchy")
|
|
207
|
+
|
|
208
|
+
def build(
|
|
209
|
+
self,
|
|
210
|
+
graph_info: GraphInfo,
|
|
211
|
+
blocks: list[Block],
|
|
212
|
+
model_name: str = "Model",
|
|
213
|
+
) -> HierarchicalGraph:
|
|
214
|
+
"""
|
|
215
|
+
Build hierarchical graph from ONNX graph and detected blocks.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
graph_info: Parsed ONNX graph.
|
|
219
|
+
blocks: Detected architectural blocks.
|
|
220
|
+
model_name: Name for the root node.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
HierarchicalGraph with multi-level structure.
|
|
224
|
+
"""
|
|
225
|
+
nodes_by_id: dict[str, HierarchicalNode] = {}
|
|
226
|
+
|
|
227
|
+
# Create root node
|
|
228
|
+
root = HierarchicalNode(
|
|
229
|
+
id="root",
|
|
230
|
+
name=model_name,
|
|
231
|
+
node_type="model",
|
|
232
|
+
depth=0,
|
|
233
|
+
is_collapsed=False,
|
|
234
|
+
)
|
|
235
|
+
nodes_by_id["root"] = root
|
|
236
|
+
|
|
237
|
+
# Map op nodes to block memberships
|
|
238
|
+
node_to_block: dict[str, str] = {}
|
|
239
|
+
for block in blocks:
|
|
240
|
+
for node_name in block.nodes:
|
|
241
|
+
if node_name not in node_to_block:
|
|
242
|
+
node_to_block[node_name] = block.name
|
|
243
|
+
|
|
244
|
+
# Create block nodes
|
|
245
|
+
block_nodes: dict[str, HierarchicalNode] = {}
|
|
246
|
+
for block in blocks:
|
|
247
|
+
block_node = HierarchicalNode(
|
|
248
|
+
id=f"block_{block.name}",
|
|
249
|
+
name=block.name,
|
|
250
|
+
node_type="block",
|
|
251
|
+
depth=1,
|
|
252
|
+
parent_id="root",
|
|
253
|
+
attributes=block.attributes.copy() if block.attributes else {},
|
|
254
|
+
)
|
|
255
|
+
block_node.attributes["block_type"] = block.block_type
|
|
256
|
+
block_nodes[block.name] = block_node
|
|
257
|
+
nodes_by_id[block_node.id] = block_node
|
|
258
|
+
|
|
259
|
+
# Process repeated blocks (Task 5.7.5)
|
|
260
|
+
repeated_blocks = [b for b in blocks if b.block_type == "RepeatedBlock"]
|
|
261
|
+
for rb in repeated_blocks:
|
|
262
|
+
rb.attributes.get("repeated_type", "")
|
|
263
|
+
repeat_count = rb.attributes.get("num_repetitions", 1)
|
|
264
|
+
block_names = rb.attributes.get("block_names", [])
|
|
265
|
+
|
|
266
|
+
# Mark the first block as repeated, hide others
|
|
267
|
+
if block_names and repeat_count > 1:
|
|
268
|
+
first_block_name = block_names[0]
|
|
269
|
+
if first_block_name in block_nodes:
|
|
270
|
+
block_nodes[first_block_name].is_repeated = True
|
|
271
|
+
block_nodes[first_block_name].repeat_count = repeat_count
|
|
272
|
+
|
|
273
|
+
# Remove subsequent blocks from display (they're represented by xN)
|
|
274
|
+
for name in block_names[1:]:
|
|
275
|
+
if name in block_nodes:
|
|
276
|
+
# Mark as hidden by adding to a "collapsed repeated" group
|
|
277
|
+
block_nodes[name].attributes["hidden_by_repeat"] = True
|
|
278
|
+
|
|
279
|
+
# Create op nodes and assign to blocks or root
|
|
280
|
+
for node in graph_info.nodes:
|
|
281
|
+
op_node = HierarchicalNode(
|
|
282
|
+
id=f"op_{node.name}",
|
|
283
|
+
name=node.name,
|
|
284
|
+
node_type="op",
|
|
285
|
+
op_type=node.op_type,
|
|
286
|
+
inputs=list(node.inputs),
|
|
287
|
+
outputs=list(node.outputs),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Get stats from graph_info if available
|
|
291
|
+
if hasattr(graph_info, "node_flops"):
|
|
292
|
+
op_node.total_flops = graph_info.node_flops.get(node.name, 0)
|
|
293
|
+
|
|
294
|
+
if node.name in node_to_block:
|
|
295
|
+
# Node belongs to a block
|
|
296
|
+
block_name = node_to_block[node.name]
|
|
297
|
+
if block_name in block_nodes:
|
|
298
|
+
parent = block_nodes[block_name]
|
|
299
|
+
op_node.parent_id = parent.id
|
|
300
|
+
op_node.depth = parent.depth + 1
|
|
301
|
+
parent.children.append(op_node)
|
|
302
|
+
else:
|
|
303
|
+
# Standalone node at root level
|
|
304
|
+
op_node.parent_id = "root"
|
|
305
|
+
op_node.depth = 1
|
|
306
|
+
root.children.append(op_node)
|
|
307
|
+
|
|
308
|
+
nodes_by_id[op_node.id] = op_node
|
|
309
|
+
|
|
310
|
+
# Add non-hidden blocks to root
|
|
311
|
+
for block_node in block_nodes.values():
|
|
312
|
+
if not block_node.attributes.get("hidden_by_repeat", False):
|
|
313
|
+
root.children.append(block_node)
|
|
314
|
+
|
|
315
|
+
# Aggregate stats up the tree
|
|
316
|
+
root.aggregate_stats()
|
|
317
|
+
|
|
318
|
+
# Calculate depth
|
|
319
|
+
max_depth = max(n.depth for n in nodes_by_id.values())
|
|
320
|
+
|
|
321
|
+
return HierarchicalGraph(
|
|
322
|
+
root=root,
|
|
323
|
+
nodes_by_id=nodes_by_id,
|
|
324
|
+
total_nodes=len(nodes_by_id),
|
|
325
|
+
depth=max_depth,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def build_layer_hierarchy(
|
|
329
|
+
self,
|
|
330
|
+
graph_info: GraphInfo,
|
|
331
|
+
blocks: list[Block],
|
|
332
|
+
model_name: str = "Model",
|
|
333
|
+
) -> HierarchicalGraph:
|
|
334
|
+
"""
|
|
335
|
+
Build a 3-level hierarchy: Model -> Layers -> Blocks -> Ops.
|
|
336
|
+
|
|
337
|
+
For LLMs, groups attention+MLP into "TransformerLayer" containers.
|
|
338
|
+
"""
|
|
339
|
+
basic_graph = self.build(graph_info, blocks, model_name)
|
|
340
|
+
|
|
341
|
+
# Group consecutive attention + MLP blocks into layers
|
|
342
|
+
layers: list[HierarchicalNode] = []
|
|
343
|
+
current_layer_blocks: list[HierarchicalNode] = []
|
|
344
|
+
layer_idx = 0
|
|
345
|
+
|
|
346
|
+
for child in basic_graph.root.children:
|
|
347
|
+
if child.node_type == "block":
|
|
348
|
+
block_type = child.attributes.get("block_type", "")
|
|
349
|
+
|
|
350
|
+
# Check if this starts a new layer
|
|
351
|
+
if block_type in ("AttentionHead", "Attention") and current_layer_blocks:
|
|
352
|
+
# Previous blocks become a layer
|
|
353
|
+
if len(current_layer_blocks) > 1:
|
|
354
|
+
layer = self._create_layer_node(current_layer_blocks, layer_idx)
|
|
355
|
+
layers.append(layer)
|
|
356
|
+
layer_idx += 1
|
|
357
|
+
else:
|
|
358
|
+
layers.extend(current_layer_blocks)
|
|
359
|
+
current_layer_blocks = []
|
|
360
|
+
|
|
361
|
+
current_layer_blocks.append(child)
|
|
362
|
+
else:
|
|
363
|
+
# Non-block node, flush any pending layer
|
|
364
|
+
if current_layer_blocks:
|
|
365
|
+
if len(current_layer_blocks) > 1:
|
|
366
|
+
layer = self._create_layer_node(current_layer_blocks, layer_idx)
|
|
367
|
+
layers.append(layer)
|
|
368
|
+
layer_idx += 1
|
|
369
|
+
else:
|
|
370
|
+
layers.extend(current_layer_blocks)
|
|
371
|
+
current_layer_blocks = []
|
|
372
|
+
layers.append(child)
|
|
373
|
+
|
|
374
|
+
# Handle remaining blocks
|
|
375
|
+
if current_layer_blocks:
|
|
376
|
+
if len(current_layer_blocks) > 1:
|
|
377
|
+
layer = self._create_layer_node(current_layer_blocks, layer_idx)
|
|
378
|
+
layers.append(layer)
|
|
379
|
+
else:
|
|
380
|
+
layers.extend(current_layer_blocks)
|
|
381
|
+
|
|
382
|
+
# Update root children
|
|
383
|
+
basic_graph.root.children = layers
|
|
384
|
+
|
|
385
|
+
# Update nodes_by_id
|
|
386
|
+
for layer in layers:
|
|
387
|
+
if layer.node_type == "layer":
|
|
388
|
+
basic_graph.nodes_by_id[layer.id] = layer
|
|
389
|
+
|
|
390
|
+
# Recalculate depths
|
|
391
|
+
self._recalculate_depths(basic_graph.root, 0)
|
|
392
|
+
basic_graph.depth = max(n.depth for n in basic_graph.nodes_by_id.values())
|
|
393
|
+
|
|
394
|
+
# Re-aggregate stats
|
|
395
|
+
basic_graph.root.aggregate_stats()
|
|
396
|
+
|
|
397
|
+
return basic_graph
|
|
398
|
+
|
|
399
|
+
def _create_layer_node(
|
|
400
|
+
self, blocks: list[HierarchicalNode], layer_idx: int
|
|
401
|
+
) -> HierarchicalNode:
|
|
402
|
+
"""Create a layer node containing multiple blocks."""
|
|
403
|
+
layer = HierarchicalNode(
|
|
404
|
+
id=f"layer_{layer_idx}",
|
|
405
|
+
name=f"Layer {layer_idx}",
|
|
406
|
+
node_type="layer",
|
|
407
|
+
depth=1,
|
|
408
|
+
parent_id="root",
|
|
409
|
+
children=blocks,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Update children's parent
|
|
413
|
+
for block in blocks:
|
|
414
|
+
block.parent_id = layer.id
|
|
415
|
+
block.depth = 2
|
|
416
|
+
# Update children of blocks
|
|
417
|
+
for child in block.children:
|
|
418
|
+
child.depth = 3
|
|
419
|
+
|
|
420
|
+
return layer
|
|
421
|
+
|
|
422
|
+
def _recalculate_depths(self, node: HierarchicalNode, depth: int) -> None:
|
|
423
|
+
"""Recursively recalculate depths."""
|
|
424
|
+
node.depth = depth
|
|
425
|
+
for child in node.children:
|
|
426
|
+
self._recalculate_depths(child, depth + 1)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def generate_summary(graph: HierarchicalGraph) -> str:
|
|
430
|
+
"""
|
|
431
|
+
Generate multi-level text summary.
|
|
432
|
+
|
|
433
|
+
Task 5.7.5: Generate multi-level summary (xN notation).
|
|
434
|
+
"""
|
|
435
|
+
lines = []
|
|
436
|
+
lines.append(f"# {graph.root.name}")
|
|
437
|
+
lines.append(f"Total Nodes: {graph.total_nodes}")
|
|
438
|
+
lines.append(f"Max Depth: {graph.depth}")
|
|
439
|
+
lines.append("")
|
|
440
|
+
|
|
441
|
+
def format_node(node: HierarchicalNode, indent: int = 0) -> None:
|
|
442
|
+
prefix = " " * indent
|
|
443
|
+
display = node.get_display_name()
|
|
444
|
+
|
|
445
|
+
if node.node_type == "op":
|
|
446
|
+
lines.append(f"{prefix}- {display} ({node.op_type})")
|
|
447
|
+
elif node.node_type == "block":
|
|
448
|
+
block_type = node.attributes.get("block_type", "Block")
|
|
449
|
+
lines.append(f"{prefix}[{block_type}] {display}")
|
|
450
|
+
elif node.node_type == "layer":
|
|
451
|
+
lines.append(f"{prefix}== {display} ==")
|
|
452
|
+
else:
|
|
453
|
+
lines.append(f"{prefix}{display}")
|
|
454
|
+
|
|
455
|
+
if not node.is_collapsed:
|
|
456
|
+
for child in node.children:
|
|
457
|
+
format_node(child, indent + 1)
|
|
458
|
+
|
|
459
|
+
for child in graph.root.children:
|
|
460
|
+
format_node(child, 0)
|
|
461
|
+
|
|
462
|
+
return "\n".join(lines)
|