kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.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 (81) hide show
  1. kicad_sch_api/__init__.py +2 -2
  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/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/collections/__init__.py +2 -2
  10. kicad_sch_api/collections/base.py +5 -7
  11. kicad_sch_api/collections/components.py +24 -12
  12. kicad_sch_api/collections/junctions.py +31 -43
  13. kicad_sch_api/collections/labels.py +19 -27
  14. kicad_sch_api/collections/wires.py +17 -18
  15. kicad_sch_api/core/collections/__init__.py +5 -0
  16. kicad_sch_api/core/collections/base.py +248 -0
  17. kicad_sch_api/core/component_bounds.py +5 -0
  18. kicad_sch_api/core/components.py +67 -45
  19. kicad_sch_api/core/config.py +85 -3
  20. kicad_sch_api/core/factories/__init__.py +5 -0
  21. kicad_sch_api/core/factories/element_factory.py +276 -0
  22. kicad_sch_api/core/formatter.py +3 -1
  23. kicad_sch_api/core/junctions.py +26 -75
  24. kicad_sch_api/core/labels.py +29 -53
  25. kicad_sch_api/core/managers/__init__.py +26 -0
  26. kicad_sch_api/core/managers/file_io.py +244 -0
  27. kicad_sch_api/core/managers/format_sync.py +501 -0
  28. kicad_sch_api/core/managers/graphics.py +579 -0
  29. kicad_sch_api/core/managers/metadata.py +269 -0
  30. kicad_sch_api/core/managers/sheet.py +454 -0
  31. kicad_sch_api/core/managers/text_elements.py +536 -0
  32. kicad_sch_api/core/managers/validation.py +475 -0
  33. kicad_sch_api/core/managers/wire.py +352 -0
  34. kicad_sch_api/core/nets.py +38 -43
  35. kicad_sch_api/core/no_connects.py +33 -55
  36. kicad_sch_api/core/parser.py +75 -1731
  37. kicad_sch_api/core/schematic.py +951 -1192
  38. kicad_sch_api/core/texts.py +28 -55
  39. kicad_sch_api/core/types.py +60 -22
  40. kicad_sch_api/core/wires.py +27 -75
  41. kicad_sch_api/geometry/font_metrics.py +3 -1
  42. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  43. kicad_sch_api/interfaces/__init__.py +1 -1
  44. kicad_sch_api/interfaces/parser.py +1 -1
  45. kicad_sch_api/interfaces/repository.py +1 -1
  46. kicad_sch_api/interfaces/resolver.py +1 -1
  47. kicad_sch_api/parsers/__init__.py +2 -2
  48. kicad_sch_api/parsers/base.py +7 -10
  49. kicad_sch_api/parsers/elements/__init__.py +22 -0
  50. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  51. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  52. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  53. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  54. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  55. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  56. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  57. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  58. kicad_sch_api/parsers/registry.py +4 -2
  59. kicad_sch_api/parsers/utils.py +80 -0
  60. kicad_sch_api/symbols/__init__.py +1 -1
  61. kicad_sch_api/symbols/cache.py +9 -12
  62. kicad_sch_api/symbols/resolver.py +20 -26
  63. kicad_sch_api/symbols/validators.py +188 -137
  64. kicad_sch_api/validation/__init__.py +25 -0
  65. kicad_sch_api/validation/erc.py +171 -0
  66. kicad_sch_api/validation/erc_models.py +203 -0
  67. kicad_sch_api/validation/pin_matrix.py +243 -0
  68. kicad_sch_api/validation/validators.py +391 -0
  69. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  70. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  71. kicad_sch_api/core/manhattan_routing.py +0 -430
  72. kicad_sch_api/core/simple_manhattan.py +0 -228
  73. kicad_sch_api/core/wire_routing.py +0 -380
  74. kicad_sch_api/parsers/label_parser.py +0 -254
  75. kicad_sch_api/parsers/symbol_parser.py +0 -227
  76. kicad_sch_api/parsers/wire_parser.py +0 -99
  77. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  78. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  79. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  80. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  81. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,475 @@
