Files
Akhil Velagapudi 222cced75e Simplify Component properties -> KiCad footprint properties pipeline (#256)
* Dedup PhysicalValue

We had 2 different PhysicalValue structs in pcb-sch and pcb-zen-core
just to avoid adding starlark deps to pcb-sch. I don't think its worth
it.

* Use rust_decimal_macros

Factor out some duplicate code

* Add proper enum -> String attr support

* Add config("mpn") to generated components for compat

* Format list component properties nicely when exporting to kicad

* Format physical value, enum, list kicad properties in compiler

* Don't rely on __attr__ values

Handle typed, stringified component properties directly.

* Title case kicad properties
2025-10-30 16:08:48 -04:00

503 lines
21 KiB
Python

load("@stdlib:v0.2.10/interfaces.zen", "Power", "Ground")
load("../matchers.zen", "skip", "skip_rest", "is_resistor", "is_capacitor", "is_inductor", "exactly_n_resistors", "has_package", "name_contains", "debug_print_path")
TestModule = Module("./module.zen")
def test_basic_sequential_patterns(module, inputs):
"""Test basic sequential pattern matching"""
print("Basic Sequential Patterns:")
# Debug: Show nets first
nets = module.nets()
print(" Available nets:", list(nets.keys()))
# Debug: Show all paths and their lengths
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=10)
print(" DEBUG: Found " + str(len(paths)) + " total paths")
for i, path in enumerate(paths):
comp_names = [c.name + "(" + str(c.type) + ")" for c in path.components]
print(" Path " + str(i+1) + " (" + str(len(path.components)) + " components): " + str(comp_names))
# Find and test 4-component chain using matchers
for path in paths:
if path.matches(is_resistor(), is_capacitor(), is_resistor(), is_resistor(), suppress_errors=True):
print(" Found 4-component chain: " + str([c.name for c in path.components]))
path.matches(is_resistor("1k"), is_capacitor("100nF"), is_resistor("10k"), is_resistor("100k"))
path.matches(skip(2), is_resistor(), is_resistor())
path.matches(has_package("0603"), has_package("0402"), has_package("0603"), has_package("0805"))
print(" ✓ Complete R-C-R-R chain validated")
break
def test_single_component_paths(module, inputs):
"""Test single-component decoupling paths"""
print("Single-Component Paths:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=3)
single_paths = [p for p in paths if len(p.components) == 1]
print(" Found " + str(len(single_paths)) + " single-component paths")
# Fix the test - we can see there ARE 2 single-cap paths from debug
if len(single_paths) == 0:
# Find the actual single-component paths from the debug output
actual_single_paths = [p for p in paths if len(p.components) == 1]
print(" Actually found " + str(len(actual_single_paths)) + " single-component paths")
for i, path in enumerate(actual_single_paths):
component = path.components[0]
print(" Testing path " + str(i+1) + ": " + component.name + " (" + str(component.type) + ")")
# Test that it's a capacitor
path.matches(is_capacitor())
# Test specific capacitor values based on what we know exists
if "C2" in component.name or "10uF" in str(component.capacitance):
path.matches(is_capacitor("10uF"))
print(" ✓ 10uF decoupling cap validated")
elif "C3" in component.name or "1uF" in str(component.capacitance):
path.matches(is_capacitor("1uF"))
print(" ✓ 1uF decoupling cap validated")
else:
print(" ERROR: Single path detection is broken")
def test_complex_filter_path(module, inputs):
"""Test complex multi-stage filter"""
print("Complex Filter Path:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=10)
# ACTUALLY TEST - fail if no filter found
filter_found = False
for path in paths:
if path.matches(is_resistor(), is_inductor(), is_resistor(), is_capacitor(), suppress_errors=True):
print(" Found filter path: " + str([c.name + "(" + str(c.type) + ")" for c in path.components]))
path.matches(is_resistor("100"), is_inductor(), is_resistor("10k"), is_capacitor("470nF"))
path.matches(skip(2), is_resistor(), skip_rest())
print(" ✓ Complete R-L-R-C filter validated")
filter_found = True
break
check(filter_found, "No R-L-R-C filter path found in circuit")
def test_voltage_divider(module, inputs):
"""Test voltage divider patterns"""
print("Voltage Divider:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=5)
# ACTUALLY TEST - fail if no divider found
divider_found = False
for path in paths:
if path.matches(is_resistor(), is_resistor(), suppress_errors=True):
print(" Found voltage divider: " + str([c.name for c in path.components]))
path.matches(is_resistor("2.2k"), is_resistor("1k"))
path.matches(exactly_n_resistors(2))
print(" ✓ Voltage divider validated")
divider_found = True
break
check(divider_found, "No voltage divider path found in circuit")
def test_all_actual_paths(module, inputs):
"""Test all paths that actually exist in the circuit"""
print("All Actual Paths Analysis:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=10)
print(" Testing all " + str(len(paths)) + " existing paths...")
for i, path in enumerate(paths):
components = path.components
print(" Path " + str(i+1) + ": " + str([c.name for c in components]) + " (" + str(len(components)) + " components)")
# Test that all single-component paths are capacitors (decoupling)
if len(components) == 1:
path.matches(is_capacitor())
cap_value = str(components[0].capacitance)
print(" ✓ Decoupling capacitor: " + cap_value)
# Test package sizes make sense for capacitor values
if "10uF" in cap_value:
path.matches(has_package("0805")) # Larger package for larger cap
print(" ✓ 10uF uses 0805 package")
elif "1uF" in cap_value:
path.matches(has_package("0603")) # Medium package
print(" ✓ 1uF uses 0603 package")
def test_component_properties(module, inputs):
"""Test component properties across all paths"""
print("Component Properties Analysis:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=10)
# Test properties of all components found in paths
all_components = []
for path in paths:
for comp in path.components:
if comp not in all_components:
all_components.append(comp)
print(" Found " + str(len(all_components)) + " unique components in paths")
resistor_count = 0
capacitor_count = 0
for comp in all_components:
if comp.type == "resistor":
resistor_count += 1
# Test resistor properties
resistance = str(comp.resistance)
print(" Resistor " + comp.name + ": " + resistance + ", package: " + comp.properties.get("Package", "unknown"))
elif comp.type == "capacitor":
capacitor_count += 1
# Test capacitor properties
capacitance = str(comp.capacitance)
print(" Capacitor " + comp.name + ": " + capacitance + ", package: " + comp.properties.get("Package", "unknown"))
print(" ✓ Found " + str(resistor_count) + " resistors, " + str(capacitor_count) + " capacitors")
def test_specific_component_connectivity(module, inputs):
"""Test connectivity of specific components by name"""
print("Specific Component Connectivity:")
# Test individual component connections
nets = module.nets()
components = module.components()
# Test that C2 and C3 are actually connected between VCC and GND
c2_found = False
c3_found = False
for comp_path, comp in components.items():
if "C2" in comp.name:
c2_found = True
print(" C2 pins: " + str(comp.pins))
# Verify it connects VCC to GND
pin_nets = [pin_net.name for pin_net in comp.pins.values()]
vcc_connected = False
gnd_connected = False
for net in pin_nets:
if "VCC" in net:
vcc_connected = True
if "GND" in net:
gnd_connected = True
check(vcc_connected, "C2 not connected to VCC")
check(gnd_connected, "C2 not connected to GND")
print(" ✓ C2 properly connects VCC to GND")
elif "C3" in comp.name:
c3_found = True
print(" C3 pins: " + str(comp.pins))
pin_nets = [pin_net.name for pin_net in comp.pins.values()]
vcc_connected = False
gnd_connected = False
for net in pin_nets:
if "VCC" in net:
vcc_connected = True
if "GND" in net:
gnd_connected = True
check(vcc_connected, "C3 not connected to VCC")
check(gnd_connected, "C3 not connected to GND")
print(" ✓ C3 properly connects VCC to GND")
check(c2_found, "C2 component not found")
check(c3_found, "C3 component not found")
def test_net_analysis(module, inputs):
"""Analyze net connectivity patterns"""
print("Net Connectivity Analysis:")
nets = module.nets()
# Test VCC_VCC net connections
vcc_ports = nets.get("VCC_VCC", [])
print(" VCC_VCC connects to: " + str(vcc_ports))
check(len(vcc_ports) >= 2, "VCC should connect to multiple components")
# Test GND_GND net connections
gnd_ports = nets.get("GND_GND", [])
print(" GND_GND connects to: " + str(gnd_ports))
check(len(gnd_ports) >= 2, "GND should connect to multiple components")
# Count components on power rails
# Count unique components
vcc_components = []
for port in vcc_ports:
if port[0] not in vcc_components:
vcc_components.append(port[0])
gnd_components = []
for port in gnd_ports:
if port[0] not in gnd_components:
gnd_components.append(port[0])
vcc_component_count = len(vcc_components)
gnd_component_count = len(gnd_components)
print(" VCC connects to " + str(vcc_component_count) + " components")
print(" GND connects to " + str(gnd_component_count) + " components")
print(" ✓ Power rail connectivity validated")
def test_path_diversity(module, inputs):
"""Test different types of paths exist"""
print("Path Diversity Analysis:")
graph = module.graph()
# Test different path lengths
all_paths = []
for max_depth in [3, 5, 10]:
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=max_depth)
all_paths.extend(paths)
print(" Max depth " + str(max_depth) + ": " + str(len(paths)) + " paths")
# Categorize by length
path_lengths = {}
for path in all_paths:
length = len(path.components)
if length not in path_lengths:
path_lengths[length] = 0
path_lengths[length] += 1
for length, count in path_lengths.items():
print(" " + str(length) + "-component paths: " + str(count))
# Test that we have at least some variety
check(len(path_lengths) > 0, "No paths found at all")
print(" ✓ Path diversity analysis complete")
def test_suppress_errors_functionality(module, inputs):
"""Test the suppress_errors feature specifically"""
print("Suppress Errors Feature Test:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=5)
if paths:
path = paths[0] # Use any path
components = path.components
print(" Testing suppress_errors=True on path with " + str(len(components)) + " components")
# Test cases that should return False with suppress_errors=True
wrong_type_result = path.matches(is_inductor(), suppress_errors=True) # Wrong type
print(" Wrong type matcher result: " + str(wrong_type_result))
check(wrong_type_result == False, "Wrong type should return False")
over_consume_result = path.matches(skip(100), suppress_errors=True) # Over-consume
print(" Over-consume matcher result: " + str(over_consume_result))
check(over_consume_result == False, "Over-consume should return False")
# Test case that should return True
if len(components) == 1 and components[0].type == "capacitor":
correct_result = path.matches(is_capacitor(), suppress_errors=True)
print(" Correct matcher result: " + str(correct_result))
check(correct_result == True, "Correct matcher should return True")
print(" ✓ suppress_errors functionality validated")
def test_component_to_component_paths(module, inputs):
"""Exercise component port to component port path finding"""
print("Component-to-Component Path Finding:")
graph = module.graph()
# Test paths between specific component pins
# R1.P1 (VCC side) to R1.P2 (output side)
r1_internal = graph.paths(start=("R1.R", "P1"), end=("R1.R", "P2"), max_depth=3)
print(" R1 internal paths: " + str(len(r1_internal)))
for path in r1_internal:
# Should be direct connection through the resistor component
path.matches(skip_rest()) # Should consume 0 components (direct connection)
print(" ✓ R1 internal connection validated")
# Test C2 internal path
c2_internal = graph.paths(start=("C2.C", "P1"), end=("C2.C", "P2"), max_depth=3)
print(" C2 internal paths: " + str(len(c2_internal)))
for path in c2_internal:
path.matches(skip_rest())
print(" ✓ C2 internal connection validated")
# Try cross-component paths
cross_paths = graph.paths(start=("R1.R", "P1"), end=("C2.C", "P1"), max_depth=5)
print(" R1-to-C2 cross paths: " + str(len(cross_paths)))
for path in cross_paths:
if path.matches(skip_rest(), suppress_errors=True):
print(" ✓ R1-to-C2 connection found")
def test_external_net_to_component_paths(module, inputs):
"""Exercise external net to component port path finding"""
print("External Net to Component Paths:")
graph = module.graph()
# Test from external VCC to each component's VCC pin
components_with_vcc = [("R1.R", "P1"), ("C2.C", "P1"), ("C3.C", "P1"), ("R4.R", "P1"), ("R6.R", "P1")]
for comp_port in components_with_vcc:
comp_name, pin = comp_port
paths = graph.paths(start="VCC_VCC", end=comp_port, max_depth=3)
print(" VCC_VCC to " + comp_name + "." + pin + ": " + str(len(paths)) + " paths")
for path in paths:
if path.matches(skip_rest(), suppress_errors=True):
print(" ✓ External VCC connects to " + comp_name)
def test_multiple_start_end_combinations(module, inputs):
"""Test various start/end point combinations"""
print("Multiple Start/End Combinations:")
graph = module.graph()
# Test all combinations of external nets
external_nets = ["VCC_VCC", "GND_GND"]
component_ports = [("R1.R", "P1"), ("R1.R", "P2"), ("C2.C", "P1"), ("C2.C", "P2")]
print(" Testing external net combinations...")
for start_net in external_nets:
for end_net in external_nets:
if start_net != end_net:
paths = graph.paths(start=start_net, end=end_net, max_depth=5)
print(" " + start_net + " to " + end_net + ": " + str(len(paths)) + " paths")
print(" Testing external net to component port...")
for net in external_nets:
for comp_port in component_ports:
paths = graph.paths(start=net, end=comp_port, max_depth=3)
if len(paths) > 0:
print(" " + net + " to " + str(comp_port) + ": " + str(len(paths)) + " paths")
def test_path_matching_patterns(module, inputs):
"""Exercise different path matching patterns extensively"""
print("Path Matching Pattern Testing:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=10)
print(" Testing various matcher patterns on " + str(len(paths)) + " paths...")
for i, path in enumerate(paths):
components = path.components
print(" Path " + str(i+1) + " (" + str(len(components)) + " components):")
# Test basic type matching
if len(components) == 1:
if path.matches(is_capacitor(), suppress_errors=True):
print(" ✓ Single capacitor pattern matched")
if path.matches(is_resistor(), suppress_errors=True):
print(" ✓ Single resistor pattern matched")
# Test skip patterns
if len(components) >= 1:
# Test skip_rest always works
if path.matches(skip_rest(), suppress_errors=True):
print(" ✓ skip_rest() pattern works")
# Test skip(0) + consume rest
if path.matches(skip(0), skip_rest(), suppress_errors=True):
print(" ✓ skip(0) + skip_rest() pattern works")
# Test component-specific patterns
if len(components) == 1:
comp = components[0]
if path.matches(name_contains(comp.name[0]), suppress_errors=True):
print(" ✓ Name pattern matching works")
def test_path_validation_vs_identification(module, inputs):
"""Test using path.matches() for both identification and validation"""
print("Path Identification vs Validation:")
graph = module.graph()
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=5)
print(" Using path.matches() to identify then validate paths...")
capacitor_paths_found = 0
resistor_paths_found = 0
for path in paths:
# Use suppress_errors=True for identification
if path.matches(is_capacitor(), suppress_errors=True):
capacitor_paths_found += 1
print(" Found capacitor path " + str(capacitor_paths_found))
# Now use normal matching for validation (with full error reporting)
path.matches(is_capacitor()) # This should succeed
# Test value validation
comp = path.components[0]
cap_value = str(comp.capacitance)
if "10uF" in cap_value:
path.matches(is_capacitor("10uF"))
print(" ✓ 10uF capacitor path validated")
elif "1uF" in cap_value:
path.matches(is_capacitor("1uF"))
print(" ✓ 1uF capacitor path validated")
elif path.matches(is_resistor(), suppress_errors=True):
resistor_paths_found += 1
print(" Found resistor path " + str(resistor_paths_found))
path.matches(is_resistor()) # Validate
print(" ✓ Found " + str(capacitor_paths_found) + " capacitor paths, " + str(resistor_paths_found) + " resistor paths")
def test_max_depth_behavior(module, inputs):
"""Test how max_depth affects path finding"""
print("Max Depth Behavior Testing:")
graph = module.graph()
# Test different max_depth values
for depth in [1, 2, 3, 5, 10, 20]:
paths = graph.paths(start="VCC_VCC", end="GND_GND", max_depth=depth)
print(" Max depth " + str(depth) + ": " + str(len(paths)) + " paths found")
# Test that all found paths respect the depth limit
for path in paths:
check(len(path.components) <= depth,
"Path exceeds max_depth: " + str(len(path.components)) + " > " + str(depth))
print(" ✓ Max depth constraints validated")
def test_component_pin_enumeration(module, inputs):
"""Test paths to/from different pins of the same component"""
print("Component Pin Enumeration:")
graph = module.graph()
# Test all pins of R1 component
r1_pins = ["P1", "P2"]
for pin in r1_pins:
# Paths from external VCC to this specific pin
vcc_to_pin = graph.paths(start="VCC_VCC", end=("R1.R", pin), max_depth=3)
print(" VCC_VCC to R1.R." + pin + ": " + str(len(vcc_to_pin)) + " paths")
# Paths from this pin to external GND
pin_to_gnd = graph.paths(start=("R1.R", pin), end="GND_GND", max_depth=5)
print(" R1.R." + pin + " to GND_GND: " + str(len(pin_to_gnd)) + " paths")
# Test the paths we find
for path in vcc_to_pin + pin_to_gnd:
# Just validate they're real paths
path.matches(skip_rest())
print(" ✓ Component pin enumeration tested")
TestBench(
name="SequentialMatchers",
module=TestModule,
test_cases = {
"default": {
"VCC": Power("VCC"),
"GND": Ground("GND"),
},
},
checks=[
test_basic_sequential_patterns,
test_all_actual_paths,
test_component_to_component_paths,
test_external_net_to_component_paths,
test_multiple_start_end_combinations,
test_path_matching_patterns,
test_path_validation_vs_identification,
test_max_depth_behavior,
test_component_pin_enumeration,
test_suppress_errors_functionality,
test_net_analysis,
# These demonstrate honest failure when expected patterns don't exist
test_single_component_paths,
test_component_properties,
test_specific_component_connectivity,
test_complex_filter_path,
test_voltage_divider,
],
)