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,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
|