1
+ """
2
+ Validation Manager for KiCAD schematic integrity checking.
3
+
4
+ Provides comprehensive validation for schematic integrity, design rules,
5
+ connectivity analysis, and format compliance while collecting and reporting
6
+ validation issues systematically.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any, Dict, List, Optional, Set, Tuple
11
+
12
+ from ...utils.validation import ValidationError, ValidationIssue
13
+ from ..types import Point
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ValidationManager:
19
+ """
20
+ Comprehensive validation manager for schematic integrity.
21
+
22
+ Responsible for:
23
+ - Schematic-wide integrity checks
24
+ - Component reference validation
25
+ - Connectivity analysis
26
+ - Design rule checking
27
+ - Format compliance validation
28
+ - Validation issue collection and reporting
29
+ """
30
+
31
+ def __init__(
32
+ self, schematic_data: Dict[str, Any], component_collection=None, wire_collection=None
33
+ ):
34
+ """
35
+ Initialize ValidationManager.
36
+
37
+ Args:
38
+ schematic_data: Reference to schematic data
39
+ component_collection: Component collection for validation
40
+ wire_collection: Wire collection for connectivity analysis
41
+ """
42
+ self._data = schematic_data
43
+ self._components = component_collection
44
+ self._wires = wire_collection
45
+ self._validation_rules = self._initialize_validation_rules()
46
+
47
+ def validate_schematic(self) -> List[ValidationIssue]:
48
+ """
49
+ Perform comprehensive schematic validation.
50
+
51
+ Returns:
52
+ List of all validation issues found
53
+ """
54
+ issues = []
55
+
56
+ # Run all validation rules
57
+ for rule_name, rule_func in self._validation_rules.items():
58
+ try:
59
+ rule_issues = rule_func()
60
+ issues.extend(rule_issues)
61
+ logger.debug(f"Validation rule '{rule_name}' found {len(rule_issues)} issues")
62
+ except Exception as e:
63
+ issues.append(
64
+ ValidationIssue(
65
+ category="validation_system",
66
+ message=f"Validation rule '{rule_name}' failed: {e}",
67
+ level="error",
68
+ context={"rule": rule_name, "error": str(e)},
69
+ )
70
+ )
71
+ logger.error(f"Validation rule '{rule_name}' failed: {e}")
72
+
73
+ logger.info(f"Schematic validation completed with {len(issues)} issues")
74
+ return issues
75
+
76
+ def validate_component_references(self) -> List[ValidationIssue]:
77
+ """
78
+ Validate component references for duplicates and format.
79
+
80
+ Returns:
81
+ List of reference validation issues
82
+ """
83
+ issues = []
84
+
85
+ if not self._components:
86
+ return issues
87
+
88
+ references = []
89
+ reference_positions = {}
90
+
91
+ # Collect all component references
92
+ for component in self._components:
93
+ ref = component.reference
94
+ references.append(ref)
95
+ reference_positions[ref] = component.position
96
+
97
+ # Check for duplicate references
98
+ duplicates = set([ref for ref in references if references.count(ref) > 1])
99
+ for duplicate_ref in duplicates:
100
+ issues.append(
101
+ ValidationIssue(
102
+ category="component_references",
103
+ message=f"Duplicate component reference: {duplicate_ref}",
104
+ level="error",
105
+ context={"reference": duplicate_ref},
106
+ )
107
+ )
108
+
109
+ # Check reference format
110
+ for ref in set(references):
111
+ if not self._validate_reference_format(ref):
112
+ issues.append(
113
+ ValidationIssue(
114
+ category="component_references",
115
+ message=f"Invalid reference format: {ref}",
116
+ level="warning",
117
+ context={"reference": ref},
118
+ )
119
+ )
120
+
121
+ return issues
122
+
123
+ def validate_connectivity(self) -> List[ValidationIssue]:
124
+ """
125
+ Validate electrical connectivity and nets.
126
+
127
+ Returns:
128
+ List of connectivity validation issues
129
+ """
130
+ issues = []
131
+
132
+ if not self._wires or not self._components:
133
+ return issues
134
+
135
+ # Check for unconnected pins
136
+ unconnected_pins = self._find_unconnected_pins()
137
+ for component_ref, pin_number in unconnected_pins:
138
+ issues.append(
139
+ ValidationIssue(
140
+ category="connectivity",
141
+ message=f"Unconnected pin: {component_ref}.{pin_number}",
142
+ level="warning",
143
+ context={"component": component_ref, "pin": pin_number},
144
+ )
145
+ )
146
+
147
+ # Check for floating wires
148
+ floating_wires = self._find_floating_wires()
149
+ for wire_uuid in floating_wires:
150
+ issues.append(
151
+ ValidationIssue(
152
+ category="connectivity",
153
+ message=f"Floating wire (not connected to components): {wire_uuid}",
154
+ level="warning",
155
+ context={"wire": wire_uuid},
156
+ )
157
+ )
158
+
159
+ # Check for short circuits
160
+ short_circuits = self._find_potential_short_circuits()
161
+ for circuit_info in short_circuits:
162
+ issues.append(
163
+ ValidationIssue(
164
+ category="connectivity",
165
+ message=f"Potential short circuit: {circuit_info['description']}",
166
+ level="error",
167
+ context=circuit_info,
168
+ )
169
+ )
170
+
171
+ return issues
172
+
173
+ def validate_positioning(self) -> List[ValidationIssue]:
174
+ """
175
+ Validate component and element positioning.
176
+
177
+ Returns:
178
+ List of positioning validation issues
179
+ """
180
+ issues = []
181
+
182
+ # Check for overlapping components
183
+ if self._components:
184
+ overlapping_components = self._find_overlapping_components()
185
+ for comp1_ref, comp2_ref, distance in overlapping_components:
186
+ issues.append(
187
+ ValidationIssue(
188
+ category="positioning",
189
+ message=f"Components too close: {comp1_ref} and {comp2_ref} (distance: {distance:.2f})",
190
+ level="warning",
191
+ context={
192
+ "component1": comp1_ref,
193
+ "component2": comp2_ref,
194
+ "distance": distance,
195
+ },
196
+ )
197
+ )
198
+
199
+ # Check for components outside typical bounds
200
+ if self._components:
201
+ out_of_bounds = self._find_components_out_of_bounds()
202
+ for component_ref, position in out_of_bounds:
203
+ issues.append(
204
+ ValidationIssue(
205
+ category="positioning",
206
+ message=f"Component outside typical bounds: {component_ref} at {position}",
207
+ level="info",
208
+ context={"component": component_ref, "position": (position.x, position.y)},
209
+ )
210
+ )
211
+
212
+ return issues
213
+
214
+ def validate_design_rules(self) -> List[ValidationIssue]:
215
+ """
216
+ Validate against design rules and best practices.
217
+
218
+ Returns:
219
+ List of design rule validation issues
220
+ """
221
+ issues = []
222
+
223
+ # Check minimum wire spacing
224
+ wire_spacing_issues = self._check_wire_spacing()
225
+ issues.extend(wire_spacing_issues)
226
+
227
+ # Check power and ground connections
228
+ power_issues = self._check_power_connections()
229
+ issues.extend(power_issues)
230
+
231
+ # Check for missing bypass capacitors (simplified check)
232
+ bypass_issues = self._check_bypass_capacitors()
233
+ issues.extend(bypass_issues)
234
+
235
+ return issues
236
+
237
+ def validate_metadata(self) -> List[ValidationIssue]:
238
+ """
239
+ Validate schematic metadata and structure.
240
+
241
+ Returns:
242
+ List of metadata validation issues
243
+ """
244
+ issues = []
245
+
246
+ # Check required metadata fields
247
+ if not self._data.get("version"):
248
+ issues.append(
249
+ ValidationIssue(
250
+ category="metadata",
251
+ message="Missing KiCAD version information",
252
+ level="warning",
253
+ context={},
254
+ )
255
+ )
256
+
257
+ if not self._data.get("generator"):
258
+ issues.append(
259
+ ValidationIssue(
260
+ category="metadata",
261
+ message="Missing generator information",
262
+ level="info",
263
+ context={},
264
+ )
265
+ )
266
+
267
+ # Check title block
268
+ title_block = self._data.get("title_block", {})
269
+ if not title_block.get("title"):
270
+ issues.append(
271
+ ValidationIssue(
272
+ category="metadata", message="Missing schematic title", level="info", context={}
273
+ )
274
+ )
275
+
276
+ # Check paper size
277
+ from ..config import config
278
+
279
+ paper = self._data.get("paper")
280
+ if paper and paper not in config.paper.valid_sizes:
281
+ issues.append(
282
+ ValidationIssue(
283
+ category="metadata",
284
+ message=f"Non-standard paper size: {paper}",
285
+ level="info",
286
+ context={"paper": paper},
287
+ )
288
+ )
289
+
290
+ return issues
291
+
292
+ def get_validation_summary(self, issues: List[ValidationIssue]) -> Dict[str, Any]:
293
+ """
294
+ Generate validation summary statistics.
295
+
296
+ Args:
297
+ issues: List of validation issues
298
+
299
+ Returns:
300
+ Summary dictionary with counts and severity
301
+ """
302
+ summary = {
303
+ "total_issues": len(issues),
304
+ "error_count": len([i for i in issues if i.level == "error"]),
305
+ "warning_count": len([i for i in issues if i.level == "warning"]),
306
+ "info_count": len([i for i in issues if i.level == "info"]),
307
+ "categories": {},
308
+ "severity": "info",
309
+ }
310
+
311
+ # Count by category
312
+ for issue in issues:
313
+ category = issue.category
314
+ if category not in summary["categories"]:
315
+ summary["categories"][category] = 0
316
+ summary["categories"][category] += 1
317
+
318
+ # Determine overall severity
319
+ if summary["error_count"] > 0:
320
+ summary["severity"] = "error"
321
+ elif summary["warning_count"] > 0:
322
+ summary["severity"] = "warning"
323
+
324
+ return summary
325
+
326
+ def _initialize_validation_rules(self) -> Dict[str, callable]:
327
+ """Initialize all validation rules."""
328
+ return {
329
+ "component_references": self.validate_component_references,
330
+ "connectivity": self.validate_connectivity,
331
+ "positioning": self.validate_positioning,
332
+ "design_rules": self.validate_design_rules,
333
+ "metadata": self.validate_metadata,
334
+ }
335
+
336
+ def _validate_reference_format(self, reference: str) -> bool:
337
+ """Validate component reference format."""
338
+ if not reference:
339
+ return False
340
+
341
+ # Must start with letter(s), followed by numbers
342
+ if not reference[0].isalpha():
343
+ return False
344
+
345
+ # Find where numbers start
346
+ alpha_end = 0
347
+ for i, char in enumerate(reference):
348
+ if char.isdigit():
349
+ alpha_end = i
350
+ break
351
+ else:
352
+ return False # No numbers found
353
+
354
+ # Check alpha part
355
+ alpha_part = reference[:alpha_end]
356
+ if not alpha_part.isalpha():
357
+ return False
358
+
359
+ # Check numeric part
360
+ numeric_part = reference[alpha_end:]
361
+ if not numeric_part.isdigit():
362
+ return False
363
+
364
+ return True
365
+
366
+ def _find_unconnected_pins(self) -> List[Tuple[str, str]]:
367
+ """Find component pins that are not connected to any wires."""
368
+ unconnected = []
369
+
370
+ if not self._components or not self._wires:
371
+ return unconnected
372
+
373
+ # Get all wire endpoints
374
+ wire_points = set()
375
+ for wire in self._wires:
376
+ wire_points.add((wire.start.x, wire.start.y))
377
+ wire_points.add((wire.end.x, wire.end.y))
378
+
379
+ # Check each component pin
380
+ for component in self._components:
381
+ # This would need actual pin position calculation
382
+ # Simplified for now - would use component's pin positions
383
+ pass
384
+
385
+ return unconnected
386
+
387
+ def _find_floating_wires(self) -> List[str]:
388
+ """Find wires that don't connect to any components."""
389
+ floating = []
390
+
391
+ if not self._wires or not self._components:
392
+ return floating
393
+
394
+ # This would need actual connectivity analysis
395
+ # Simplified implementation
396
+ return floating
397
+
398
+ def _find_potential_short_circuits(self) -> List[Dict[str, Any]]:
399
+ """Find potential short circuits in the design."""
400
+ short_circuits = []
401
+
402
+ # This would need sophisticated electrical analysis
403
+ # Simplified implementation
404
+ return short_circuits
405
+
406
+ def _find_overlapping_components(self) -> List[Tuple[str, str, float]]:
407
+ """Find components that are positioned too close together."""
408
+ overlapping = []
409
+
410
+ if not self._components:
411
+ return overlapping
412
+
413
+ components = list(self._components)
414
+ min_distance = 10.0 # Minimum distance threshold
415
+
416
+ for i, comp1 in enumerate(components):
417
+ for comp2 in components[i + 1 :]:
418
+ distance = comp1.position.distance_to(comp2.position)
419
+ if distance < min_distance:
420
+ overlapping.append((comp1.reference, comp2.reference, distance))
421
+
422
+ return overlapping
423
+
424
+ def _find_components_out_of_bounds(self) -> List[Tuple[str, Point]]:
425
+ """Find components positioned outside typical schematic bounds."""
426
+ out_of_bounds = []
427
+
428
+ if not self._components:
429
+ return out_of_bounds
430
+
431
+ # Define typical bounds (these could be configurable)
432
+ min_x, min_y = 0, 0
433
+ max_x, max_y = 1000, 1000 # Adjust based on paper size
434
+
435
+ for component in self._components:
436
+ pos = component.position
437
+ if pos.x < min_x or pos.x > max_x or pos.y < min_y or pos.y > max_y:
438
+ out_of_bounds.append((component.reference, pos))
439
+
440
+ return out_of_bounds
441
+
442
+ def _check_wire_spacing(self) -> List[ValidationIssue]:
443
+ """Check minimum wire spacing requirements."""
444
+ issues = []
445
+
446
+ if not self._wires:
447
+ return issues
448
+
449
+ # This would check wire-to-wire spacing
450
+ # Simplified implementation
451
+ return issues
452
+
453
+ def _check_power_connections(self) -> List[ValidationIssue]:
454
+ """Check power and ground connection integrity."""
455
+ issues = []
456
+
457
+ if not self._components:
458
+ return issues
459
+
460
+ # Look for power components without proper connections
461
+ # This would need symbol analysis to identify power pins
462
+ # Simplified implementation
463
+ return issues
464
+
465
+ def _check_bypass_capacitors(self) -> List[ValidationIssue]:
466
+ """Check for missing bypass capacitors near ICs."""
467
+ issues = []
468
+
469
+ if not self._components:
470
+ return issues
471
+
472
+ # Find ICs and check for nearby capacitors
473
+ # This would need symbol analysis and proximity checking
474
+ # Simplified implementation
475
+ return issues