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,171 @@
1
+ """
2
+ Main Electrical Rules Checker orchestrator.
3
+
4
+ Coordinates all validators and produces comprehensive ERC results.
5
+ """
6
+
7
+ import time
8
+ from typing import TYPE_CHECKING, List, Optional
9
+
10
+ from kicad_sch_api.validation.erc_models import ERCConfig, ERCResult, ERCViolation
11
+ from kicad_sch_api.validation.validators import (
12
+ BaseValidator,
13
+ ComponentValidator,
14
+ ConnectivityValidator,
15
+ PinTypeValidator,
16
+ PowerValidator,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from kicad_sch_api.core.schematic import Schematic
21
+
22
+
23
+ class ElectricalRulesChecker:
24
+ """Main ERC orchestrator.
25
+
26
+ Coordinates all validation checks and produces comprehensive results.
27
+
28
+ Example:
29
+ >>> import kicad_sch_api as ksa
30
+ >>> from kicad_sch_api.validation import ElectricalRulesChecker
31
+ >>>
32
+ >>> sch = ksa.load_schematic("circuit.kicad_sch")
33
+ >>> erc = ElectricalRulesChecker(sch)
34
+ >>> result = erc.run_all_checks()
35
+ >>>
36
+ >>> if result.has_errors():
37
+ ... for error in result.errors:
38
+ ... print(f"ERROR: {error.message}")
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ schematic: "Schematic",
44
+ config: Optional[ERCConfig] = None
45
+ ) -> None:
46
+ """Initialize ERC checker.
47
+
48
+ Args:
49
+ schematic: Schematic to validate
50
+ config: Optional custom configuration
51
+ """
52
+ self.schematic = schematic
53
+ self.config = config or ERCConfig()
54
+ self.validators: List[BaseValidator] = []
55
+
56
+ # Register default validators
57
+ self._register_default_validators()
58
+
59
+ def _register_default_validators(self) -> None:
60
+ """Register default validators."""
61
+ self.validators = [
62
+ PinTypeValidator(self.schematic),
63
+ ConnectivityValidator(self.schematic),
64
+ ComponentValidator(self.schematic),
65
+ PowerValidator(self.schematic),
66
+ ]
67
+
68
+ def add_validator(self, validator: BaseValidator) -> None:
69
+ """Add custom validator.
70
+
71
+ Args:
72
+ validator: Custom validator to add
73
+ """
74
+ self.validators.append(validator)
75
+
76
+ def run_all_checks(self) -> ERCResult:
77
+ """Run all ERC checks.
78
+
79
+ Returns:
80
+ Complete ERC result with all violations
81
+ """
82
+ start_time = time.time()
83
+
84
+ all_violations: List[ERCViolation] = []
85
+
86
+ # Run each validator
87
+ for validator in self.validators:
88
+ violations = validator.validate()
89
+ all_violations.extend(violations)
90
+
91
+ # Apply configuration (severity overrides, suppression)
92
+ all_violations = self._apply_config(all_violations)
93
+
94
+ # Categorize by severity
95
+ errors = [v for v in all_violations if v.severity == "error"]
96
+ warnings = [v for v in all_violations if v.severity == "warning"]
97
+ info = [v for v in all_violations if v.severity == "info"]
98
+
99
+ # Calculate statistics
100
+ total_checks = len(all_violations) + 100 # Placeholder
101
+ passed_checks = total_checks - len(all_violations)
102
+
103
+ duration_ms = (time.time() - start_time) * 1000
104
+
105
+ return ERCResult(
106
+ errors=errors,
107
+ warnings=warnings,
108
+ info=info,
109
+ total_checks=total_checks,
110
+ passed_checks=passed_checks,
111
+ duration_ms=duration_ms,
112
+ )
113
+
114
+ def run_check(self, check_type: str) -> List[ERCViolation]:
115
+ """Run specific check type.
116
+
117
+ Args:
118
+ check_type: Type of check ("pin_types", "connectivity", "components", "power")
119
+
120
+ Returns:
121
+ List of violations from that check
122
+
123
+ Raises:
124
+ ValueError: If check type is invalid
125
+ """
126
+ validator_map = {
127
+ "pin_types": PinTypeValidator,
128
+ "connectivity": ConnectivityValidator,
129
+ "components": ComponentValidator,
130
+ "power": PowerValidator,
131
+ }
132
+
133
+ if check_type not in validator_map:
134
+ raise ValueError(f"Unknown check type: {check_type}")
135
+
136
+ validator = validator_map[check_type](self.schematic)
137
+ violations = validator.validate()
138
+
139
+ return self._apply_config(violations)
140
+
141
+ def _apply_config(self, violations: List[ERCViolation]) -> List[ERCViolation]:
142
+ """Apply configuration to violations.
143
+
144
+ Applies severity overrides and filters suppressed warnings.
145
+
146
+ Args:
147
+ violations: Raw violations
148
+
149
+ Returns:
150
+ Filtered and adjusted violations
151
+ """
152
+ result: List[ERCViolation] = []
153
+
154
+ for violation in violations:
155
+ # Check if suppressed
156
+ is_suppressed = False
157
+ for component_ref in violation.component_refs:
158
+ if self.config.is_suppressed(violation.error_code, component_ref):
159
+ is_suppressed = True
160
+ break
161
+
162
+ if is_suppressed:
163
+ continue
164
+
165
+ # Apply severity override
166
+ if violation.violation_type in self.config.severity_overrides:
167
+ violation.severity = self.config.severity_overrides[violation.violation_type]
168
+
169
+ result.append(violation)
170
+
171
+ return result
@@ -0,0 +1,203 @@
1
+ """
2
+ ERC data models: ERCViolation, ERCResult, ERCConfig.
3
+
4
+ These models represent validation results and configuration.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional, Set
10
+
11
+ from kicad_sch_api.core.types import Point
12
+
13
+
14
+ @dataclass
15
+ class ERCViolation:
16
+ """Single ERC violation.
17
+
18
+ Represents one electrical rule violation found during validation.
19
+
20
+ Attributes:
21
+ violation_type: Category of violation (e.g., "pin_conflict", "dangling_wire")
22
+ severity: "error", "warning", or "info"
23
+ message: Human-readable description
24
+ component_refs: List of affected component references
25
+ error_code: Unique error code (e.g., "E001", "W042")
26
+ net_name: Optional net name where violation occurred
27
+ pin_numbers: List of affected pin numbers
28
+ location: Optional schematic coordinates
29
+ suggested_fix: Optional recommended fix
30
+ """
31
+
32
+ violation_type: str
33
+ severity: str
34
+ message: str
35
+ component_refs: List[str]
36
+ error_code: str
37
+ net_name: Optional[str] = None
38
+ pin_numbers: List[str] = field(default_factory=list)
39
+ location: Optional[Point] = None
40
+ suggested_fix: Optional[str] = None
41
+
42
+ def to_dict(self) -> Dict[str, Any]:
43
+ """Convert violation to dictionary for serialization."""
44
+ return {
45
+ "violation_type": self.violation_type,
46
+ "severity": self.severity,
47
+ "message": self.message,
48
+ "component_refs": self.component_refs,
49
+ "error_code": self.error_code,
50
+ "net_name": self.net_name,
51
+ "pin_numbers": self.pin_numbers,
52
+ "location": {
53
+ "x": self.location.x,
54
+ "y": self.location.y
55
+ } if self.location else None,
56
+ "suggested_fix": self.suggested_fix,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class ERCResult:
62
+ """Complete ERC validation results.
63
+
64
+ Aggregates all violations found during validation.
65
+
66
+ Attributes:
67
+ errors: List of error-level violations
68
+ warnings: List of warning-level violations
69
+ info: List of info-level violations
70
+ total_checks: Total number of checks performed
71
+ passed_checks: Number of checks that passed
72
+ duration_ms: Execution time in milliseconds
73
+ """
74
+
75
+ errors: List[ERCViolation]
76
+ warnings: List[ERCViolation]
77
+ info: List[ERCViolation]
78
+ total_checks: int
79
+ passed_checks: int
80
+ duration_ms: float
81
+
82
+ def has_errors(self) -> bool:
83
+ """Check if any errors were found."""
84
+ return len(self.errors) > 0
85
+
86
+ def summary(self) -> str:
87
+ """Generate human-readable summary."""
88
+ error_count = len(self.errors)
89
+ warning_count = len(self.warnings)
90
+
91
+ error_str = f"{error_count} error{'s' if error_count != 1 else ''}"
92
+ warning_str = f"{warning_count} warning{'s' if warning_count != 1 else ''}"
93
+
94
+ return f"{error_str}, {warning_str}"
95
+
96
+ def filter_by_severity(self, severity: str) -> List[ERCViolation]:
97
+ """Filter violations by severity level.
98
+
99
+ Args:
100
+ severity: "error", "warning", or "info"
101
+
102
+ Returns:
103
+ List of violations matching severity
104
+ """
105
+ if severity == "error":
106
+ return self.errors
107
+ elif severity == "warning":
108
+ return self.warnings
109
+ elif severity == "info":
110
+ return self.info
111
+ else:
112
+ return []
113
+
114
+ def filter_by_component(self, ref: str) -> List[ERCViolation]:
115
+ """Filter violations affecting a specific component.
116
+
117
+ Args:
118
+ ref: Component reference (e.g., "R1")
119
+
120
+ Returns:
121
+ List of violations involving this component
122
+ """
123
+ all_violations = self.errors + self.warnings + self.info
124
+ return [v for v in all_violations if ref in v.component_refs]
125
+
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ """Convert result to dictionary for serialization."""
128
+ return {
129
+ "errors": [e.to_dict() for e in self.errors],
130
+ "warnings": [w.to_dict() for w in self.warnings],
131
+ "info": [i.to_dict() for i in self.info],
132
+ "total_checks": self.total_checks,
133
+ "passed_checks": self.passed_checks,
134
+ "duration_ms": self.duration_ms,
135
+ "summary": self.summary(),
136
+ }
137
+
138
+ def to_json(self) -> str:
139
+ """Convert result to JSON string."""
140
+ return json.dumps(self.to_dict(), indent=2)
141
+
142
+
143
+ class ERCConfig:
144
+ """ERC configuration.
145
+
146
+ Allows customizing validation behavior, severity levels, and rule suppression.
147
+
148
+ Attributes:
149
+ severity_overrides: Custom severity levels for specific rules
150
+ suppressed_warnings: Set of suppressed warning codes
151
+ custom_rules: List of custom validation rules (not yet implemented)
152
+ """
153
+
154
+ def __init__(self) -> None:
155
+ """Initialize with default configuration."""
156
+ self.severity_overrides: Dict[str, str] = {}
157
+ self.suppressed_warnings: Set[str] = set()
158
+ self.custom_rules: List[Any] = []
159
+
160
+ def set_severity(self, rule: str, severity: str) -> None:
161
+ """Override default severity for a rule.
162
+
163
+ Args:
164
+ rule: Rule identifier (e.g., "unconnected_input")
165
+ severity: "error", "warning", or "info"
166
+ """
167
+ if severity not in ["error", "warning", "info"]:
168
+ raise ValueError(f"Invalid severity: {severity}")
169
+ self.severity_overrides[rule] = severity
170
+
171
+ def suppress_warning(self, code: str, component: Optional[str] = None) -> None:
172
+ """Suppress a specific warning.
173
+
174
+ Args:
175
+ code: Warning code (e.g., "W001")
176
+ component: Optional component reference to suppress only for that component
177
+ """
178
+ if component:
179
+ # Store as "code:component" for component-specific suppression
180
+ self.suppressed_warnings.add(f"{code}:{component}")
181
+ else:
182
+ # Store as just "code" for global suppression
183
+ self.suppressed_warnings.add(code)
184
+
185
+ def is_suppressed(self, code: str, component: Optional[str] = None) -> bool:
186
+ """Check if a warning is suppressed.
187
+
188
+ Args:
189
+ code: Warning code
190
+ component: Optional component reference
191
+
192
+ Returns:
193
+ True if warning should be suppressed
194
+ """
195
+ # Check global suppression
196
+ if code in self.suppressed_warnings:
197
+ return True
198
+
199
+ # Check component-specific suppression
200
+ if component and f"{code}:{component}" in self.suppressed_warnings:
201
+ return True
202
+
203
+ return False
@@ -0,0 +1,243 @@
1
+ """
2
+ Pin Conflict Matrix for ERC validation.
3
+
4
+ Defines electrical compatibility rules between different pin types,
5
+ matching KiCAD's default ERC matrix.
6
+ """
7
+
8
+ from enum import IntEnum
9
+ from typing import Dict, Tuple
10
+
11
+
12
+ class PinSeverity(IntEnum):
13
+ """Severity levels for pin connections."""
14
+ OK = 0
15
+ WARNING = 1
16
+ ERROR = 2
17
+
18
+
19
+ class PinConflictMatrix:
20
+ """Pin type compatibility matrix.
21
+
22
+ Defines which pin type combinations are OK, WARNING, or ERROR.
23
+ Based on KiCAD's default ERC matrix.
24
+ """
25
+
26
+ # Pin type aliases for normalization
27
+ PIN_TYPE_ALIASES = {
28
+ "input": "input",
29
+ "pt_input": "input",
30
+ "i": "input",
31
+
32
+ "output": "output",
33
+ "pt_output": "output",
34
+ "o": "output",
35
+
36
+ "bidirectional": "bidirectional",
37
+ "pt_bidi": "bidirectional",
38
+ "bidi": "bidirectional",
39
+ "b": "bidirectional",
40
+
41
+ "tristate": "tristate",
42
+ "pt_tristate": "tristate",
43
+ "tri": "tristate",
44
+ "t": "tristate",
45
+
46
+ "passive": "passive",
47
+ "pt_passive": "passive",
48
+ "p": "passive",
49
+
50
+ "free": "free",
51
+ "nic": "free",
52
+ "pt_nic": "free",
53
+ "not_connected": "free",
54
+ "f": "free",
55
+
56
+ "unspecified": "unspecified",
57
+ "pt_unspecified": "unspecified",
58
+ "u": "unspecified",
59
+
60
+ "power_input": "power_input",
61
+ "pt_power_in": "power_input",
62
+ "pwr_in": "power_input",
63
+ "w": "power_input",
64
+
65
+ "power_output": "power_output",
66
+ "pt_power_out": "power_output",
67
+ "pwr_out": "power_output",
68
+
69
+ "open_collector": "open_collector",
70
+ "pt_opencollector": "open_collector",
71
+ "oc": "open_collector",
72
+ "c": "open_collector",
73
+
74
+ "open_emitter": "open_emitter",
75
+ "pt_openemitter": "open_emitter",
76
+ "oe": "open_emitter",
77
+ "e": "open_emitter",
78
+
79
+ "nc": "nc",
80
+ "pt_nc": "nc",
81
+ "not_connected": "nc",
82
+ "n": "nc",
83
+ }
84
+
85
+ def __init__(self) -> None:
86
+ """Initialize with KiCAD default matrix."""
87
+ self.matrix = self.get_default_matrix()
88
+
89
+ @staticmethod
90
+ def get_default_matrix() -> Dict[Tuple[str, str], int]:
91
+ """Get KiCAD default pin conflict matrix.
92
+
93
+ Returns:
94
+ Dictionary mapping (pin_type1, pin_type2) to severity
95
+ """
96
+ # Start with all combinations as OK
97
+ matrix: Dict[Tuple[str, str], int] = {}
98
+
99
+ # Define all pin types
100
+ pin_types = [
101
+ "input", "output", "bidirectional", "tristate", "passive",
102
+ "free", "unspecified", "power_input", "power_output",
103
+ "open_collector", "open_emitter", "nc"
104
+ ]
105
+
106
+ # Default: everything is OK
107
+ for pin1 in pin_types:
108
+ for pin2 in pin_types:
109
+ matrix[(pin1, pin2)] = PinSeverity.OK
110
+ matrix[(pin2, pin1)] = PinSeverity.OK # Ensure symmetry
111
+
112
+ # ERROR conditions (serious electrical conflicts)
113
+ error_rules = [
114
+ ("output", "output"), # Multiple outputs driving same net
115
+ ("power_output", "power_output"), # Multiple power supplies shorted
116
+ ("output", "power_output"), # Logic output to power rail
117
+ ("nc", "input"), # NC pin should not connect
118
+ ("nc", "output"),
119
+ ("nc", "bidirectional"),
120
+ ("nc", "tristate"),
121
+ ("nc", "power_input"),
122
+ ("nc", "power_output"),
123
+ ("nc", "open_collector"),
124
+ ("nc", "open_emitter"),
125
+ ]
126
+
127
+ for pin1, pin2 in error_rules:
128
+ matrix[(pin1, pin2)] = PinSeverity.ERROR
129
+ matrix[(pin2, pin1)] = PinSeverity.ERROR
130
+
131
+ # WARNING conditions (potential issues)
132
+ warning_rules = [
133
+ ("unspecified", "input"),
134
+ ("unspecified", "output"),
135
+ ("unspecified", "bidirectional"),
136
+ ("unspecified", "tristate"),
137
+ ("unspecified", "passive"),
138
+ ("unspecified", "power_input"),
139
+ ("unspecified", "power_output"),
140
+ ("unspecified", "open_collector"),
141
+ ("unspecified", "open_emitter"),
142
+ ("unspecified", "unspecified"),
143
+ ("tristate", "output"), # Tri-state with output can conflict
144
+ ("tristate", "tristate"), # Multiple tri-states
145
+ ]
146
+
147
+ for pin1, pin2 in warning_rules:
148
+ matrix[(pin1, pin2)] = PinSeverity.WARNING
149
+ matrix[(pin2, pin1)] = PinSeverity.WARNING
150
+
151
+ # Passive is OK with everything (except NC which is already ERROR)
152
+ for pin_type in pin_types:
153
+ if pin_type != "nc":
154
+ matrix[("passive", pin_type)] = PinSeverity.OK
155
+ matrix[(pin_type, "passive")] = PinSeverity.OK
156
+
157
+ # Free/NIC is OK with everything
158
+ for pin_type in pin_types:
159
+ matrix[("free", pin_type)] = PinSeverity.OK
160
+ matrix[(pin_type, "free")] = PinSeverity.OK
161
+
162
+ return matrix
163
+
164
+ def normalize_pin_type(self, pin_type: str) -> str:
165
+ """Normalize pin type string.
166
+
167
+ Handles case-insensitive matching and aliases.
168
+
169
+ Args:
170
+ pin_type: Pin type string
171
+
172
+ Returns:
173
+ Normalized pin type
174
+
175
+ Raises:
176
+ ValueError: If pin type is invalid
177
+ """
178
+ normalized = pin_type.lower().strip()
179
+
180
+ if normalized in self.PIN_TYPE_ALIASES:
181
+ return self.PIN_TYPE_ALIASES[normalized]
182
+
183
+ raise ValueError(f"Unknown pin type: {pin_type}")
184
+
185
+ def check_connection(self, pin1_type: str, pin2_type: str) -> int:
186
+ """Check if connection between two pin types is OK, WARNING, or ERROR.
187
+
188
+ Args:
189
+ pin1_type: First pin type
190
+ pin2_type: Second pin type
191
+
192
+ Returns:
193
+ PinSeverity.OK, PinSeverity.WARNING, or PinSeverity.ERROR
194
+
195
+ Raises:
196
+ ValueError: If pin type is invalid
197
+ """
198
+ # Normalize pin types
199
+ pin1 = self.normalize_pin_type(pin1_type)
200
+ pin2 = self.normalize_pin_type(pin2_type)
201
+
202
+ # Look up in matrix
203
+ key = (pin1, pin2)
204
+ if key in self.matrix:
205
+ return self.matrix[key]
206
+
207
+ # Should not happen if matrix is complete
208
+ raise ValueError(f"No rule for pin combination: {pin1} + {pin2}")
209
+
210
+ def set_rule(self, pin1_type: str, pin2_type: str, severity: int) -> None:
211
+ """Set custom rule for pin type combination.
212
+
213
+ Args:
214
+ pin1_type: First pin type
215
+ pin2_type: Second pin type
216
+ severity: PinSeverity.OK, WARNING, or ERROR
217
+ """
218
+ pin1 = self.normalize_pin_type(pin1_type)
219
+ pin2 = self.normalize_pin_type(pin2_type)
220
+
221
+ if severity not in [PinSeverity.OK, PinSeverity.WARNING, PinSeverity.ERROR]:
222
+ raise ValueError(f"Invalid severity: {severity}")
223
+
224
+ # Set both directions for symmetry
225
+ self.matrix[(pin1, pin2)] = severity
226
+ self.matrix[(pin2, pin1)] = severity
227
+
228
+ @classmethod
229
+ def from_dict(cls, custom_matrix: Dict[Tuple[str, str], int]) -> "PinConflictMatrix":
230
+ """Create matrix from custom dictionary.
231
+
232
+ Args:
233
+ custom_matrix: Dictionary of custom rules
234
+
235
+ Returns:
236
+ PinConflictMatrix with custom rules applied
237
+ """
238
+ matrix = cls()
239
+
240
+ for (pin1, pin2), severity in custom_matrix.items():
241
+ matrix.set_rule(pin1, pin2, severity)
242
+
243
+ return matrix