kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__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.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,661 @@
1
+ """
2
+ Advanced Hierarchy Manager for KiCAD schematic hierarchical designs.
3
+
4
+ Handles complex hierarchical features including:
5
+ - Sheets used multiple times (reusable sheets)
6
+ - Cross-sheet signal tracking
7
+ - Sheet pin validation
8
+ - Hierarchy flattening
9
+ - Signal tracing through hierarchy levels
10
+ """
11
+
12
+ import logging
13
+ from collections import defaultdict
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Set, Tuple
17
+
18
+ from ..types import Point
19
+ from .base import BaseManager
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class SheetInstance:
26
+ """Represents a single instance of a hierarchical sheet."""
27
+
28
+ sheet_uuid: str # UUID of sheet symbol in parent
29
+ sheet_name: str # Name of the sheet
30
+ filename: str # Referenced schematic filename
31
+ path: str # Hierarchical path (e.g., "/root_uuid/sheet_uuid")
32
+ parent_path: str # Parent's hierarchical path
33
+ schematic: Optional[Any] = None # Loaded schematic object
34
+ sheet_pins: List[Dict[str, Any]] = field(default_factory=list)
35
+ position: Optional[Point] = None
36
+ instances_in_parent: int = 1 # How many times this sheet is used
37
+
38
+
39
+ @dataclass
40
+ class HierarchyNode:
41
+ """Represents a node in the hierarchy tree."""
42
+
43
+ path: str # Hierarchical path
44
+ name: str # Sheet name
45
+ filename: Optional[str] = None # Schematic filename
46
+ schematic: Optional[Any] = None
47
+ parent: Optional["HierarchyNode"] = None
48
+ children: List["HierarchyNode"] = field(default_factory=list)
49
+ sheet_uuid: Optional[str] = None
50
+ is_root: bool = False
51
+
52
+ def add_child(self, child: "HierarchyNode"):
53
+ """Add child node."""
54
+ child.parent = self
55
+ self.children.append(child)
56
+
57
+ def get_depth(self) -> int:
58
+ """Get depth in hierarchy (root = 0)."""
59
+ depth = 0
60
+ node = self.parent
61
+ while node:
62
+ depth += 1
63
+ node = node.parent
64
+ return depth
65
+
66
+ def get_full_path(self) -> List[str]:
67
+ """Get full path from root to this node."""
68
+ path = []
69
+ node = self
70
+ while node:
71
+ path.insert(0, node.name)
72
+ node = node.parent
73
+ return path
74
+
75
+
76
+ @dataclass
77
+ class SheetPinConnection:
78
+ """Represents a connection between a sheet pin and hierarchical label."""
79
+
80
+ sheet_path: str # Path to sheet instance
81
+ sheet_pin_name: str
82
+ sheet_pin_type: str
83
+ sheet_pin_uuid: str
84
+ hierarchical_label_name: str
85
+ hierarchical_label_uuid: Optional[str] = None
86
+ child_schematic_path: Optional[str] = None
87
+ validated: bool = False
88
+ validation_errors: List[str] = field(default_factory=list)
89
+
90
+
91
+ @dataclass
92
+ class SignalPath:
93
+ """Represents a signal's path through the hierarchy."""
94
+
95
+ signal_name: str
96
+ start_path: str # Hierarchical path where signal starts
97
+ end_path: str # Hierarchical path where signal ends
98
+ connections: List[str] = field(default_factory=list) # List of connection points
99
+ sheet_crossings: int = 0 # Number of sheet boundaries crossed
100
+
101
+
102
+ class HierarchyManager(BaseManager):
103
+ """
104
+ Manages advanced hierarchical schematic features.
105
+
106
+ Provides:
107
+ - Sheet reuse tracking (same sheet used multiple times)
108
+ - Cross-sheet signal tracking
109
+ - Sheet pin validation
110
+ - Hierarchy flattening
111
+ - Signal tracing
112
+ - Hierarchy visualization
113
+ """
114
+
115
+ def __init__(self, schematic_data: Dict[str, Any]):
116
+ """
117
+ Initialize HierarchyManager.
118
+
119
+ Args:
120
+ schematic_data: Reference to schematic data
121
+ """
122
+ super().__init__(schematic_data)
123
+ self._hierarchy_tree: Optional[HierarchyNode] = None
124
+ self._sheet_instances: Dict[str, List[SheetInstance]] = defaultdict(list)
125
+ self._loaded_schematics: Dict[str, Any] = {}
126
+ self._pin_connections: List[SheetPinConnection] = []
127
+
128
+ def build_hierarchy_tree(self, root_schematic, root_path: Optional[Path] = None) -> HierarchyNode:
129
+ """
130
+ Build complete hierarchy tree from root schematic.
131
+
132
+ Args:
133
+ root_schematic: Root schematic object
134
+ root_path: Path to root schematic file
135
+
136
+ Returns:
137
+ Root HierarchyNode representing the hierarchy tree
138
+ """
139
+ logger.info("Building hierarchy tree...")
140
+
141
+ # Create root node
142
+ root_node = HierarchyNode(
143
+ path="/",
144
+ name=getattr(root_schematic, 'name', 'Root') or "Root",
145
+ filename=str(root_path) if root_path else None,
146
+ schematic=root_schematic,
147
+ is_root=True,
148
+ )
149
+
150
+ # Track root schematic
151
+ self._loaded_schematics["/"] = root_schematic
152
+ self._hierarchy_tree = root_node
153
+
154
+ # Recursively build tree
155
+ self._build_tree_recursive(root_node, root_schematic, root_path, "/")
156
+
157
+ logger.info(f"Hierarchy tree built: {self._count_nodes(root_node)} nodes")
158
+ return root_node
159
+
160
+ def _build_tree_recursive(
161
+ self,
162
+ parent_node: HierarchyNode,
163
+ parent_schematic,
164
+ parent_path: Optional[Path],
165
+ current_path: str,
166
+ ):
167
+ """Recursively build hierarchy tree."""
168
+ # Get sheets from parent schematic
169
+ sheets = self._data.get("sheets", []) if parent_schematic == self._get_root_schematic() else []
170
+
171
+ if hasattr(parent_schematic, "_data"):
172
+ sheets = parent_schematic._data.get("sheets", [])
173
+
174
+ for sheet in sheets:
175
+ sheet_uuid = sheet.get("uuid")
176
+ sheet_name = sheet.get("name", "Unnamed")
177
+ sheet_filename = sheet.get("filename")
178
+
179
+ if not sheet_filename:
180
+ logger.warning(f"Sheet {sheet_name} has no filename")
181
+ continue
182
+
183
+ # Build hierarchical path
184
+ sheet_path = f"{current_path}{sheet_uuid}/"
185
+
186
+ # Create child node
187
+ child_node = HierarchyNode(
188
+ path=sheet_path,
189
+ name=sheet_name,
190
+ filename=sheet_filename,
191
+ sheet_uuid=sheet_uuid,
192
+ )
193
+
194
+ parent_node.add_child(child_node)
195
+
196
+ # Load child schematic if exists
197
+ if parent_path:
198
+ child_path = parent_path.parent / sheet_filename
199
+ if child_path.exists():
200
+ try:
201
+ # Import here to avoid circular dependency
202
+ import kicad_sch_api as ksa
203
+
204
+ child_sch = ksa.Schematic.load(str(child_path))
205
+ child_node.schematic = child_sch
206
+ self._loaded_schematics[sheet_path] = child_sch
207
+
208
+ # Track sheet instance
209
+ sheet_instance = SheetInstance(
210
+ sheet_uuid=sheet_uuid,
211
+ sheet_name=sheet_name,
212
+ filename=sheet_filename,
213
+ path=sheet_path,
214
+ parent_path=current_path,
215
+ schematic=child_sch,
216
+ sheet_pins=sheet.get("pins", []),
217
+ position=Point(
218
+ sheet["position"]["x"], sheet["position"]["y"]
219
+ ) if "position" in sheet else None,
220
+ )
221
+ self._sheet_instances[sheet_filename].append(sheet_instance)
222
+
223
+ # Recursively process child sheets
224
+ self._build_tree_recursive(child_node, child_sch, child_path, sheet_path)
225
+
226
+ logger.debug(f"Loaded child schematic: {sheet_filename} at {sheet_path}")
227
+ except Exception as e:
228
+ logger.warning(f"Could not load child schematic {sheet_filename}: {e}")
229
+ else:
230
+ logger.warning(f"Child schematic not found: {child_path}")
231
+
232
+ def find_reused_sheets(self) -> Dict[str, List[SheetInstance]]:
233
+ """
234
+ Find sheets that are used multiple times in the hierarchy.
235
+
236
+ Returns:
237
+ Dictionary mapping filename to list of sheet instances
238
+ """
239
+ reused = {}
240
+ for filename, instances in self._sheet_instances.items():
241
+ if len(instances) > 1:
242
+ reused[filename] = instances
243
+ logger.info(f"Sheet '{filename}' is reused {len(instances)} times")
244
+
245
+ return reused
246
+
247
+ def validate_sheet_pins(self) -> List[SheetPinConnection]:
248
+ """
249
+ Validate sheet pin connections against hierarchical labels.
250
+
251
+ Checks:
252
+ - Sheet pins have matching hierarchical labels in child
253
+ - Pin types are compatible
254
+ - Pin names match exactly
255
+ - No duplicate pins
256
+
257
+ Returns:
258
+ List of validated sheet pin connections with validation status
259
+ """
260
+ logger.info("Validating sheet pin connections...")
261
+ self._pin_connections = []
262
+
263
+ for filename, instances in self._sheet_instances.items():
264
+ for instance in instances:
265
+ child_sch = instance.schematic
266
+ if not child_sch:
267
+ continue
268
+
269
+ # Get hierarchical labels from child schematic
270
+ child_labels = self._get_hierarchical_labels(child_sch)
271
+ child_label_map = {label["name"]: label for label in child_labels}
272
+
273
+ # Validate each sheet pin
274
+ for pin in instance.sheet_pins:
275
+ pin_name = pin.get("name")
276
+ pin_type = pin.get("pin_type")
277
+ pin_uuid = pin.get("uuid")
278
+
279
+ connection = SheetPinConnection(
280
+ sheet_path=instance.path,
281
+ sheet_pin_name=pin_name,
282
+ sheet_pin_type=pin_type,
283
+ sheet_pin_uuid=pin_uuid,
284
+ hierarchical_label_name=pin_name,
285
+ child_schematic_path=str(instance.filename),
286
+ )
287
+
288
+ # Check if matching hierarchical label exists
289
+ if pin_name not in child_label_map:
290
+ connection.validation_errors.append(
291
+ f"No matching hierarchical label '{pin_name}' in {filename}"
292
+ )
293
+ else:
294
+ matching_label = child_label_map[pin_name]
295
+ connection.hierarchical_label_uuid = matching_label.get("uuid")
296
+
297
+ # Validate pin type compatibility
298
+ label_type = matching_label.get("shape", "input")
299
+ if not self._are_pin_types_compatible(pin_type, label_type):
300
+ connection.validation_errors.append(
301
+ f"Pin type mismatch: sheet pin '{pin_type}' vs label '{label_type}'"
302
+ )
303
+
304
+ connection.validated = len(connection.validation_errors) == 0
305
+ self._pin_connections.append(connection)
306
+
307
+ # Log validation results
308
+ valid_count = sum(1 for c in self._pin_connections if c.validated)
309
+ logger.info(
310
+ f"Sheet pin validation: {valid_count}/{len(self._pin_connections)} valid connections"
311
+ )
312
+
313
+ return self._pin_connections
314
+
315
+ def get_validation_errors(self) -> List[Dict[str, Any]]:
316
+ """
317
+ Get all sheet pin validation errors.
318
+
319
+ Returns:
320
+ List of validation error dictionaries
321
+ """
322
+ errors = []
323
+ for connection in self._pin_connections:
324
+ if not connection.validated:
325
+ for error_msg in connection.validation_errors:
326
+ errors.append(
327
+ {
328
+ "sheet_path": connection.sheet_path,
329
+ "pin_name": connection.sheet_pin_name,
330
+ "error": error_msg,
331
+ }
332
+ )
333
+ return errors
334
+
335
+ def trace_signal_path(
336
+ self, signal_name: str, start_path: str = "/"
337
+ ) -> List[SignalPath]:
338
+ """
339
+ Trace a signal through the hierarchy.
340
+
341
+ Args:
342
+ signal_name: Name of signal to trace
343
+ start_path: Starting hierarchical path (default: root)
344
+
345
+ Returns:
346
+ List of SignalPath objects showing signal routing
347
+ """
348
+ logger.info(f"Tracing signal '{signal_name}' from {start_path}")
349
+ paths = []
350
+
351
+ # Find signal in start schematic
352
+ start_sch = self._loaded_schematics.get(start_path)
353
+ if not start_sch:
354
+ logger.warning(f"No schematic found at path: {start_path}")
355
+ return paths
356
+
357
+ # Search for labels with this signal name
358
+ labels = self._get_all_labels(start_sch)
359
+ matching_labels = [l for l in labels if l.get("name") == signal_name]
360
+
361
+ for label in matching_labels:
362
+ signal_path = SignalPath(
363
+ signal_name=signal_name,
364
+ start_path=start_path,
365
+ end_path=start_path,
366
+ connections=[f"{start_path}:{label.get('type', 'label')}"],
367
+ )
368
+
369
+ # If it's a hierarchical label, trace upward
370
+ if label.get("type") == "hierarchical":
371
+ self._trace_hierarchical_upward(signal_path, signal_name, start_path)
372
+
373
+ # If it's a global label, find all instances
374
+ if label.get("type") == "global":
375
+ self._trace_global_connections(signal_path, signal_name)
376
+
377
+ paths.append(signal_path)
378
+
379
+ logger.info(f"Found {len(paths)} signal paths for '{signal_name}'")
380
+ return paths
381
+
382
+ def flatten_hierarchy(
383
+ self, prefix_references: bool = True
384
+ ) -> Dict[str, Any]:
385
+ """
386
+ Flatten hierarchical design into a single schematic representation.
387
+
388
+ Args:
389
+ prefix_references: If True, prefix component references with sheet path
390
+
391
+ Returns:
392
+ Dictionary containing flattened schematic data
393
+
394
+ Note: This creates a data representation only - does not create a real schematic
395
+ """
396
+ logger.info("Flattening hierarchy...")
397
+
398
+ if not self._hierarchy_tree:
399
+ logger.error("Hierarchy tree not built. Call build_hierarchy_tree() first")
400
+ return {}
401
+
402
+ flattened = {
403
+ "components": [],
404
+ "wires": [],
405
+ "labels": [],
406
+ "junctions": [],
407
+ "nets": [],
408
+ "hierarchy_map": {}, # Maps flattened refs to original paths
409
+ }
410
+
411
+ # Recursively flatten from root
412
+ self._flatten_recursive(
413
+ self._hierarchy_tree,
414
+ flattened,
415
+ prefix_references,
416
+ "",
417
+ )
418
+
419
+ logger.info(
420
+ f"Flattened hierarchy: {len(flattened['components'])} components, "
421
+ f"{len(flattened['wires'])} wires"
422
+ )
423
+
424
+ return flattened
425
+
426
+ def _flatten_recursive(
427
+ self,
428
+ node: HierarchyNode,
429
+ flattened: Dict[str, Any],
430
+ prefix_references: bool,
431
+ prefix: str,
432
+ ):
433
+ """Recursively flatten hierarchy tree."""
434
+ if not node.schematic:
435
+ return
436
+
437
+ # Process components
438
+ for component in node.schematic.components:
439
+ comp_data = component._data if hasattr(component, "_data") else component
440
+
441
+ # Create reference prefix from hierarchy path
442
+ if prefix_references and not node.is_root:
443
+ new_ref = f"{prefix}{component.reference}"
444
+ else:
445
+ new_ref = component.reference
446
+
447
+ flattened_comp = {
448
+ "reference": new_ref,
449
+ "original_reference": component.reference,
450
+ "lib_id": component.lib_id,
451
+ "value": component.value,
452
+ "position": component.position,
453
+ "hierarchy_path": node.path,
454
+ "original_data": comp_data,
455
+ }
456
+
457
+ flattened["components"].append(flattened_comp)
458
+ flattened["hierarchy_map"][new_ref] = node.path
459
+
460
+ # Process wires, labels, junctions similarly
461
+ if hasattr(node.schematic, "_data"):
462
+ # Copy wires
463
+ wires = node.schematic._data.get("wires", [])
464
+ for wire in wires:
465
+ flattened["wires"].append(
466
+ {
467
+ "hierarchy_path": node.path,
468
+ "data": wire,
469
+ }
470
+ )
471
+
472
+ # Copy labels
473
+ labels = node.schematic._data.get("labels", [])
474
+ for label in labels:
475
+ flattened["labels"].append(
476
+ {
477
+ "hierarchy_path": node.path,
478
+ "data": label,
479
+ }
480
+ )
481
+
482
+ # Recursively process children
483
+ for child in node.children:
484
+ child_prefix = f"{prefix}{node.name}_" if prefix_references else prefix
485
+ self._flatten_recursive(child, flattened, prefix_references, child_prefix)
486
+
487
+ def get_hierarchy_statistics(self) -> Dict[str, Any]:
488
+ """
489
+ Get comprehensive hierarchy statistics.
490
+
491
+ Returns:
492
+ Dictionary with hierarchy statistics
493
+ """
494
+ if not self._hierarchy_tree:
495
+ return {"error": "Hierarchy tree not built"}
496
+
497
+ total_nodes = self._count_nodes(self._hierarchy_tree)
498
+ max_depth = self._get_max_depth(self._hierarchy_tree)
499
+ reused_sheets = self.find_reused_sheets()
500
+
501
+ total_components = 0
502
+ total_wires = 0
503
+ total_labels = 0
504
+
505
+ for schematic in self._loaded_schematics.values():
506
+ if hasattr(schematic, "components"):
507
+ total_components += len(list(schematic.components))
508
+ if hasattr(schematic, "_data"):
509
+ total_wires += len(schematic._data.get("wires", []))
510
+ total_labels += len(schematic._data.get("labels", []))
511
+
512
+ return {
513
+ "total_sheets": total_nodes,
514
+ "max_hierarchy_depth": max_depth,
515
+ "reused_sheets_count": len(reused_sheets),
516
+ "reused_sheets": {
517
+ filename: len(instances)
518
+ for filename, instances in reused_sheets.items()
519
+ },
520
+ "total_components": total_components,
521
+ "total_wires": total_wires,
522
+ "total_labels": total_labels,
523
+ "loaded_schematics": len(self._loaded_schematics),
524
+ "sheet_pin_connections": len(self._pin_connections),
525
+ "valid_connections": sum(
526
+ 1 for c in self._pin_connections if c.validated
527
+ ),
528
+ }
529
+
530
+ def visualize_hierarchy(self, include_stats: bool = False) -> str:
531
+ """
532
+ Generate text visualization of hierarchy tree.
533
+
534
+ Args:
535
+ include_stats: Include statistics for each node
536
+
537
+ Returns:
538
+ String representation of hierarchy tree
539
+ """
540
+ if not self._hierarchy_tree:
541
+ return "Hierarchy tree not built. Call build_hierarchy_tree() first."
542
+
543
+ lines = []
544
+ self._visualize_recursive(self._hierarchy_tree, lines, "", include_stats)
545
+ return "\n".join(lines)
546
+
547
+ def _visualize_recursive(
548
+ self,
549
+ node: HierarchyNode,
550
+ lines: List[str],
551
+ prefix: str,
552
+ include_stats: bool,
553
+ ):
554
+ """Recursively generate hierarchy visualization."""
555
+ # Create node line
556
+ is_last = False # Will be set properly when we know
557
+ connector = "└── " if is_last else "├── "
558
+
559
+ node_str = f"{prefix}{connector}{node.name}"
560
+
561
+ if node.filename:
562
+ node_str += f" [{node.filename}]"
563
+
564
+ if include_stats and node.schematic:
565
+ comp_count = len(list(node.schematic.components)) if hasattr(node.schematic, "components") else 0
566
+ node_str += f" ({comp_count} components)"
567
+
568
+ lines.append(node_str)
569
+
570
+ # Process children
571
+ for i, child in enumerate(node.children):
572
+ is_last_child = i == len(node.children) - 1
573
+ child_prefix = prefix + (" " if is_last else "│ ")
574
+ self._visualize_recursive(child, lines, child_prefix, include_stats)
575
+
576
+ # Helper methods
577
+
578
+ def _count_nodes(self, node: HierarchyNode) -> int:
579
+ """Count total nodes in tree."""
580
+ count = 1
581
+ for child in node.children:
582
+ count += self._count_nodes(child)
583
+ return count
584
+
585
+ def _get_max_depth(self, node: HierarchyNode, current_depth: int = 0) -> int:
586
+ """Get maximum depth of tree."""
587
+ if not node.children:
588
+ return current_depth
589
+
590
+ max_child_depth = current_depth
591
+ for child in node.children:
592
+ child_depth = self._get_max_depth(child, current_depth + 1)
593
+ max_child_depth = max(max_child_depth, child_depth)
594
+
595
+ return max_child_depth
596
+
597
+ def _get_hierarchical_labels(self, schematic) -> List[Dict[str, Any]]:
598
+ """Get all hierarchical labels from a schematic."""
599
+ labels = []
600
+ if hasattr(schematic, "_data"):
601
+ for label in schematic._data.get("labels", []):
602
+ if label.get("type") == "hierarchical":
603
+ labels.append(label)
604
+ return labels
605
+
606
+ def _get_all_labels(self, schematic) -> List[Dict[str, Any]]:
607
+ """Get all labels from a schematic."""
608
+ if hasattr(schematic, "_data"):
609
+ return schematic._data.get("labels", [])
610
+ return []
611
+
612
+ def _are_pin_types_compatible(self, pin_type: str, label_type: str) -> bool:
613
+ """
614
+ Check if sheet pin type is compatible with hierarchical label type.
615
+
616
+ Args:
617
+ pin_type: Sheet pin type
618
+ label_type: Hierarchical label shape/type
619
+
620
+ Returns:
621
+ True if compatible
622
+ """
623
+ # Define compatibility rules
624
+ compatible = {
625
+ "input": ["output", "bidirectional", "tri_state", "passive"],
626
+ "output": ["input", "bidirectional", "tri_state", "passive"],
627
+ "bidirectional": ["input", "output", "bidirectional", "tri_state", "passive"],
628
+ "tri_state": ["input", "output", "bidirectional", "tri_state", "passive"],
629
+ "passive": ["input", "output", "bidirectional", "tri_state", "passive"],
630
+ }
631
+
632
+ return label_type in compatible.get(pin_type, [])
633
+
634
+ def _trace_hierarchical_upward(
635
+ self, signal_path: SignalPath, signal_name: str, current_path: str
636
+ ):
637
+ """Trace hierarchical label upward through sheet pins."""
638
+ # Find parent sheet that contains this path
639
+ for filename, instances in self._sheet_instances.items():
640
+ for instance in instances:
641
+ if instance.path == current_path:
642
+ # Check if parent has matching sheet pin
643
+ for pin in instance.sheet_pins:
644
+ if pin.get("name") == signal_name:
645
+ signal_path.connections.append(
646
+ f"{instance.parent_path}:sheet_pin:{pin.get('name')}"
647
+ )
648
+ signal_path.sheet_crossings += 1
649
+ signal_path.end_path = instance.parent_path
650
+
651
+ def _trace_global_connections(self, signal_path: SignalPath, signal_name: str):
652
+ """Trace global label connections across all schematics."""
653
+ for path, schematic in self._loaded_schematics.items():
654
+ labels = self._get_all_labels(schematic)
655
+ for label in labels:
656
+ if label.get("name") == signal_name and label.get("type") == "global":
657
+ signal_path.connections.append(f"{path}:global:{signal_name}")
658
+
659
+ def _get_root_schematic(self):
660
+ """Get root schematic from loaded schematics."""
661
+ return self._loaded_schematics.get("/")