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,410 @@
1
+ """
2
+ Wire Manager for KiCAD schematic wire operations.
3
+
4
+ Handles wire creation, removal, pin connections, and auto-routing functionality
5
+ while managing component pin position calculations and connectivity analysis.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ...library.cache import get_symbol_cache
13
+ from ..connectivity import ConnectivityAnalyzer, Net
14
+ from ..types import Point, Wire, WireType
15
+ from ..wires import WireCollection
16
+ from .base import BaseManager
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class WireManager(BaseManager):
22
+ """
23
+ Manages wire operations and pin connectivity in KiCAD schematics.
24
+
25
+ Responsible for:
26
+ - Wire creation and removal
27
+ - Pin position calculations
28
+ - Auto-routing between pins
29
+ - Connectivity analysis
30
+ - Wire-to-pin connections
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ schematic_data: Dict[str, Any],
36
+ wire_collection: WireCollection,
37
+ component_collection,
38
+ schematic,
39
+ ):
40
+ """
41
+ Initialize WireManager.
42
+
43
+ Args:
44
+ schematic_data: Reference to schematic data
45
+ wire_collection: Wire collection for management
46
+ component_collection: Component collection for pin lookups
47
+ schematic: Reference to parent Schematic object for connectivity analysis
48
+ """
49
+ super().__init__(schematic_data)
50
+ self._wires = wire_collection
51
+ self._components = component_collection
52
+ self._schematic = schematic
53
+ self._symbol_cache = get_symbol_cache()
54
+
55
+ # Lazy-initialized connectivity analyzer (always hierarchical)
56
+ self._connectivity_analyzer: Optional[ConnectivityAnalyzer] = None
57
+ self._connectivity_valid = False
58
+
59
+ def add_wire(
60
+ self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
61
+ ) -> str:
62
+ """
63
+ Add a wire connection.
64
+
65
+ Args:
66
+ start: Start point
67
+ end: End point
68
+
69
+ Returns:
70
+ UUID of created wire
71
+ """
72
+ if isinstance(start, tuple):
73
+ start = Point(start[0], start[1])
74
+ if isinstance(end, tuple):
75
+ end = Point(end[0], end[1])
76
+
77
+ # Use the wire collection to add the wire
78
+ wire_uuid = self._wires.add(start=start, end=end)
79
+
80
+ # Invalidate connectivity cache
81
+ self._invalidate_connectivity()
82
+
83
+ logger.debug(f"Added wire: {start} -> {end}")
84
+ return wire_uuid
85
+
86
+ def remove_wire(self, wire_uuid: str) -> bool:
87
+ """
88
+ Remove wire by UUID.
89
+
90
+ Args:
91
+ wire_uuid: UUID of wire to remove
92
+
93
+ Returns:
94
+ True if wire was removed, False if not found
95
+ """
96
+ # Remove from wire collection
97
+ removed_from_collection = self._wires.remove(wire_uuid)
98
+
99
+ # Also remove from data structure for consistency
100
+ wires = self._data.get("wires", [])
101
+ removed_from_data = False
102
+ for i, wire in enumerate(wires):
103
+ if wire.get("uuid") == wire_uuid:
104
+ del wires[i]
105
+ removed_from_data = True
106
+ break
107
+
108
+ success = removed_from_collection or removed_from_data
109
+ if success:
110
+ # Invalidate connectivity cache
111
+ self._invalidate_connectivity()
112
+ logger.debug(f"Removed wire: {wire_uuid}")
113
+
114
+ return success
115
+
116
+ def add_wire_to_pin(
117
+ self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
118
+ ) -> str:
119
+ """
120
+ Add wire from a point to a component pin.
121
+
122
+ Args:
123
+ start: Starting point
124
+ component_ref: Component reference (e.g., "R1")
125
+ pin_number: Pin number on component
126
+
127
+ Returns:
128
+ UUID of created wire
129
+
130
+ Raises:
131
+ ValueError: If component or pin not found
132
+ """
133
+ pin_position = self.get_component_pin_position(component_ref, pin_number)
134
+ if pin_position is None:
135
+ raise ValueError(f"Pin {pin_number} not found on component {component_ref}")
136
+
137
+ return self.add_wire(start, pin_position)
138
+
139
+ def add_wire_between_pins(
140
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
141
+ ) -> str:
142
+ """
143
+ Add wire between two component pins.
144
+
145
+ Args:
146
+ component1_ref: First component reference
147
+ pin1_number: First component pin number
148
+ component2_ref: Second component reference
149
+ pin2_number: Second component pin number
150
+
151
+ Returns:
152
+ UUID of created wire
153
+
154
+ Raises:
155
+ ValueError: If components or pins not found
156
+ """
157
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
158
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
159
+
160
+ if pin1_pos is None:
161
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
162
+ if pin2_pos is None:
163
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
164
+
165
+ return self.add_wire(pin1_pos, pin2_pos)
166
+
167
+ def get_component_pin_position(self, component_ref: str, pin_number: str) -> Optional[Point]:
168
+ """
169
+ Get absolute position of a component pin.
170
+
171
+ Uses the same geometry transformation as the connectivity analyzer
172
+ to ensure consistent pin positions.
173
+
174
+ Args:
175
+ component_ref: Component reference (e.g., "R1")
176
+ pin_number: Pin number
177
+
178
+ Returns:
179
+ Absolute pin position or None if not found
180
+ """
181
+ from ..pin_utils import list_component_pins
182
+
183
+ # Find component
184
+ component = self._components.get(component_ref)
185
+ if not component:
186
+ logger.warning(f"Component not found: {component_ref}")
187
+ return None
188
+
189
+ # Use pin_utils to get correct transformed positions
190
+ pins = list_component_pins(component)
191
+
192
+ for pin_num, pin_pos in pins:
193
+ if pin_num == pin_number:
194
+ return pin_pos
195
+
196
+ logger.warning(f"Pin {pin_number} not found on component {component_ref}")
197
+ return None
198
+
199
+ def list_component_pins(self, component_ref: str) -> List[Tuple[str, Point]]:
200
+ """
201
+ List all pins and their positions for a component.
202
+
203
+ Uses the same geometry transformation as the connectivity analyzer
204
+ to ensure consistent pin positions.
205
+
206
+ Args:
207
+ component_ref: Component reference
208
+
209
+ Returns:
210
+ List of (pin_number, absolute_position) tuples
211
+ """
212
+ from ..pin_utils import list_component_pins
213
+
214
+ # Find component
215
+ component = self._components.get(component_ref)
216
+ if not component:
217
+ return []
218
+
219
+ # Use pin_utils to get correct transformed positions
220
+ return list_component_pins(component)
221
+
222
+ def auto_route_pins(
223
+ self,
224
+ component1_ref: str,
225
+ pin1_number: str,
226
+ component2_ref: str,
227
+ pin2_number: str,
228
+ routing_strategy: str = "direct",
229
+ ) -> List[str]:
230
+ """
231
+ Auto-route between two pins with different strategies.
232
+
233
+ Args:
234
+ component1_ref: First component reference
235
+ pin1_number: First component pin number
236
+ component2_ref: Second component reference
237
+ pin2_number: Second component pin number
238
+ routing_strategy: "direct" or "manhattan"
239
+
240
+ Returns:
241
+ List of wire UUIDs created
242
+
243
+ Raises:
244
+ ValueError: If components or pins not found
245
+ """
246
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
247
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
248
+
249
+ if pin1_pos is None:
250
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
251
+ if pin2_pos is None:
252
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
253
+
254
+ wire_uuids = []
255
+
256
+ if routing_strategy == "direct":
257
+ # Direct wire between pins
258
+ wire_uuid = self.add_wire(pin1_pos, pin2_pos)
259
+ wire_uuids.append(wire_uuid)
260
+
261
+ elif routing_strategy == "manhattan":
262
+ # Manhattan routing (L-shaped path)
263
+ # Route horizontally first, then vertically
264
+ intermediate_point = Point(pin2_pos.x, pin1_pos.y)
265
+
266
+ # Only add intermediate wire if it has length
267
+ if abs(pin1_pos.x - pin2_pos.x) > 0.1: # Minimum wire length
268
+ wire1_uuid = self.add_wire(pin1_pos, intermediate_point)
269
+ wire_uuids.append(wire1_uuid)
270
+
271
+ if abs(pin1_pos.y - pin2_pos.y) > 0.1: # Minimum wire length
272
+ wire2_uuid = self.add_wire(intermediate_point, pin2_pos)
273
+ wire_uuids.append(wire2_uuid)
274
+
275
+ else:
276
+ raise ValueError(f"Unknown routing strategy: {routing_strategy}")
277
+
278
+ logger.info(
279
+ f"Auto-routed {component1_ref}:{pin1_number} to {component2_ref}:{pin2_number} using {routing_strategy}"
280
+ )
281
+ return wire_uuids
282
+
283
+ def are_pins_connected(
284
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
285
+ ) -> bool:
286
+ """
287
+ Check if two pins are electrically connected.
288
+
289
+ Performs full connectivity analysis including:
290
+ - Direct wire connections
291
+ - Connections through junctions
292
+ - Connections through labels (local/global/hierarchical)
293
+ - Connections through power symbols
294
+ - Hierarchical sheet connections
295
+
296
+ Args:
297
+ component1_ref: First component reference
298
+ pin1_number: First component pin number
299
+ component2_ref: Second component reference
300
+ pin2_number: Second component pin number
301
+
302
+ Returns:
303
+ True if pins are electrically connected, False otherwise
304
+ """
305
+ self._ensure_connectivity()
306
+
307
+ if self._connectivity_analyzer:
308
+ return self._connectivity_analyzer.are_connected(
309
+ component1_ref, pin1_number, component2_ref, pin2_number
310
+ )
311
+
312
+ return False
313
+
314
+ def connect_pins_with_wire(
315
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
316
+ ) -> str:
317
+ """
318
+ Legacy alias for add_wire_between_pins.
319
+
320
+ Args:
321
+ component1_ref: First component reference
322
+ pin1_number: First component pin number
323
+ component2_ref: Second component reference
324
+ pin2_number: Second component pin number
325
+
326
+ Returns:
327
+ UUID of created wire
328
+ """
329
+ return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
330
+
331
+ def get_wire_statistics(self) -> Dict[str, Any]:
332
+ """
333
+ Get statistics about wires in the schematic.
334
+
335
+ Returns:
336
+ Dictionary with wire statistics
337
+ """
338
+ total_wires = len(self._wires)
339
+ total_length = sum(wire.start.distance_to(wire.end) for wire in self._wires)
340
+
341
+ return {
342
+ "total_wires": total_wires,
343
+ "total_length": total_length,
344
+ "average_length": total_length / total_wires if total_wires > 0 else 0,
345
+ "wire_types": {
346
+ "normal": len([w for w in self._wires if w.wire_type == WireType.WIRE]),
347
+ "bus": len([w for w in self._wires if w.wire_type == WireType.BUS]),
348
+ },
349
+ }
350
+
351
+ def get_net_for_pin(self, component_ref: str, pin_number: str) -> Optional[Net]:
352
+ """
353
+ Get the electrical net connected to a specific pin.
354
+
355
+ Args:
356
+ component_ref: Component reference (e.g., "R1")
357
+ pin_number: Pin number
358
+
359
+ Returns:
360
+ Net object if pin is connected, None otherwise
361
+ """
362
+ self._ensure_connectivity()
363
+
364
+ if self._connectivity_analyzer:
365
+ return self._connectivity_analyzer.get_net_for_pin(component_ref, pin_number)
366
+
367
+ return None
368
+
369
+ def get_connected_pins(self, component_ref: str, pin_number: str) -> List[Tuple[str, str]]:
370
+ """
371
+ Get all pins electrically connected to a specific pin.
372
+
373
+ Args:
374
+ component_ref: Component reference (e.g., "R1")
375
+ pin_number: Pin number
376
+
377
+ Returns:
378
+ List of (reference, pin_number) tuples for all connected pins
379
+ """
380
+ self._ensure_connectivity()
381
+
382
+ if self._connectivity_analyzer:
383
+ return self._connectivity_analyzer.get_connected_pins(component_ref, pin_number)
384
+
385
+ return []
386
+
387
+ def _ensure_connectivity(self):
388
+ """
389
+ Ensure connectivity analysis is up-to-date.
390
+
391
+ Lazily initializes and runs connectivity analyzer on first query.
392
+ Re-runs analysis if schematic has changed since last analysis.
393
+ """
394
+ if not self._connectivity_valid:
395
+ logger.debug("Running connectivity analysis (hierarchical)...")
396
+ self._connectivity_analyzer = ConnectivityAnalyzer()
397
+ self._connectivity_analyzer.analyze(self._schematic, hierarchical=True)
398
+ self._connectivity_valid = True
399
+ logger.debug("Connectivity analysis complete")
400
+
401
+ def _invalidate_connectivity(self):
402
+ """
403
+ Invalidate connectivity cache.
404
+
405
+ Called when schematic changes that affect connectivity (wires, components, etc.).
406
+ Next connectivity query will trigger re-analysis.
407
+ """
408
+ if self._connectivity_valid:
409
+ logger.debug("Invalidating connectivity cache")
410
+ self._connectivity_valid = False
@@ -0,0 +1,305 @@
1
+ """
2
+ Net management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing electrical nets,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
10
+
11
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
12
+ from .collections import BaseCollection
13
+ from .types import Net
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NetElement:
19
+ """
20
+ Enhanced wrapper for schematic net elements with modern API.
21
+
22
+ Provides intuitive access to net properties and operations
23
+ while maintaining exact format preservation.
24
+ """
25
+
26
+ def __init__(self, net_data: Net, parent_collection: "NetCollection"):
27
+ """
28
+ Initialize net element wrapper.
29
+
30
+ Args:
31
+ net_data: Underlying net data
32
+ parent_collection: Parent collection for updates
33
+ """
34
+ self._data = net_data
35
+ self._collection = parent_collection
36
+ self._validator = SchematicValidator()
37
+
38
+ # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """Net UUID (uses name as unique identifier)."""
42
+ return self._data.name
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ """Net name."""
47
+ return self._data.name
48
+
49
+ @name.setter
50
+ def name(self, value: str):
51
+ """Set net name with validation."""
52
+ if not isinstance(value, str) or not value.strip():
53
+ raise ValidationError("Net name cannot be empty")
54
+ old_name = self._data.name
55
+ self._data.name = value.strip()
56
+ # Update name index and rebuild base UUID index since UUID changed
57
+ self._collection._update_name_index(old_name, self)
58
+ self._collection._rebuild_index()
59
+ self._collection._mark_modified()
60
+
61
+ @property
62
+ def components(self) -> List[Tuple[str, str]]:
63
+ """List of component connections (reference, pin) tuples."""
64
+ return self._data.components.copy()
65
+
66
+ @property
67
+ def wires(self) -> List[str]:
68
+ """List of wire UUIDs in this net."""
69
+ return self._data.wires.copy()
70
+
71
+ @property
72
+ def labels(self) -> List[str]:
73
+ """List of label UUIDs in this net."""
74
+ return self._data.labels.copy()
75
+
76
+ def add_connection(self, reference: str, pin: str):
77
+ """Add component pin to net."""
78
+ self._data.add_connection(reference, pin)
79
+ self._collection._mark_modified()
80
+
81
+ def remove_connection(self, reference: str, pin: str):
82
+ """Remove component pin from net."""
83
+ self._data.remove_connection(reference, pin)
84
+ self._collection._mark_modified()
85
+
86
+ def add_wire(self, wire_uuid: str):
87
+ """Add wire to net."""
88
+ if wire_uuid not in self._data.wires:
89
+ self._data.wires.append(wire_uuid)
90
+ self._collection._mark_modified()
91
+
92
+ def remove_wire(self, wire_uuid: str):
93
+ """Remove wire from net."""
94
+ if wire_uuid in self._data.wires:
95
+ self._data.wires.remove(wire_uuid)
96
+ self._collection._mark_modified()
97
+
98
+ def add_label(self, label_uuid: str):
99
+ """Add label to net."""
100
+ if label_uuid not in self._data.labels:
101
+ self._data.labels.append(label_uuid)
102
+ self._collection._mark_modified()
103
+
104
+ def remove_label(self, label_uuid: str):
105
+ """Remove label from net."""
106
+ if label_uuid in self._data.labels:
107
+ self._data.labels.remove(label_uuid)
108
+ self._collection._mark_modified()
109
+
110
+ def validate(self) -> List[ValidationIssue]:
111
+ """Validate this net element."""
112
+ return self._validator.validate_net(self._data.__dict__)
113
+
114
+ def to_dict(self) -> Dict[str, Any]:
115
+ """Convert net element to dictionary representation."""
116
+ return {
117
+ "name": self.name,
118
+ "components": self.components,
119
+ "wires": self.wires,
120
+ "labels": self.labels,
121
+ }
122
+
123
+ def __str__(self) -> str:
124
+ """String representation."""
125
+ return f"<Net '{self.name}' ({len(self.components)} connections)>"
126
+
127
+
128
+ class NetCollection(BaseCollection[NetElement]):
129
+ """
130
+ Collection class for efficient net management.
131
+
132
+ Inherits from BaseCollection for standard operations and adds net-specific
133
+ functionality including name-based indexing.
134
+
135
+ Provides fast lookup, filtering, and bulk operations for schematic nets.
136
+ Note: Nets use name as the unique identifier (exposed as .uuid for protocol compatibility).
137
+ """
138
+
139
+ def __init__(self, nets: List[Net] = None):
140
+ """
141
+ Initialize net collection.
142
+
143
+ Args:
144
+ nets: Initial list of net data
145
+ """
146
+ # Initialize base collection
147
+ super().__init__([], collection_name="nets")
148
+
149
+ # Additional net-specific index (for convenience, duplicates base UUID index)
150
+ self._name_index: Dict[str, NetElement] = {}
151
+
152
+ # Add initial nets
153
+ if nets:
154
+ for net_data in nets:
155
+ self._add_to_indexes(NetElement(net_data, self))
156
+
157
+ def add(
158
+ self,
159
+ name: str,
160
+ components: List[Tuple[str, str]] = None,
161
+ wires: List[str] = None,
162
+ labels: List[str] = None,
163
+ ) -> NetElement:
164
+ """
165
+ Add a new net to the schematic.
166
+
167
+ Args:
168
+ name: Net name
169
+ components: Initial component connections
170
+ wires: Initial wire UUIDs
171
+ labels: Initial label UUIDs
172
+
173
+ Returns:
174
+ Newly created NetElement
175
+
176
+ Raises:
177
+ ValidationError: If net data is invalid
178
+ """
179
+ # Validate inputs
180
+ if not isinstance(name, str) or not name.strip():
181
+ raise ValidationError("Net name cannot be empty")
182
+
183
+ name = name.strip()
184
+
185
+ # Check for duplicate name
186
+ if name in self._name_index:
187
+ raise ValidationError(f"Net name {name} already exists")
188
+
189
+ # Create net data
190
+ net_data = Net(
191
+ name=name,
192
+ components=components or [],
193
+ wires=wires or [],
194
+ labels=labels or [],
195
+ )
196
+
197
+ # Create wrapper and add to collection
198
+ net_element = NetElement(net_data, self)
199
+ self._add_to_indexes(net_element)
200
+ self._mark_modified()
201
+
202
+ logger.debug(f"Added net: {net_element}")
203
+ return net_element
204
+
205
+ def get_by_name(self, name: str) -> Optional[NetElement]:
206
+ """Get net by name (convenience method, equivalent to get(name))."""
207
+ return self.get(name)
208
+
209
+ # get() method inherited from BaseCollection (uses name as UUID)
210
+
211
+ def remove(self, name: str) -> bool:
212
+ """
213
+ Remove net by name.
214
+
215
+ Args:
216
+ name: Name of net to remove
217
+
218
+ Returns:
219
+ True if net was removed, False if not found
220
+ """
221
+ net_element = self.get(name)
222
+ if not net_element:
223
+ return False
224
+
225
+ # Remove from name index
226
+ if net_element.name in self._name_index:
227
+ del self._name_index[net_element.name]
228
+
229
+ # Remove using base class method
230
+ super().remove(name)
231
+
232
+ logger.debug(f"Removed net: {net_element}")
233
+ return True
234
+
235
+ def find_by_component(self, reference: str, pin: Optional[str] = None) -> List[NetElement]:
236
+ """
237
+ Find nets connected to a component.
238
+
239
+ Args:
240
+ reference: Component reference
241
+ pin: Specific pin (if None, returns all nets for component)
242
+
243
+ Returns:
244
+ List of matching net elements
245
+ """
246
+ matches = []
247
+ for net_element in self._items:
248
+ for comp_ref, comp_pin in net_element.components:
249
+ if comp_ref == reference and (pin is None or comp_pin == pin):
250
+ matches.append(net_element)
251
+ break
252
+ return matches
253
+
254
+ def filter(self, predicate: Callable[[NetElement], bool]) -> List[NetElement]:
255
+ """
256
+ Filter nets by predicate function (delegates to base class find).
257
+
258
+ Args:
259
+ predicate: Function that returns True for nets to include
260
+
261
+ Returns:
262
+ List of nets matching predicate
263
+ """
264
+ return self.find(predicate)
265
+
266
+ def bulk_update(self, criteria: Callable[[NetElement], bool], updates: Dict[str, Any]):
267
+ """
268
+ Update multiple nets matching criteria.
269
+
270
+ Args:
271
+ criteria: Function to select nets to update
272
+ updates: Dictionary of property updates
273
+ """
274
+ updated_count = 0
275
+ for net_element in self._items:
276
+ if criteria(net_element):
277
+ for prop, value in updates.items():
278
+ if hasattr(net_element, prop):
279
+ setattr(net_element, prop, value)
280
+ updated_count += 1
281
+
282
+ if updated_count > 0:
283
+ self._mark_modified()
284
+ logger.debug(f"Bulk updated {updated_count} net properties")
285
+
286
+ def clear(self):
287
+ """Remove all nets from collection."""
288
+ self._name_index.clear()
289
+ super().clear()
290
+
291
+ def _add_to_indexes(self, net_element: NetElement):
292
+ """Add net to internal indexes (base + name index)."""
293
+ self._add_item(net_element)
294
+ self._name_index[net_element.name] = net_element
295
+
296
+ def _update_name_index(self, old_name: str, net_element: NetElement):
297
+ """Update name index when net name changes."""
298
+ if old_name in self._name_index:
299
+ del self._name_index[old_name]
300
+ self._name_index[net_element.name] = net_element
301
+
302
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
303
+ def __bool__(self) -> bool:
304
+ """Return True if collection has nets."""
305
+ return len(self._items) > 0