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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {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("/")
|