kicad-sch-api 0.4.1__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 (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,692 @@
1
+ """
2
+ Network connectivity analysis for KiCAD schematics.
3
+
4
+ Implements comprehensive net tracing through wires, junctions, labels,
5
+ hierarchical connections, and power symbols.
6
+ """
7
+
8
+ import logging
9
+ from collections import defaultdict
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, List, Optional, Set, Tuple
12
+
13
+ from .geometry import points_equal
14
+ from .types import Point, SchematicSymbol, Wire, Junction, Label, LabelType
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class PinConnection:
21
+ """Represents a component pin in the connectivity graph."""
22
+
23
+ reference: str # Component reference (e.g., "R1")
24
+ pin_number: str # Pin number (e.g., "2")
25
+ position: Point # Absolute position of pin
26
+
27
+ def __hash__(self):
28
+ return hash((self.reference, self.pin_number))
29
+
30
+ def __eq__(self, other):
31
+ return (self.reference == other.reference and
32
+ self.pin_number == other.pin_number)
33
+
34
+ def __repr__(self):
35
+ return f"{self.reference}.{self.pin_number}@({self.position.x:.2f},{self.position.y:.2f})"
36
+
37
+
38
+ @dataclass
39
+ class Net:
40
+ """Represents an electrical net (connected set of pins/wires/labels)."""
41
+
42
+ name: Optional[str] = None # Net name (from label, or auto-generated)
43
+ pins: Set[PinConnection] = field(default_factory=set) # Connected pins
44
+ wires: Set[str] = field(default_factory=set) # Wire UUIDs
45
+ junctions: Set[str] = field(default_factory=set) # Junction UUIDs
46
+ labels: Set[str] = field(default_factory=set) # Label UUIDs
47
+ points: Set[Tuple[float, float]] = field(default_factory=set) # All connection points
48
+
49
+ def add_pin(self, pin: PinConnection):
50
+ """Add a pin to this net."""
51
+ self.pins.add(pin)
52
+ self.points.add((pin.position.x, pin.position.y))
53
+
54
+ def merge(self, other: "Net"):
55
+ """Merge another net into this one."""
56
+ self.pins.update(other.pins)
57
+ self.wires.update(other.wires)
58
+ self.junctions.update(other.junctions)
59
+ self.labels.update(other.labels)
60
+ self.points.update(other.points)
61
+
62
+ # Prefer named nets over unnamed
63
+ if other.name and not self.name:
64
+ self.name = other.name
65
+
66
+ def __repr__(self):
67
+ name_str = f"'{self.name}'" if self.name else "unnamed"
68
+ return f"Net({name_str}, {len(self.pins)} pins, {len(self.wires)} wires)"
69
+
70
+
71
+ class ConnectivityAnalyzer:
72
+ """
73
+ Analyzes schematic connectivity and builds electrical nets.
74
+
75
+ Traces connections through:
76
+ - Direct wire-to-pin connections
77
+ - Junction points connecting multiple wires
78
+ - Labels connecting separated wire segments
79
+ - Global labels (cross-schematic connections)
80
+ - Hierarchical labels (parent-child sheet connections)
81
+ - Power symbols (implicit global connections)
82
+ """
83
+
84
+ def __init__(self, tolerance: float = 0.01):
85
+ """
86
+ Initialize connectivity analyzer.
87
+
88
+ Args:
89
+ tolerance: Position matching tolerance in mm (default: 0.01)
90
+ """
91
+ self.tolerance = tolerance
92
+ self.nets: List[Net] = []
93
+ self._point_to_net: Dict[Tuple[float, float], Net] = {}
94
+ self._pin_to_net: Dict[PinConnection, Net] = {}
95
+ self._label_name_to_nets: Dict[str, List[Net]] = defaultdict(list)
96
+
97
+ logger.info(f"Initialized ConnectivityAnalyzer (tolerance={tolerance}mm)")
98
+
99
+ def analyze(self, schematic, hierarchical=True) -> List[Net]:
100
+ """
101
+ Analyze schematic connectivity and return all nets.
102
+
103
+ Args:
104
+ schematic: Schematic object to analyze (root schematic)
105
+ hierarchical: If True, also analyze child sheets (default: True)
106
+
107
+ Returns:
108
+ List of Net objects representing all electrical connections
109
+ """
110
+ logger.info("Starting connectivity analysis...")
111
+
112
+ # Collect all schematics (parent + children if hierarchical)
113
+ if hierarchical:
114
+ schematics = self._load_hierarchical_schematics(schematic)
115
+ logger.info(f"Analyzing {len(schematics)} schematics (hierarchical)")
116
+ else:
117
+ schematics = [schematic]
118
+
119
+ # Step 1: Build component pin positions from all schematics
120
+ all_pin_positions = {}
121
+ for sch in schematics:
122
+ pin_positions = self._build_pin_positions(sch)
123
+ all_pin_positions.update(pin_positions)
124
+ logger.info(f"Found {len(all_pin_positions)} component pins across all sheets")
125
+
126
+ # Step 2: Create initial nets from wire-to-pin connections
127
+ for sch in schematics:
128
+ sch_pins = {pc: pos for pc, pos in all_pin_positions.items()
129
+ if any(c.reference == pc.reference for c in sch.components)}
130
+ self._trace_wire_connections(sch, sch_pins)
131
+ logger.info(f"Created {len(self.nets)} nets from wire connections")
132
+
133
+ # Step 3: Merge nets connected by junctions
134
+ for sch in schematics:
135
+ self._merge_junction_nets(sch)
136
+ logger.info(f"After junction merging: {len(self.nets)} nets")
137
+
138
+ # Step 4: Merge nets connected by local labels
139
+ for sch in schematics:
140
+ self._merge_label_nets(sch)
141
+ logger.info(f"After label merging: {len(self.nets)} nets")
142
+
143
+ # Step 5: Process hierarchical connections (sheet pins ↔ hierarchical labels)
144
+ if hierarchical and len(schematics) > 1:
145
+ self._process_hierarchical_connections(schematic, schematics)
146
+ logger.info(f"After hierarchical connections: {len(self.nets)} nets")
147
+
148
+ # Step 6: Process power symbols (implicit global connections across ALL sheets)
149
+ for sch in schematics:
150
+ self._process_power_symbols(sch)
151
+ logger.info(f"After power symbols: {len(self.nets)} nets")
152
+
153
+ # Step 7: Handle global labels
154
+ for sch in schematics:
155
+ self._process_global_labels(sch)
156
+ logger.info(f"After global labels: {len(self.nets)} nets")
157
+
158
+ # Step 8: Auto-generate net names for unnamed nets
159
+ self._generate_net_names()
160
+
161
+ logger.info(f"Connectivity analysis complete: {len(self.nets)} nets")
162
+ return self.nets
163
+
164
+ def _build_pin_positions(self, schematic) -> Dict[PinConnection, Point]:
165
+ """
166
+ Build mapping of all component pins to their absolute positions.
167
+
168
+ Args:
169
+ schematic: Schematic to analyze
170
+
171
+ Returns:
172
+ Dict mapping PinConnection to absolute Point
173
+ """
174
+ from .pin_utils import list_component_pins
175
+
176
+ pin_positions = {}
177
+
178
+ for component in schematic.components:
179
+ # Get all pins for this component
180
+ pins = list_component_pins(component)
181
+
182
+ for pin_number, pin_position in pins:
183
+ if pin_position is not None:
184
+ pin_conn = PinConnection(
185
+ reference=component.reference,
186
+ pin_number=pin_number,
187
+ position=pin_position
188
+ )
189
+ pin_positions[pin_conn] = pin_position
190
+ logger.debug(f" {pin_conn}")
191
+
192
+ return pin_positions
193
+
194
+ def _trace_wire_connections(self, schematic, pin_positions: Dict[PinConnection, Point]):
195
+ """
196
+ Create initial nets by tracing wire-to-pin connections.
197
+
198
+ Args:
199
+ schematic: Schematic to analyze
200
+ pin_positions: Mapping of pins to positions
201
+ """
202
+ for wire in schematic.wires:
203
+ # Get wire endpoints
204
+ wire_points = wire.points
205
+ if len(wire_points) < 2:
206
+ logger.warning(f"Wire {wire.uuid} has < 2 points, skipping")
207
+ continue
208
+
209
+ # Find which pins connect to this wire
210
+ connected_pins = set()
211
+
212
+ for pin_conn, pin_pos in pin_positions.items():
213
+ # Check if pin connects to any point on the wire
214
+ for wire_point in wire_points:
215
+ if points_equal(wire_point, pin_pos, self.tolerance):
216
+ connected_pins.add(pin_conn)
217
+ logger.debug(f" Wire {wire.uuid} connects to {pin_conn}")
218
+ break
219
+
220
+ # Create or update net for this wire
221
+ # Always create a net for the wire, even if no pins connect yet
222
+ # (labels, junctions, or hierarchical connections may merge it later)
223
+ if connected_pins:
224
+ self._add_wire_to_net(wire, connected_pins, wire_points)
225
+ else:
226
+ # Create net for wire without pins (will be merged via labels/junctions)
227
+ net = Net()
228
+ net.wires.add(wire.uuid)
229
+ for point in wire_points:
230
+ net.points.add((point.x, point.y))
231
+ self._point_to_net[(point.x, point.y)] = net
232
+ self.nets.append(net)
233
+ logger.debug(f" Created net for wire {wire.uuid} without pins")
234
+
235
+ def _add_wire_to_net(self, wire: Wire, pins: Set[PinConnection], wire_points: List[Point]):
236
+ """
237
+ Add wire and its connected pins to a net (create new or merge existing).
238
+
239
+ Args:
240
+ wire: Wire object
241
+ pins: Set of pins connected to this wire
242
+ wire_points: Points along the wire
243
+ """
244
+ # Check if any of these pins are already in a net
245
+ existing_nets = set()
246
+ for pin in pins:
247
+ if pin in self._pin_to_net:
248
+ existing_nets.add(self._pin_to_net[pin])
249
+
250
+ if existing_nets:
251
+ # Merge all existing nets into the first one
252
+ primary_net = existing_nets.pop()
253
+ for other_net in existing_nets:
254
+ primary_net.merge(other_net)
255
+ self.nets.remove(other_net)
256
+
257
+ # Add new pins and wire to primary net
258
+ for pin in pins:
259
+ primary_net.add_pin(pin)
260
+ self._pin_to_net[pin] = primary_net
261
+
262
+ primary_net.wires.add(wire.uuid)
263
+ for point in wire_points:
264
+ primary_net.points.add((point.x, point.y))
265
+ self._point_to_net[(point.x, point.y)] = primary_net
266
+ else:
267
+ # Create new net
268
+ net = Net()
269
+ for pin in pins:
270
+ net.add_pin(pin)
271
+ self._pin_to_net[pin] = net
272
+
273
+ net.wires.add(wire.uuid)
274
+ for point in wire_points:
275
+ net.points.add((point.x, point.y))
276
+ self._point_to_net[(point.x, point.y)] = net
277
+
278
+ self.nets.append(net)
279
+ logger.debug(f" Created new {net}")
280
+
281
+ def _merge_junction_nets(self, schematic):
282
+ """
283
+ Merge nets that are connected by junction points.
284
+
285
+ Also includes wires at the junction that don't connect to pins
286
+ (e.g., test point taps, voltage monitoring points).
287
+
288
+ Args:
289
+ schematic: Schematic to analyze
290
+ """
291
+ for junction in schematic.junctions:
292
+ junc_pos = junction.position
293
+
294
+ # Find all nets that have points at this junction position
295
+ nets_at_junction = []
296
+
297
+ for net in self.nets:
298
+ for point in net.points:
299
+ if points_equal(Point(point[0], point[1]), junc_pos, self.tolerance):
300
+ if net not in nets_at_junction: # Avoid duplicates
301
+ nets_at_junction.append(net)
302
+ break
303
+
304
+ # Also find wires at this junction that aren't in any net yet
305
+ # (e.g., tap wires that don't connect to component pins)
306
+ unconnected_wires = []
307
+ for wire in schematic.wires:
308
+ # Check if wire has a point at junction
309
+ wire_at_junction = False
310
+ for wire_point in wire.points:
311
+ if points_equal(wire_point, junc_pos, self.tolerance):
312
+ wire_at_junction = True
313
+ break
314
+
315
+ if wire_at_junction:
316
+ # Check if this wire is already in a net
317
+ wire_in_net = False
318
+ for net in nets_at_junction:
319
+ if wire.uuid in net.wires:
320
+ wire_in_net = True
321
+ break
322
+
323
+ if not wire_in_net:
324
+ unconnected_wires.append(wire)
325
+
326
+ # If we have nets at junction, merge them and add unconnected wires
327
+ if len(nets_at_junction) >= 1:
328
+ primary_net = nets_at_junction[0]
329
+
330
+ # Merge other nets
331
+ for other_net in nets_at_junction[1:]:
332
+ primary_net.merge(other_net)
333
+
334
+ # Update all pin mappings
335
+ for pin in other_net.pins:
336
+ self._pin_to_net[pin] = primary_net
337
+
338
+ # Update all point mappings
339
+ for point in other_net.points:
340
+ self._point_to_net[point] = primary_net
341
+
342
+ self.nets.remove(other_net)
343
+
344
+ # Add unconnected wires to the primary net
345
+ for wire in unconnected_wires:
346
+ primary_net.wires.add(wire.uuid)
347
+ for point in wire.points:
348
+ primary_net.points.add((point.x, point.y))
349
+ self._point_to_net[(point.x, point.y)] = primary_net
350
+ logger.debug(f"Added unconnected wire {wire.uuid[:8]} to net at junction")
351
+
352
+ primary_net.junctions.add(junction.uuid)
353
+
354
+ def _merge_label_nets(self, schematic):
355
+ """
356
+ Merge nets that are connected by labels with the same name.
357
+
358
+ Args:
359
+ schematic: Schematic to analyze
360
+ """
361
+ # Process local labels only (global labels handled separately)
362
+ local_labels = [label for label in schematic.labels
363
+ if hasattr(label, '_data') and label._data.label_type == LabelType.LOCAL]
364
+
365
+ for label in local_labels:
366
+ label_pos = label.position
367
+
368
+ # Find which net this label is on
369
+ net_for_label = None
370
+ for net in self.nets:
371
+ for point in net.points:
372
+ if points_equal(Point(point[0], point[1]), label_pos, self.tolerance):
373
+ net_for_label = net
374
+ break
375
+ if net_for_label:
376
+ break
377
+
378
+ if net_for_label:
379
+ # Set or merge net name
380
+ if not net_for_label.name:
381
+ net_for_label.name = label.text
382
+ logger.debug(f"Named {net_for_label} from label")
383
+
384
+ net_for_label.labels.add(label.uuid)
385
+ self._label_name_to_nets[label.text].append(net_for_label)
386
+
387
+ # Merge nets with the same label name
388
+ for label_name, nets_with_label in self._label_name_to_nets.items():
389
+ if len(nets_with_label) > 1:
390
+ logger.debug(f"Label '{label_name}' connects {len(nets_with_label)} nets")
391
+
392
+ primary_net = nets_with_label[0]
393
+ for other_net in nets_with_label[1:]:
394
+ if other_net in self.nets: # Check if not already merged
395
+ primary_net.merge(other_net)
396
+
397
+ # Update mappings
398
+ for pin in other_net.pins:
399
+ self._pin_to_net[pin] = primary_net
400
+ for point in other_net.points:
401
+ self._point_to_net[point] = primary_net
402
+
403
+ self.nets.remove(other_net)
404
+
405
+ def _process_power_symbols(self, schematic):
406
+ """
407
+ Process power symbols and create implicit global connections.
408
+
409
+ Power symbols (like GND, VCC, +5V) create implicit global nets.
410
+ All power symbols with the same value are electrically connected,
411
+ even if they're not physically wired together.
412
+
413
+ Args:
414
+ schematic: Schematic to analyze
415
+ """
416
+ # Group power symbols by their value property
417
+ power_symbol_nets_by_value = defaultdict(list)
418
+
419
+ for component in schematic.components:
420
+ # Identify power symbols by lib_id pattern
421
+ if component.lib_id.startswith('power:'):
422
+ power_value = component.value
423
+
424
+ logger.debug(f"Found power symbol: {component.reference} (value={power_value})")
425
+
426
+ # Find the net this power symbol is connected to
427
+ # Power symbols have a single pin (usually pin "1")
428
+ power_pin_conn = None
429
+ for pin_conn in self._pin_to_net.keys():
430
+ if pin_conn.reference == component.reference:
431
+ power_pin_conn = pin_conn
432
+ break
433
+
434
+ if power_pin_conn:
435
+ net = self._pin_to_net[power_pin_conn]
436
+ power_symbol_nets_by_value[power_value].append(net)
437
+ logger.debug(f" Power symbol {component.reference} on net '{net.name}'")
438
+
439
+ # Merge all nets with the same power symbol value
440
+ for power_value, nets_to_merge in power_symbol_nets_by_value.items():
441
+ if len(nets_to_merge) > 1:
442
+ logger.debug(f"Merging {len(nets_to_merge)} nets for power symbol '{power_value}'")
443
+
444
+ # Merge all nets into the first one
445
+ primary_net = nets_to_merge[0]
446
+
447
+ # Set the net name from power symbol value
448
+ primary_net.name = power_value
449
+
450
+ for other_net in nets_to_merge[1:]:
451
+ if other_net in self.nets: # Check if not already merged
452
+ primary_net.merge(other_net)
453
+
454
+ # Update mappings
455
+ for pin in other_net.pins:
456
+ self._pin_to_net[pin] = primary_net
457
+ for point in other_net.points:
458
+ self._point_to_net[point] = primary_net
459
+
460
+ self.nets.remove(other_net)
461
+ elif len(nets_to_merge) == 1:
462
+ # Single power symbol - just name the net
463
+ nets_to_merge[0].name = power_value
464
+
465
+ def _process_global_labels(self, schematic):
466
+ """
467
+ Process global labels (cross-schematic connections).
468
+
469
+ Args:
470
+ schematic: Schematic to analyze
471
+ """
472
+ # TODO: Implement global label handling
473
+ # Global labels with same name should connect across schematics
474
+ pass
475
+
476
+ def _generate_net_names(self):
477
+ """Generate names for nets that don't have explicit names."""
478
+ unnamed_counter = 1
479
+
480
+ for net in self.nets:
481
+ if not net.name:
482
+ # Try to name from connected component pins
483
+ if net.pins:
484
+ first_pin = next(iter(net.pins))
485
+ net.name = f"Net-({first_pin.reference}-Pad{first_pin.pin_number})"
486
+ else:
487
+ net.name = f"Net-(unnamed-{unnamed_counter})"
488
+ unnamed_counter += 1
489
+
490
+ def are_connected(self, ref1: str, pin1: str, ref2: str, pin2: str) -> bool:
491
+ """
492
+ Check if two pins are electrically connected.
493
+
494
+ Args:
495
+ ref1: First component reference
496
+ pin1: First pin number
497
+ ref2: Second component reference
498
+ pin2: Second pin number
499
+
500
+ Returns:
501
+ True if pins are on the same net, False otherwise
502
+ """
503
+ # Find pins in the connectivity graph
504
+ pin_conn1 = None
505
+ pin_conn2 = None
506
+
507
+ for pin in self._pin_to_net.keys():
508
+ if pin.reference == ref1 and pin.pin_number == pin1:
509
+ pin_conn1 = pin
510
+ if pin.reference == ref2 and pin.pin_number == pin2:
511
+ pin_conn2 = pin
512
+
513
+ if not pin_conn1 or not pin_conn2:
514
+ return False
515
+
516
+ # Check if both pins are in the same net
517
+ net1 = self._pin_to_net.get(pin_conn1)
518
+ net2 = self._pin_to_net.get(pin_conn2)
519
+
520
+ return net1 is not None and net1 is net2
521
+
522
+ def _load_hierarchical_schematics(self, root_schematic):
523
+ """
524
+ Load root schematic and all child schematics.
525
+
526
+ Args:
527
+ root_schematic: Root schematic object
528
+
529
+ Returns:
530
+ List of all schematics (root + children)
531
+ """
532
+ from pathlib import Path
533
+
534
+ schematics = [root_schematic]
535
+
536
+ # Check if root schematic has hierarchical sheets
537
+ if not hasattr(root_schematic, '_data') or 'sheets' not in root_schematic._data:
538
+ return schematics
539
+
540
+ sheets = root_schematic._data.get('sheets', [])
541
+
542
+ # Load each child schematic
543
+ root_path = Path(root_schematic.file_path) if root_schematic.file_path else None
544
+
545
+ for sheet in sheets:
546
+ sheet_filename = sheet.get('filename')
547
+ if not sheet_filename:
548
+ continue
549
+
550
+ # Build path to child schematic
551
+ if root_path:
552
+ child_path = root_path.parent / sheet_filename
553
+ else:
554
+ child_path = Path(sheet_filename)
555
+
556
+ if child_path.exists():
557
+ try:
558
+ # Import Schematic class - use absolute import to avoid circular dependency
559
+ import kicad_sch_api as ksa
560
+ child_sch = ksa.Schematic.load(str(child_path))
561
+ schematics.append(child_sch)
562
+ logger.info(f"Loaded child schematic: {sheet_filename}")
563
+ except Exception as e:
564
+ logger.warning(f"Could not load child schematic {sheet_filename}: {e}")
565
+ else:
566
+ logger.warning(f"Child schematic not found: {child_path}")
567
+
568
+ return schematics
569
+
570
+ def _process_hierarchical_connections(self, root_schematic, all_schematics):
571
+ """
572
+ Process hierarchical connections between parent and child sheets.
573
+
574
+ Connects sheet pins in parent to hierarchical labels in child sheets.
575
+
576
+ Args:
577
+ root_schematic: Root schematic with hierarchical sheets
578
+ all_schematics: List of all schematics (root + children)
579
+ """
580
+ from pathlib import Path
581
+
582
+ if not hasattr(root_schematic, '_data') or 'sheets' not in root_schematic._data:
583
+ return
584
+
585
+ sheets = root_schematic._data.get('sheets', [])
586
+
587
+ for sheet_data in sheets:
588
+ sheet_filename = sheet_data.get('filename')
589
+ sheet_pins = sheet_data.get('pins', [])
590
+
591
+ if not sheet_filename or not sheet_pins:
592
+ continue
593
+
594
+ # Find the child schematic
595
+ child_sch = None
596
+ for sch in all_schematics:
597
+ if sch.file_path and Path(sch.file_path).name == sheet_filename:
598
+ child_sch = sch
599
+ break
600
+
601
+ if not child_sch:
602
+ logger.warning(f"Child schematic not found for sheet: {sheet_filename}")
603
+ continue
604
+
605
+ # For each sheet pin, find matching hierarchical label in child
606
+ for pin_data in sheet_pins:
607
+ pin_name = pin_data.get('name')
608
+ pin_position = pin_data.get('position')
609
+
610
+ if not pin_name or not pin_position:
611
+ continue
612
+
613
+ pin_pos = Point(pin_position['x'], pin_position['y'])
614
+
615
+ # Find net at sheet pin position in parent
616
+ parent_net = None
617
+ for net in self.nets:
618
+ for point in net.points:
619
+ if points_equal(Point(point[0], point[1]), pin_pos, self.tolerance):
620
+ parent_net = net
621
+ break
622
+ if parent_net:
623
+ break
624
+
625
+ if not parent_net:
626
+ logger.debug(f"No net found at sheet pin '{pin_name}' position")
627
+ continue
628
+
629
+ # Find matching hierarchical label in child schematic
630
+ for hier_label in child_sch.hierarchical_labels:
631
+ if hier_label.text == pin_name:
632
+ label_pos = hier_label.position
633
+
634
+ # Find net at hierarchical label position in child
635
+ child_net = None
636
+ for net in self.nets:
637
+ for point in net.points:
638
+ if points_equal(Point(point[0], point[1]), label_pos, self.tolerance):
639
+ child_net = net
640
+ break
641
+ if child_net:
642
+ break
643
+
644
+ if child_net and child_net is not parent_net:
645
+ # Merge child net into parent net
646
+ logger.debug(f"Merging nets via hierarchical connection '{pin_name}'")
647
+ parent_net.merge(child_net)
648
+
649
+ # Update mappings
650
+ for pin in child_net.pins:
651
+ self._pin_to_net[pin] = parent_net
652
+ for point in child_net.points:
653
+ self._point_to_net[point] = parent_net
654
+
655
+ self.nets.remove(child_net)
656
+
657
+ break
658
+
659
+ def get_net_for_pin(self, reference: str, pin_number: str) -> Optional[Net]:
660
+ """
661
+ Get the net connected to a specific pin.
662
+
663
+ Args:
664
+ reference: Component reference
665
+ pin_number: Pin number
666
+
667
+ Returns:
668
+ Net object if pin is connected, None otherwise
669
+ """
670
+ for pin in self._pin_to_net.keys():
671
+ if pin.reference == reference and pin.pin_number == pin_number:
672
+ return self._pin_to_net[pin]
673
+
674
+ return None
675
+
676
+ def get_connected_pins(self, reference: str, pin_number: str) -> List[Tuple[str, str]]:
677
+ """
678
+ Get all pins connected to a specific pin.
679
+
680
+ Args:
681
+ reference: Component reference
682
+ pin_number: Pin number
683
+
684
+ Returns:
685
+ List of (reference, pin_number) tuples for connected pins
686
+ """
687
+ net = self.get_net_for_pin(reference, pin_number)
688
+ if not net:
689
+ return []
690
+
691
+ return [(pin.reference, pin.pin_number) for pin in net.pins
692
+ if not (pin.reference == reference and pin.pin_number == pin_number)]