Files
diodeinc__pcb/docs/pages/spec.mdx
2025-10-15 14:23:45 -04:00

1702 lines
56 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Zener Language"
description: "Hardware Description Language for PCB design"
---
# Specification
## Overview
Zener is a domain-specific language built on top of
[Starlark](https://github.com/bazelbuild/starlark/blob/master/spec.md) for
describing PCB schematics. It provides primitives for defining components, symbols, nets,
interfaces, and modules in a type-safe, composable manner.
This specification describes the language extensions and primitives added on top
of Starlark. For the base Starlark language features, please refer to the
[Starlark specification](https://github.com/bazelbuild/starlark/blob/master/spec.md) and the
[starlark-rust types extension](https://github.com/facebook/starlark-rust/blob/main/docs/types.md).
## Table of Contents
1. [Evaluation Model](#evaluation-model)
2. [Core Types](#core-types)
3. [Built-in Functions](#built-in-functions)
4. [Module System](#module-system)
5. [Type System](#type-system)
## Evaluation Model
### Files as Modules
Each `.zen` file is a Starlark module. It can be used in two ways:
1. Its exported symbols can be `load()`ed into other modules. For example,
`load("./MyFile.zen", "MyFunction", "MyType")` will load the `MyFunction`
and `MyType` symbols from the `MyFile.zen` module.
2. It can be loaded as a schematic module using the `Module()` helper. For
example, `MyFile = Module("./MyFile.zen")` will import `MyFile.zen` as a
schematic module, which you can instantiate like so:
```python
MyFile = Module("./MyFile.zen")
MyFile(
name = "MyFile",
...
)
```
### Load Resolution
The `load()` and `Module()` statements support multiple resolution strategies:
```python
# Local file (relative to current file)
load("./utils.zen", "helper")
# Package reference
load("@stdlib:1.2.3/math.zen", "calculate")
# GitHub repository
load("@github/user/repo:branch/path.zen", "function")
# GitLab repository
load("@gitlab/user/repo:branch/path.zen", "function")
# GitLab repository (nested groups)
Symbol(library = "@gitlab/kicad/libraries/kicad-symbols:v7.0.0/Device.kicad_sym", name = "R_US")
```
#### Default Package Aliases
Zener provides built-in package aliases for commonly used libraries:
- `@kicad-footprints` → `@gitlab/kicad/libraries/kicad-footprints:9.0.0`
- `@kicad-symbols` → `@gitlab/kicad/libraries/kicad-symbols:9.0.0`
- `@stdlib` → `@github/diodeinc/stdlib:HEAD`
These can be used directly:
```python
# Load from stdlib
load("@stdlib/units.zen", "kohm", "uF")
# Load from KiCad symbols library
R_symbol = Symbol(library = "@kicad-symbols/Device.kicad_sym", name = "R")
```
#### Custom Package Aliases
You can define custom package aliases or override the defaults in your workspace's `pcb.toml`:
```toml
[packages]
# Override default version
kicad-symbols = "@gitlab/kicad/libraries/kicad-symbols:7.0.0"
# Add custom aliases
my-lib = "@github/myorg/mylib:v1.0.0"
local-lib = "./path/to/local/lib"
```
## Core Types
### Net
A `Net` represents an electrical connection between component pins. Nets can optionally specify electrical properties like impedance, voltage range, and schematic symbols.
```python
# Create a net with optional name
net1 = Net()
net2 = Net("VCC")
# Net with impedance (for controlled impedance routing)
load("@stdlib/units.zen", "Impedance")
clk = Net("CLK", impedance=Impedance(50)) # 50Ω single-ended
# Net with voltage range
load("@stdlib/units.zen", "VoltageRange")
vdd = Net("VDD_3V3", voltage=VoltageRange("3.0V to 3.6V"))
# Net with custom schematic symbol
vcc_sym = Symbol(library="@kicad-symbols/power.kicad_sym", name="VCC")
vcc = Net("VCC", symbol=vcc_sym)
```
**Type**: `Net`
**Constructor**: `Net(name="", symbol=None, voltage=None, impedance=None)`
- `name` (optional): String identifier for the net
- `symbol` (optional): Symbol object for schematic representation
- `voltage` (optional): VoltageRange specification for the net
- `impedance` (optional): Impedance specification for single-ended nets (in Ohms)
### Symbol
A `Symbol` represents a schematic symbol definition with its pins. Symbols can be created manually or loaded from KiCad symbol libraries.
```python
# Create a symbol from explicit definition
my_symbol = Symbol(
name = "MyDevice",
definition = [
("VCC", ["1", "8"]), # VCC on pins 1 and 8
("GND", ["4"]), # GND on pin 4
("IN", ["2"]), # IN on pin 2
("OUT", ["7"]) # OUT on pin 7
]
)
# Load from a KiCad symbol library
op_amp = Symbol(library = "./symbols/LM358.kicad_sym")
# For multi-symbol libraries, specify which symbol
mcu = Symbol(library = "./symbols/microcontrollers.kicad_sym", name = "STM32F103")
# Shorthand syntax: library path and symbol name in one string
gnd = Symbol("@kicad-symbols/power.kicad_sym:GND")
resistor = Symbol("./symbols/passives.kicad_sym:R_0402")
# For single-symbol libraries, the name can be omitted
op_amp = Symbol("./symbols/LM358.kicad_sym")
```
**Type**: `Symbol`
**Constructor**: `Symbol(library_spec=None, name=None, definition=None, library=None)`
- `library_spec`: (positional) String in format "library_path:symbol_name" or just "library_path" for single-symbol libraries
- `name`: Symbol name (required when loading from multi-symbol library with named parameters)
- `definition`: List of (signal_name, [pad_numbers]) tuples
- `library`: Path to KiCad symbol library file
Note: You cannot mix the positional `library_spec` argument with the named `library` or `name` parameters.
### Component
Components represent physical electronic parts with pins and properties.
```python
# Using a Symbol for pin definitions
my_symbol = Symbol(
definition = [
("VCC", ["1"]),
("GND", ["4"]),
("OUT", ["8"])
]
)
Component(
name = "U1", # Required: instance name
footprint = "SOIC-8", # Required: PCB footprint
symbol = my_symbol, # Symbol defines the pins
pins = { # Required: pin connections
"VCC": vcc_net,
"GND": gnd_net,
"OUT": output_net
},
prefix = "U", # Optional: reference designator prefix (default: "U")
mpn = "LM358", # Optional: manufacturer part number
type = "op-amp", # Optional: component type
properties = { # Optional: additional properties
"voltage": "5V"
}
)
```
**Type**: `Component`
**Constructor**: `Component(**kwargs)`
Key parameters:
- `name`: Instance name (required)
- `footprint`: PCB footprint (required)
- `symbol`: Symbol object defining pins (required)
- `pins`: Pin connections to nets (required)
- `prefix`: Reference designator prefix (default: "U")
- `mpn`: Manufacturer part number
- `type`: Component type
- `properties`: Additional properties dict
### Interface
Interfaces define reusable connection patterns with field specifications and type validation. Interfaces can specify impedance requirements that automatically propagate to their constituent nets during layout.
**Differential Pair Example:**
```python
load("@stdlib/units.zen", "Impedance")
# DiffPair interface with 90Ω differential impedance
DiffPair = interface(
P=Net(),
N=Net(),
impedance=field(Impedance, None), # Optional impedance
)
# USB interface uses DiffPair with 90Ω impedance
Usb2 = interface(
D=DiffPair(impedance=Impedance(90)),
)
# When instantiated, impedance automatically propagates to P/N nets
usb = Usb2("USB") # USB.D.P and USB.D.N both get 90Ω differential impedance
```
The impedance from DiffPair interfaces is stored as `differential_impedance` on the P and N nets, allowing the layout system to assign appropriate netclasses for differential pair routing.
#### Basic Syntax
```python
InterfaceName = interface(
field_name = field_specification,
__post_init__ = callback_function, # Optional
)
```
#### Field Types
**Net Instances**: Use the provided Net instance as the default template
```python
NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC"))
SDA = Net("I2C_SDA")
```
**Interface Instances**: Use the provided interface instance for composition
```python
uart = Uart(TX=Net("UART_TX"), RX=Net("UART_RX"))
power = Power(NET=Net("VDD"))
```
**field() Specifications**: Enforce type checking with explicit defaults
```python
voltage = field(Voltage, unit("3.3V", Voltage))
freqs = field(list[str], ["100kHz", "400kHz"])
count = field(int, 42)
```
#### Interface Instantiation
```python
InterfaceName([name], field1=value1, field2=value2, ...)
```
- **Optional name**: First positional argument sets the interface instance name
- **Field overrides**: Named parameters override defaults
- **Type validation**: Values must match field specifications
#### Examples
```python
# Define interfaces
Power = interface(
NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC")),
voltage = field(Voltage, unit("3.3V", Voltage)),
)
Uart = interface(
TX = Net("UART_TX"),
RX = Net("UART_RX"),
)
SystemInterface = interface(
power = Power(),
uart = Uart(),
debug = field(bool, False),
)
# Create instances
power = Power() # All defaults
vcc = Power("VCC_3V3") # Named instance
custom = Power("MAIN", voltage=unit("5V", Voltage)) # Named with override
system = SystemInterface("MAIN", debug=True)
```
#### Post-Initialization Callbacks
```python
def _power_post_init(self):
if self.voltage.value <= 0:
error("Power voltage must be positive")
Power = interface(
NET = Net("VCC"),
voltage = field(Voltage, unit("3.3V", Voltage)),
__post_init__ = _power_post_init,
)
```
Callbacks receive `self` and cannot be overridden during instantiation.
**Type**: `interface`
**Constructor**: `interface(**fields)`
- Fields can be Net instances, interface instances, or `field()` specifications
### Module
Modules represent hierarchical subcircuits that can be instantiated multiple times. Module objects support indexing to access child components and submodules directly.
```python
# Load a module from a file
SubCircuit = Module("./subcircuit.zen")
# Instantiate the module
SubCircuit(
name = "power_supply",
# ... pass inputs defined by io() and config() in the module
)
# Access child components and submodules via indexing
component = module["ComponentName"] # Access component by name
submodule = module["SubmoduleName"] # Access submodule by name
# Chain indexing for nested access
nested_component = module["SubmoduleName"]["ComponentName"]
# Nested path syntax (equivalent to chained indexing)
nested_component = module["SubmoduleName.ComponentName"] # Same as above
# Check if components exist using membership operator
if "ComponentName" in module:
component = module["ComponentName"]
# Check nested paths
if "SubmoduleName.ComponentName" in module:
nested_component = module["SubmoduleName.ComponentName"]
```
**Type**: `Module`
**Constructor**: `Module(path)`
**Module Indexing**: `module[name]` supports:
- Single names: `module["ComponentName"]` returns Component or Module objects
- Nested paths: `module["Sub.Component"]` equivalent to `module["Sub"]["Component"]`
- Deep nesting: `module["A.B.C"]` equivalent to `module["A"]["B"]["C"]`
- Returns Component objects for leaf components, Module objects for intermediate submodules
- Raises an error if any part of the path is not found
**Module Membership**: `name in module` supports:
- Single names: `"ComponentName" in module` checks if component or submodule exists
- Nested paths: `"Sub.Component" in module` equivalent to checking nested existence
- Returns `True` if the path exists, `False` otherwise
- Works with the same path syntax as indexing
**Module Attributes**:
- `module.nets`: Dict mapping net names to lists of connected port tuples
- `module.components`: Dict mapping component paths to component objects
**Naming Conventions**:
- Component paths in `module.components` follow the pattern `SubmoduleName.ComponentName` (e.g., `"BMI270.BMI270"`, `"C1.C"`)
- The first part is the submodule name, the second part is the component name within that submodule
- Indexing supports both single names and nested paths:
- `module["BMI270"]` returns the BMI270 submodule
- `module["BMI270"]["BMI270"]` returns the component within the submodule (chained)
- `module["BMI270.BMI270"]` also returns the component (nested path syntax)
- All three approaches are equivalent for accessing nested components
### TestBench
TestBench values represent the results of module validation tests. They are created by the `TestBench()` function and contain information about the tested module and check results.
```python
# TestBench values are returned by TestBench()
result = TestBench(
name = "MyTest",
module = MyModule,
checks = [check_func1, check_func2]
)
# Access TestBench properties
print(result) # TestBench(MyTest)
```
**Type**: `TestBench`
**Created by**: `TestBench()` function (see [Built-in Functions](#testbenchname-module-checks))
Properties accessible via the TestBench value:
- `name`: The test bench identifier
- Module evaluation status
- Check function results
### PhysicalRange
A `PhysicalRange` represents a bounded range of physical values with an optional nominal value, used for specifying operating ranges, tolerances, and electrical characteristics. Ranges have minimum and maximum bounds with consistent physical units.
```python
# PhysicalRange values are created by unit-specific range constructors
# (See builtin.physical_range() in Built-in Functions)
# String parsing with range separators
voltage_range = VoltageRange("1.1–3.6V") # 1.1–3.6 V
supply_range = VoltageRange("11V to 26V") # 11–26 V
# With nominal value in parentheses
input_range = VoltageRange("11–26 V (12 V nom.)") # 11–26 V (12 V nom.)
# Tolerance expansion
nominal_voltage = VoltageRange("15V 10%") # 13.5–16.5 V
# From PhysicalValue
single_point = VoltageRange(Voltage(15)) # 15–15 V
# Keyword arguments
operating_range = VoltageRange(min=11, nominal=16, max=26) # 11–26 V (16 V nom.)
# Mixed string/number keywords
custom_range = VoltageRange(min="11V", nominal="16", max="26V") # 11–26 V (16 V nom.)
# Override nominal separately
with_nominal = VoltageRange("11V to 26V", nominal="16V") # 11–26 V (16 V nom.)
```
**Type**: `PhysicalRange`
**Created by**: Unit-specific range constructors (e.g., `VoltageRange`, `CurrentRange`)
Properties:
- `min`: Minimum value (Decimal)
- `max`: Maximum value (Decimal)
- `nominal`: Optional nominal/typical value (Decimal or None)
- `unit`: Physical unit dimensions
**Display Format**:
- With nominal: `11–26 V (12 V nom.)`
- Without nominal: `11–26 V`
#### PhysicalRangeType
A `PhysicalRangeType` is a factory type that creates `PhysicalRange` instances for a specific physical unit. Each unit has its own range type (e.g., `VoltageRange` for volts, `CurrentRange` for amperes).
```python
# Range types are created by builtin.physical_range()
VoltageRange = builtin.physical_range("V")
CurrentRange = builtin.physical_range("A")
# Range types are callable and return PhysicalRange instances
input_voltage = VoltageRange("3.3V to 5V")
load_current = CurrentRange("100mA to 500mA")
```
**Type**: `PhysicalRangeType`
**Created by**: `builtin.physical_range(unit)` (see [Built-in Functions](#builtinphysical_rangeunit))
**Standard Range Types** (available in `@stdlib/units.zen`):
- `VoltageRange` - Voltage ranges (V)
- `CurrentRange` - Current ranges (A)
- `ResistanceRange` - Resistance ranges (Ω)
- `CapacitanceRange` - Capacitance ranges (F)
- `InductanceRange` - Inductance ranges (H)
- `FrequencyRange` - Frequency ranges (Hz)
- `TemperatureRange` - Temperature ranges (K)
- `TimeRange` - Time ranges (s)
- `PowerRange` - Power ranges (W)
**Construction Flexibility**:
Range constructors accept multiple input formats for maximum flexibility:
1. **String parsing with separators**:
- En-dash: `"1.1–3.6V"`
- Word "to": `"11V to 26V"`
- Left side can omit unit: `"11–26V"`
2. **Nominal value extraction**:
- Parenthesized suffix: `"11–26 V (12 V nom.)"`
- Can be combined with string ranges or keyword override
3. **Tolerance expansion**:
- Percentage tolerance: `"15V 10%"` expands to `13.5–16.5 V`
4. **PhysicalValue conversion**:
- Single value: `VoltageRange(Voltage(15))` creates point range `15–15 V`
- Toleranced value: `VoltageRange(Voltage(15, 0.1))` expands tolerance to range
5. **Keyword arguments**:
- Explicit bounds: `min=11, max=26`
- With nominal: `min=11, nominal=16, max=26`
- Accepts numbers or strings: `min="11V"`, `max="26V"`
6. **Mixed modes**:
- String range with keyword nominal: `VoltageRange("11V to 26V", nominal="16V")`
**Type Safety**:
- Units must be consistent across min, max, and nominal values
- Range types are distinct: `VoltageRange` ≠ `CurrentRange`
- Type checking enforces unit correctness at compile time
**Use Cases**:
- Component operating ranges: `input_voltage = VoltageRange("2.7V to 5.5V")`
- Load specifications: `output_current = CurrentRange("0A to 3A")`
- Environmental conditions: `operating_temp = TemperatureRange("-40C to 85C")`
- Future: Electrical Rule Checking (ERC) - validate that supply ranges satisfy load requirements
## Built-in Functions
### moved(old_path, new_path)
**Parameters**
- `old_path`: The old path that should be remapped
- `new_path`: The new path to remap to
The `moved()` directive allows you to specify path remapping for refactoring support. When modules or components are moved or renamed, downstream artifacts (like layout files) may still reference the old paths. The `moved()` directive provides a mapping from old paths to new paths.
`moved()` directives are scoped to the module where they are defined. They affect how paths from that module are resolved in downstream artifacts.
If the path refers to a module, all children of that module are automatically remapped:
```python
# This single directive handles:
# POW.PS1 -> PS1
# POW.PS1.some_resistor -> PS1.some_resistor
# POW.PS1.inner.component -> PS1.inner.component
moved("POW.PS1", "PS1")
```
```python
# Moved a power supply module from POW namespace to root
moved("POW.PS1", "PS1")
# Renamed a component
moved("OLD_COMP", "NEW_COMP")
# Moved components between modules
moved("ModuleA.ComponentX", "ModuleB.ComponentX")
# Net remapping for position comments
moved("AN_OLD_FILTERED_VCC_VCC", "FILTERED_VCC_VCC")
```
### io(name, type, checks=None, default=None, optional=False)
Declares a net or interface input for a module.
```python
# Required net input
vcc = io("vcc", Net)
# Optional interface input with default
PowerInterface = interface(vcc = Net, gnd = Net)
power = io("power", PowerInterface, optional=True)
# With explicit default
data = io("data", Net, default=Net("DATA"))
# With validation checks (single function)
def check_voltage_within(within):
def check_fn(power_value):
voltage = power_value.voltage
check(voltage.min >= within.min and voltage.max <= within.max,
f"Voltage {voltage} not within {within}")
return check_fn
vbat = io("VBAT", Power, check_voltage_within(VoltageRange("10–30 V")))
# Or with multiple checks as a list
vbat = io("VBAT", Power, [check_voltage_within(VoltageRange("10–30 V")), check_current()])
# Or as a named argument
vbat = io("VBAT", Power, checks=check_voltage_within(VoltageRange("10–30 V")))
```
**Parameters:**
- `name`: String identifier for the input (positional)
- `type`: Expected type - `Net` or interface type (positional)
- `checks`: Optional check function or list of check functions to run on the value at eval time (3rd positional or named)
- `default`: Default value if not provided by parent (named)
- `optional`: If True, returns None when not provided (unless default is specified) (named)
### config(name, type, default=None, convert=None, optional=False)
Declares a configuration value input for a module.
```python
# String configuration
prefix = config("prefix", str, default="U")
# Integer with conversion
baudrate = config("baudrate", int, convert=int)
# Enum configuration
Direction = enum("NORTH", "SOUTH", "EAST", "WEST")
heading = config("heading", Direction)
# Optional configuration
debug = config("debug", bool, optional=True)
```
**Parameters:**
- `name`: String identifier for the input
- `type`: Expected type (str, int, float, bool, enum, or record type)
- `default`: Default value if not provided
- `convert`: Optional conversion function
- `optional`: If True, returns None when not provided (unless default is specified)
### File(path)
Resolves a file or directory path using the load resolver.
```python
# Get absolute path to a file
config_path = File("./config.json")
# Works with load resolver syntax
stdlib_path = File("@stdlib/components")
```
### Path(path, allow_not_exist=False)
Advanced path resolution supporting all LoadSpec formats with optional existence checking.
```python
# Basic path resolution (same as File)
config_path = Path("./config.json")
# LoadSpec format support
stdlib_path = Path("@stdlib/components")
github_path = Path("@github/user/repo:branch/path.zen")
# Allow non-existent paths (only works with local paths)
optional_file = Path("./optional.zen", allow_not_exist=True)
```
**Parameters:**
- `path`: String path to resolve (supports any LoadSpec format)
- `allow_not_exist`: Optional boolean (default: False). If True, allows non-existent paths. Can only be used with local path LoadSpecs, not package/GitHub/GitLab specs.
### error(msg)
Raises a runtime error with the given message.
```python
if not condition:
error("Condition failed")
```
### check(condition, msg)
Checks a condition and raises an error if false.
```python
check(voltage > 0, "Voltage must be positive")
check(len(pins) == 8, "Expected 8 pins")
```
### add_property(name, value)
Adds a property to the current module instance.
```python
add_property("layout_group", "power_supply")
add_property("critical", True)
```
### builtin.net(type_name, **fields)
**Built-in function** that creates custom typed net constructors with optional field parameters.
```python
# Create a basic net type (equivalent to the builtin Net)
Net = builtin.net("Net")
# Create a typed net with field() specifications (with defaults)
Power = builtin.net("Power",
voltage=field(str, "3.3V"),
max_current=field(int, 1000)
)
# Create a typed net with direct type constructors (no defaults)
Signal = builtin.net("Signal", frequency=int, slew_rate=str)
# Create a typed net with physical value fields
load("@stdlib/units.zen", "Voltage", "VoltageRange")
Supply = builtin.net("Supply",
voltage=Voltage,
range=VoltageRange
)
# Create a typed net with enum fields
Level = enum("LOW", "HIGH")
DigitalSignal = builtin.net("DigitalSignal", level=Level)
```
**Parameters:**
- `type_name`: String name for the net type (required, positional)
- `**fields`: Keyword arguments defining optional typed fields
- Each field can be a `field()` spec (with default), a type constructor (str/int/float/bool), an enum type, or a physical value type
**Field Specifications:**
Fields accept any of these specifications:
- `field(type, default)` - Typed field with a default value (from starlark's builtin `field()`)
- `str`, `int`, `float`, `bool` - Direct type constructors (no default)
- `EnumType` - Enum type created with `enum()`
- `Voltage`, `Current`, etc. - Physical value types from stdlib
**Usage:**
All fields are **optional** during net instantiation - you can create a net without providing any field values:
```python
Power = builtin.net("Power", voltage=field(str, "3.3V"), max_current=field(int, 1000))
# Provide all fields
vcc = Power("VCC", voltage="5V", max_current=2000)
# Provide some fields (others get defaults from field() specs)
vdd = Power("VDD", voltage="3.3V") # max_current uses default 1000
# Provide no fields (all use defaults from field() specs)
v3v3 = Power("3V3") # voltage="3.3V", max_current=1000
# Access field values as attributes
print(vcc.voltage) # "5V"
print(vcc.max_current) # 2000
```
**Field Access:**
- Fields can be accessed as attributes on net instances: `net.field_name`
- If a field was not provided and has no default, accessing it will fail
- Fields are stored in the net's properties and can be introspected
**Type Validation:**
- Field values are validated at net instantiation time using starlark's type system
- Type mismatches produce clear error messages:
```python
Power = builtin.net("Power", voltage=str)
vcc = Power("VCC", voltage=123) # Error: Field 'voltage' has wrong type: expected str, got int
```
- Validation works uniformly for all type specifications: builtin types, field() wrappers, enums, and custom types
**Typical Use Cases:**
```python
# Power nets with voltage specifications
Power = builtin.net("Power", voltage=field(str, "3.3V"))
vcc_3v3 = Power("VCC_3V3", voltage="3.3V")
vcc_5v = Power("VCC_5V", voltage="5V")
# Clock signals with frequency metadata
Clock = builtin.net("Clock", frequency=int)
main_clk = Clock("MAIN_CLK", frequency=8000000)
rtc_clk = Clock("RTC_CLK", frequency=32768)
# Digital signals with logic level
Level = enum("1V8", "3V3", "5V")
Gpio = builtin.net("Gpio", level=Level)
gpio_pin = Gpio("GPIO_1", level=Level("3V3"))
# Power rails with electrical specifications
load("@stdlib/units.zen", "Voltage", "Current", "unit")
PowerRail = builtin.net("PowerRail",
nominal_voltage=Voltage,
max_current=Current
)
main_rail = PowerRail("MAIN_RAIL",
nominal_voltage=unit("3.3V", Voltage),
max_current=unit("2A", Current)
)
```
**Returns:** A callable net type constructor that creates typed net instances
### builtin.physical_range(unit)
**Built-in function** that creates unit-specific range constructor types for defining bounded physical value ranges.
```python
# Create range constructors for different units
VoltageRange = builtin.physical_range("V")
CurrentRange = builtin.physical_range("A")
ResistanceRange = builtin.physical_range("Ohm")
CapacitanceRange = builtin.physical_range("F")
InductanceRange = builtin.physical_range("H")
FrequencyRange = builtin.physical_range("Hz")
TemperatureRange = builtin.physical_range("K")
TimeRange = builtin.physical_range("s")
PowerRange = builtin.physical_range("W")
# Use the constructors to create ranges
input_voltage = VoltageRange("2.7V to 5.5V")
output_current = CurrentRange("0A to 3A")
operating_temp = TemperatureRange("-40C to 85C")
# All construction patterns are supported
supply = VoltageRange("11–26 V (12 V nom.)") # String with nominal
tolerance = VoltageRange("15V 10%") # Tolerance expansion
explicit = VoltageRange(min=11, nominal=16, max=26) # Keyword arguments
from_value = VoltageRange(Voltage(15)) # From PhysicalValue
mixed = VoltageRange("11V to 26V", nominal="16V") # Mixed modes
```
**Parameters:**
- `unit`: String identifier for the physical unit (e.g., `"V"`, `"A"`, `"Ohm"`, `"F"`, `"H"`, `"Hz"`, `"K"`, `"s"`, `"W"`)
**Returns:** A `PhysicalRangeType` that can be called to create `PhysicalRange` instances
**Standard Usage:**
This builtin is typically accessed through `@stdlib/units.zen`, which provides pre-defined range constructors:
```python
load("@stdlib/units.zen", "VoltageRange", "CurrentRange", "ResistanceRange")
# Use standard constructors from stdlib
input_range = VoltageRange("3.3V to 5V")
load_range = CurrentRange("100mA to 500mA")
pullup_range = ResistanceRange("4.7kOhm to 10kOhm")
```
**Available in stdlib/units.zen:**
- `VoltageRange` - Voltage ranges (V)
- `CurrentRange` - Current ranges (A)
- `ResistanceRange` - Resistance ranges (Ω)
- `CapacitanceRange` - Capacitance ranges (F)
- `InductanceRange` - Inductance ranges (H)
- `FrequencyRange` - Frequency ranges (Hz)
- `TemperatureRange` - Temperature ranges (K)
- `TimeRange` - Time ranges (s)
- `PowerRange` - Power ranges (W)
**Construction Patterns:**
The returned range constructor accepts flexible input formats:
```python
# String parsing with range separators
VoltageRange("1.1–3.6V") # En-dash separator
VoltageRange("11V to 26V") # Word "to" separator
VoltageRange("11–26V") # Left side can omit unit
# Nominal value in parentheses
VoltageRange("11–26 V (12 V nom.)")
# Tolerance expansion
VoltageRange("15V 10%") # Expands to 13.5–16.5 V
# From PhysicalValue
VoltageRange(Voltage(15)) # Single point: 15–15 V
VoltageRange(Voltage(15, 0.1)) # With tolerance: 13.5–16.5 V
# Keyword arguments (all must be present)
VoltageRange(min=11, max=26)
VoltageRange(min=11, nominal=16, max=26)
# Mixed string/number keywords
VoltageRange(min="11V", nominal="16", max="26V")
# Override nominal separately
VoltageRange("11V to 26V", nominal="16V")
```
**Type Safety:**
- Units must be consistent: `VoltageRange("5V to 3A")` → Error (mixing V and A)
- Range types are distinct: Cannot pass a `VoltageRange` where `CurrentRange` is expected
- Type checking enforces correctness at compile time
**Use Cases:**
Physical ranges are used to specify operating characteristics, constraints, and requirements:
```python
# Component specifications
input_voltage = VoltageRange("2.7V to 5.5V")
output_current = CurrentRange("0A to 3A (1.5A nom.)")
operating_temp = TemperatureRange("-40C to 85C")
# Load requirements
supply_current = CurrentRange("10mA to 100mA")
clock_frequency = FrequencyRange("1MHz to 100MHz")
# Tolerance specifications
capacitance = CapacitanceRange("100nF 10%") # 90nF to 110nF
resistance = ResistanceRange("10kOhm 5%") # 9.5kΩ to 10.5kΩ
# Future: Electrical Rule Checking (ERC)
# Validate that supply ranges satisfy load requirements
# check(supply_voltage_range.contains(load_voltage_range))
```
See [PhysicalRange](#physicalrange) in Core Types for detailed information about the returned range values.
### builtin.add_board_config(name, config, default=False)
**Built-in function** for registering board configurations with the layout system.
**Parameters:**
- `name`: String identifier for the board configuration
- `config`: `BoardConfig` object containing design rules and stackup
- `default`: If True, this becomes the default board config for the project
This builtin is typically called through the stdlib `Board()` function rather than directly.
### builtin.add_electrical_check(name, check_fn, inputs=None)
**Built-in function** for registering electrical validation checks that run during `pcb build`.
**Parameters:**
- `name`: String identifier for the check (required)
- `check_fn`: Function to execute for validation (required)
- `inputs`: Optional dictionary of input parameters to pass to the check function
Electrical checks use lazy evaluation - they are registered during module evaluation but execute after the design is fully evaluated. This allows checks to validate electrical properties, design rules, or other constraints across the entire design.
The check function receives the module as its first argument, followed by any specified inputs as keyword arguments:
```python
def voltage_range_check(module, min_voltage, max_voltage):
"""Check that supply voltage is within acceptable range"""
supply = module.supply_voltage
if supply < min_voltage or supply > max_voltage:
error("Supply voltage {} is outside range {}-{}".format(
supply, min_voltage, max_voltage
))
builtin.add_electrical_check(
name="supply_voltage_range",
check_fn=voltage_range_check,
inputs={
"min_voltage": 3.0,
"max_voltage": 5.5,
}
)
```
**Check Function Signature:**
```python
def check_function(module, **kwargs):
# Validation logic
# Raise error() or fail assertion to indicate failure
pass
```
**Example - Basic Check:**
```python
def check_no_floating_nets(module):
"""Ensure all nets are connected to at least 2 pins"""
for net in module.nets:
if len(net.pins) < 2:
error("Net '{}' is floating (only {} pin connected)".format(
net.name, len(net.pins)
))
builtin.add_electrical_check(
name="no_floating_nets",
check_fn=check_no_floating_nets
)
```
**Example - Parameterized Check:**
```python
def check_power_capacity(module, max_current):
"""Verify power supply can handle load current"""
total_load = sum([c.max_current for c in module.components])
if total_load > max_current:
error("Total load current {}A exceeds supply capacity {}A".format(
total_load, max_current
))
builtin.add_electrical_check(
name="power_capacity",
check_fn=check_power_capacity,
inputs={"max_current": 3.0}
)
```
**Execution Model:**
1. Checks are registered during module evaluation via `builtin.add_electrical_check()`
2. Check functions and inputs are stored as frozen values
3. During `pcb build`, after evaluation completes, all checks are collected from the module tree
4. Each check executes with a fresh evaluator context
5. Check failures generate error diagnostics that are reported through the standard diagnostic system
6. Build fails if any electrical checks fail
**Notes:**
- Checks run only during `pcb build`, not during `pcb test` (use `TestBench` for test-specific validation)
- Check failures generate error-level diagnostics
- Checks can access the entire module structure, including components, nets, interfaces, and properties
- Multiple checks with the same name are allowed (they execute independently)
## Board Configuration Types
The board configuration record types are provided by `@stdlib/board_config.zen` and offer comprehensive control over PCB manufacturing specifications.
**Available Records and Functions:**
```python
load("@stdlib/board_config.zen",
"Board", # Board configuration function
"BoardConfig", # Main configuration container
"DesignRules", # Design rule constraints
"Stackup", # Layer stackup definition
"Material", # Material properties
"CopperLayer", # Copper layer specification
"DielectricLayer", # Dielectric layer specification
"Constraints", # Manufacturing constraints
"Copper", # Copper-specific rules
"NetClass", # Netclass definitions
"merge_configs", # Configuration merging utility
"deep_merge" # Deep merge utility
)
```
### Board(name, layout_path, config=None, layers=None, layout_hints=None, default=False)
**Stdlib function** for declaring board configurations for PCB layout generation.
```python
# Load from stdlib
load("@stdlib/board_config.zen", "Board", "BoardConfig", "DesignRules", "Stackup", "Material", "CopperLayer", "DielectricLayer")
# Simple usage with predefined layer count (recommended for most projects)
Board(
name="MyBoard",
layout_path="layout/",
layers=4,
default=True
)
# Advanced usage with custom configuration
config = BoardConfig(
stackup=Stackup(
materials=[
Material(
name="FR-4",
vendor="Isola",
relative_permittivity=4.21,
loss_tangent=0.025,
reference_frequency=1e9,
),
],
silk_screen_color="#44805BFF",
solder_mask_color="#191919E6",
thickness=1.605,
symmetric=True,
copper_finish="ENIG",
layers=[
CopperLayer(thickness=0.035, role="mixed"),
DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
CopperLayer(thickness=0.035, role="power"),
DielectricLayer(thickness=1.065, material="FR-4", form="core"),
CopperLayer(thickness=0.035, role="power"),
DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
CopperLayer(thickness=0.035, role="mixed"),
],
)
)
# Declare board using custom configuration
Board(
name="MyBoard",
layout_path="layout/",
config=config,
default=True
)
```
**Parameters:**
- `name`: String identifier for the board configuration (required)
- `layout_path`: Directory path where layout files will be generated (required)
- `config`: `BoardConfig` record containing design rules and stackup (optional, mutually exclusive with `layers`)
- `layers`: Number of layers - 2, 4, 6, or 8 (optional, mutually exclusive with `config`). When specified, uses predefined stackups from the stdlib:
- `layers=2`: 2-layer board (1.6mm, 1oz copper, SIG/SIG)
- `layers=4`: 4-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/PWR/SIG)
- `layers=6`: 6-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/(SIG/PWR)/(SIG/PWR)/GND/SIG)
- `layers=8`: 8-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/SIG/PWR/GND/SIG/GND/SIG)
- `layout_hints`: Optional list of layout optimization hints
- `default`: If True, this becomes the default board config for the project
**Note:** Either `config` or `layers` must be provided, but not both. If neither is provided, an error will be raised.
**Implementation:** The stdlib `Board()` function calls the language builtin `builtin.add_board_config()` and sets layout properties.
**BoardConfig Structure:**
```python
BoardConfig = record(
design_rules=DesignRules, # PCB design rules and constraints
stackup=Stackup, # Layer stackup and materials
num_user_layers=int, # Number of User.N layers (default: 4)
spec=str | None, # Optional path to specification file
)
```
**Parameters:**
- `design_rules`: Optional PCB design rules and constraints
- `stackup`: Optional layer stackup and materials configuration
- `num_user_layers`: Number of User.N layers to create (default: 4)
- `spec`: Optional path to a specification markdown file. Use `Path()` to resolve the path:
```python
BoardConfig(spec=Path("./docs/my-spec.md"))
```
**Design Rules Examples:**
```python
# Copper constraints (from @stdlib/board_config.zen)
Copper(
minimum_clearance=0.1, # mm
minimum_track_width=0.1, # mm
minimum_via_diameter=0.2, # mm
copper_to_edge_clearance=0.3, # mm
)
# Netclasses for different signal types
NetClass(
name="Power",
clearance=0.25, # mm
track_width=0.6, # mm
via_diameter=0.8, # mm
color="#FF0000",
)
```
**Stackup Configuration Examples:**
```python
# Material definitions (from @stdlib/board_config.zen)
Material(
name="FR-4",
vendor="Isola",
relative_permittivity=4.21, # Dielectric constant
loss_tangent=0.025, # Loss tangent
reference_frequency=1e9, # Hz
)
# Layer definitions (from @stdlib/board_config.zen)
CopperLayer(thickness=0.035, role="signal") # "signal", "power", "mixed"
DielectricLayer(thickness=1.6, material="FR-4", form="core") # "core", "prepreg"
```
**Configuration Merging (Stdlib Utilities):**
The stdlib provides utilities for merging multiple board configurations:
```python
load("@stdlib/board_config.zen", "merge_configs", "deep_merge")
base_config = BoardConfig(stackup=my_stackup)
tight_rules = BoardConfig(design_rules=tight_constraints)
# Merge multiple configs using stdlib utilities
final_config = merge_configs(base_config, tight_rules)
# Use built-in Board() with merged config
Board("Production", final_config, "layout/", default=True)
```
### TestBench(name, module, checks)
Creates a test bench for validating module connectivity and properties without requiring inputs.
```python
# Load a module to test
MyCircuit = Module("./my_circuit.zen")
# Define check functions
def verify_power_connections(module):
"""Ensure all power pins are connected to VCC"""
vcc_connections = module.nets.get("VCC", [])
check(len(vcc_connections) >= 2, "Need at least 2 VCC connections")
def verify_ground_plane(module):
"""Check for proper ground connections"""
check("GND" in module.nets, "Missing GND net")
check(len(module.nets["GND"]) > 3, "GND net needs more than 3 connections")
# Create test bench
TestBench(
name = "PowerTest",
module = MyCircuit,
checks = [verify_power_connections, verify_ground_plane]
)
```
**Parameters:**
- `name`: String identifier for the test bench
- `module`: Module instance to test (created with `Module()`)
- `checks`: List of check functions to execute
**Check Function Signature:**
Check functions receive a single `Module` argument containing circuit data:
```python
def check_function(module: Module):
# Access circuit data through the Module
nets = module.nets # Map of net names to connected port tuples
components = module.components # Map of component paths to component objects
# Access components via indexing (new preferred method)
component = module["ComponentName"] # Direct component access
submodule = module["SubmoduleName"] # Direct submodule access
nested = module["SubmoduleName"]["ComponentName"] # Chained access
# Access component attributes
component_pins = component.pins # Component pin connections
component_type = component.type # Component type (if available)
component_props = component.properties # Component properties dict
# Perform validation - signal failures using check() or error()
check(condition, "Failure message") # Throws error if condition is False
```
**Module Contents:**
- `module.nets`: Maps each net name to a list of connected port tuples (e.g., `{"VCC": [("U1", "VDD"), ("C1", "P1")]}`)
- `module.components`: Maps component paths to component objects (e.g., `{"U1.IC": <Component>, "C1.C": <Component>}`)
- `module["name"]`: Direct indexing access to child components and submodules by name
**Check Function Behavior:**
- Check functions should use `check(condition, message)` or `error(message)` to signal failures
- Any unhandled error or exception in a check function is treated as a test failure
- Check functions do not need to return any specific value
- Use `print()` for informational output during testing
**TestBench Behavior:**
- Evaluates the module with relaxed input requirements (missing required inputs are allowed)
- Stores check functions for deferred execution (checks run later when using `pcb test` command)
- Returns a TestBench value containing test cases and deferred checks
- When executed by `pcb test`: runs each check function in order, reports failures as diagnostics with precise source location pointing to the failing `check()` call, and prints a success message if all checks pass
**Example with Modern Indexing Syntax:**
```python
def check_power_connections(module: Module):
"""Verify power IC connections using modern indexing syntax"""
# Access submodule and then component within it (chained syntax)
power_ic = module["PowerIC"]["LDO"]
# Equivalent using nested path syntax
power_ic_nested = module["PowerIC.LDO"]
# Both approaches return the same component
check(power_ic.name == power_ic_nested.name, "Chained and nested path syntax should be equivalent")
# Check that power pins are connected properly
check("VCC" in power_ic.pins["VIN"].name, "LDO input not connected to VCC")
check("GND" in power_ic.pins["GND"].name, "LDO ground not connected")
print(f"✓ {power_ic.name} power connections verified")
def check_capacitor_placement(module: Module):
"""Verify bypass capacitors are properly placed"""
# Access capacitor components by name using both syntaxes
c1 = module["C1"]["C"] # Chained syntax
c2 = module["C2.C"] # Nested path syntax
# Verify they're connected to power and ground
check("VCC" in c1.pins["P1"].name, "C1 not connected to power")
check("GND" in c1.pins["P2"].name, "C1 not connected to ground")
check("VCC" in c2.pins["P1"].name, "C2 not connected to power")
check("GND" in c2.pins["P2"].name, "C2 not connected to ground")
print(f"✓ Capacitors {c1.name} and {c2.name} properly placed")
def check_traditional_syntax(module: Module):
"""Example using traditional components dict access"""
for comp_path, component in module.components.items():
if component.type == "capacitor":
check(hasattr(component, 'capacitance'), f"{comp_path} missing capacitance")
print(f"✓ {comp_path} capacitance: {component.capacitance}")
def check_net_connectivity(module: Module):
"""Verify net connections using nets attribute"""
check("VCC" in module.nets, "Missing VCC power net")
check("GND" in module.nets, "Missing GND net")
vcc_connections = len(module.nets["VCC"])
check(vcc_connections >= 2, f"VCC only has {vcc_connections} connections, need at least 2")
print(f"✓ VCC net has {vcc_connections} connections")
def check_component_presence(module: Module):
"""Use membership operator to check component existence"""
# Check if components exist before accessing them
if "PowerIC" in module:
power_ic = module["PowerIC"]
print(f"✓ Found PowerIC submodule: {power_ic}")
# Check nested component existence
if "PowerIC.LDO" in module:
ldo = module["PowerIC.LDO"]
print(f"✓ Found LDO component: {ldo.name}")
else:
error("LDO component not found in PowerIC")
else:
print("PowerIC not present in this module")
# Check multiple components at once
required_caps = ["C1", "C2", "C3"]
missing_caps = [cap for cap in required_caps if cap not in module]
if missing_caps:
error(f"Missing required capacitors: {missing_caps}")
else:
print("✓ All required capacitors present")
TestBench(
name = "PowerCircuitValidation",
module = MyPowerCircuit,
checks = [
check_power_connections, # Uses modern indexing syntax
check_capacitor_placement, # Uses chained indexing
check_traditional_syntax, # Uses traditional components dict
check_net_connectivity, # Uses nets attribute
check_component_presence, # Uses membership operator
]
)
```
## Circuit Graph Analysis & Path Validation
### Overview
Zener provides circuit graph analysis for validating module connectivity and topology. The system converts circuit schematics into searchable graphs, enabling path finding between component pins and verification of component sequences.
The graph analysis operates on the **public interface paradigm**: path finding works between **component ports** (specific IC pins) and **external nets** (module's io() declarations), while automatically discovering internal routing paths.
### Core Concepts
#### Circuit Graph
Every module automatically generates a circuit graph that models component connectivity:
```python
def analyze_circuit(module: Module):
# Get the circuit graph from any module
graph = module.graph()
# Find paths between public interface points
paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND", max_depth=5)
```
#### Public Interface Boundaries
Path finding operates between two types of well-defined endpoints:
**Component Ports**: Specific pins on specific components
```python
("ComponentName", "PinName") # e.g., ("TPS82140", "VIN"), ("STM32", "PA0")
```
**External Nets**: Public nets exposed by the module's io() declarations
```python
"NetName" # e.g., "GND_GND", "VCC_VCC", "SPI_CLK"
```
### Path Finding API
#### graph.paths(start, end, max_depth=10)
Finds all simple paths between two points in the circuit:
```python
def validate_power_supply(module: Module):
graph = module.graph()
# IC pin to external net
input_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
# IC pin to IC pin (internal feedback path)
feedback_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
# External net to IC pin
enable_paths = graph.paths(start="EN_EN", end=("TPS82140", "EN"))
```
**Parameters:**
- `start`: Component port tuple `("Component", "Pin")` or external net name
- `end`: Component port tuple `("Component", "Pin")` or external net name
- `max_depth`: Maximum number of components to traverse (default: 10)
**Returns:** List of Path objects
#### Path Objects
Each path contains discovered connectivity information:
```python
def analyze_path(path):
print("Path details:")
print(" Ports:", path.ports) # List of (component, pin) tuples traversed
print(" Components:", len(path.components)) # Components in the path
print(" Nets:", path.nets) # Net names traversed
```
### Path Validation Methods
#### Basic Validation
```python
# Count components matching predicate
resistor_count = path.count(is_resistor)
# Ensure at least one component matches
path.any(is_capacitor)
# Ensure all components match
path.all(is_passive_component)
# Ensure no components match
path.none(is_active_component)
```
#### Sequential Pattern Matching
The `path.matches()` method validates component sequences in order:
```python
def validate_filter_topology(module: Module):
graph = module.graph()
filter_paths = graph.paths(start=("OpAmp", "OUT"), end="GND_GND")
# Validate exact component sequence
filter_paths[0].matches(
is_resistor("1kOhm"), # Series resistor
is_capacitor("100nF"), # Filter capacitor
is_resistor("10kOhm") # Load resistor
)
```
### Design Principles
#### Datasheet Requirements Translation
Circuit validation can directly implement datasheet requirements by mapping component pin constraints to path validation:
**Power Supply Decoupling**
```python
def validate_vin_decoupling(module: Module):
"""Validate VIN decoupling per datasheet Figure 8-1"""
graph = module.graph()
# Find all paths from VIN pin to ground
vin_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
# Datasheet requirement: "≥10µF bulk + 100nF ceramic"
vin_paths.any(path.matches(is_capacitor("10uF"))) # Bulk capacitor
vin_paths.any(path.matches(is_capacitor("100nF"))) # Bypass capacitor
print("✓ VIN decoupling meets datasheet requirements")
// Reference: Datasheet §8.2.1, Figure 8-1
```
**Feedback Networks**
```python
def validate_feedback_divider(module: Module):
"""Validate feedback network per datasheet Section 8.3"""
graph = module.graph()
# Find path from output to feedback pin
fb_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
# Datasheet requirement: "Precision resistor divider"
fb_paths[0].matches(
is_resistor(), # Upper feedback resistor
is_resistor() # Lower feedback resistor
)
print("✓ Feedback divider topology validated")
// Reference: Datasheet §8.3.2, Equation 1
```
**Bootstrap Circuits**
```python
def validate_bootstrap_cap(module: Module):
"""Validate bootstrap capacitor per datasheet Figure 8-4"""
graph = module.graph()
# Bootstrap cap connects BOOT pin to SW pin
boot_paths = graph.paths(start=("TPS82140", "BOOT"), end=("TPS82140", "SW"))
# Datasheet requirement: "100nF ceramic, X7R dielectric"
boot_paths[0].matches(is_capacitor("100nF", dielectric="X7R"))
print("✓ Bootstrap capacitor meets specifications")
// Reference: Datasheet §8.4.1, Figure 8-4
```
#### Public Interface Paradigm
**Start/End Points**: Always use the module's "public" interface:
- **Component ports**: Known IC pins from the main component
- **External nets**: Public nets from io() declarations
**Internal Discovery**: Path finding automatically traverses internal components and nets
**Benefits**:
- **Deterministic scope**: Clear rule boundaries
- **Implementation freedom**: Internal routing flexibility
- **Hierarchical composability**: Rules work at any module level
- **Performance**: Constrained search space
#### Validation Strategies
**Path Existence**: Verify required connections exist
```python
# "VIN must connect to GND through decoupling"
vin_paths = graph.paths(start=("IC", "VIN"), end="GND_GND")
check(len(vin_paths) > 0, "VIN decoupling missing")
```
**Topology Validation**: Verify exact component sequences
```python
# "Output filter must be L-C configuration"
filter_paths = graph.paths(start=("IC", "SW"), end=("IC", "VOUT"))
filter_paths[0].matches(is_inductor(), is_capacitor())
```
**Alternative Paths**: Handle multiple valid implementations
```python
# "Either RC or LC filter acceptable"
filter_paths = graph.paths(start=("IC", "OUT"), end="LOAD_LOAD")
rc_valid = any(path.matches(is_resistor(), is_capacitor()) for path in filter_paths)
lc_valid = any(path.matches(is_inductor(), is_capacitor()) for path in filter_paths)
check(rc_valid or lc_valid, "Filter topology required")
```
**Error Suppression**: Use `suppress_errors=True` for path identification
```python
# Find all paths matching a pattern without failing
matching_paths = [p for p in all_paths if p.matches(
is_resistor(), is_capacitor(), suppress_errors=True
)]
check(len(matching_paths) > 0, "No RC filter found")
```
### path.matches() API Reference
#### Syntax
```python
path.matches(*matchers, suppress_errors=False)
```
**Parameters:**
- `*matchers`: Sequential matcher functions to apply in order
- `suppress_errors`: If True, returns False on validation failure instead of raising errors
#### Sequential Processing Model
- Matchers consume components sequentially using cursor-based processing
- Each matcher receives `(path, cursor_index)` and returns components consumed
- Validation fails if any matcher fails or if components remain after all matchers
#### Built-in Matcher Functions
```python
# Basic component type matchers
is_resistor(expected_value=None) # Match resistor, optionally with specific value
is_capacitor(expected_value=None) # Match capacitor, optionally with specific value
is_inductor(expected_value=None) # Match inductor, optionally with specific value
# Navigation matchers
skip(n) # Skip exactly n components
skip_rest() # Consume all remaining components
# Quantified matchers
exactly_n_resistors(n) # Exactly n consecutive resistors
at_least_n_capacitors(n) # At least n consecutive capacitors
at_most_n_components(n) # At most n components of any type
# Conditional matching
any_of(matcher1, matcher2, ...) # Match any of the provided matchers
skip_until(matcher) # Skip components until matcher succeeds
contains_somewhere(matcher) # Matcher succeeds somewhere in remaining path
# Component properties
has_package(size) # Component with specific package size
name_contains(pattern) # Component name contains pattern
```
#### Custom Matcher Functions
```python
def custom_matcher(path, cursor):
"""Custom matcher function template"""
if cursor >= len(path.components):
error("path ended, expected component")
component = path.components[cursor]
# Validation logic here
check(component.type == "resistor", "Expected resistor")
return 1 # Consume 1 component, advance cursor
```
## Module System
### Module Definition
A module is defined by a `.zen` file that declares its inputs and creates components:
```python
# voltage_divider.zen
# Declare inputs
vin = io("vin", Net)
vout = io("vout", Net)
gnd = io("gnd", Net)
r1_value = config("r1", str, default="10k")
r2_value = config("r2", str, default="10k")
# Define a resistor symbol (could also load from library)
resistor_symbol = Symbol(
definition = [
("1", ["1"]),
("2", ["2"])
]
)
# Create components
Component(
name = "R1",
type = "resistor",
footprint = "0402",
symbol = resistor_symbol,
pins = {"1": vin, "2": vout},
properties = {"value": r1_value}
)
Component(
name = "R2",
type = "resistor",
footprint = "0402",
symbol = resistor_symbol,
pins = {"1": vout, "2": gnd},
properties = {"value": r2_value}
)
```
### Module Instantiation
```python
# Load the module
VDivider = Module("./voltage_divider.zen")
# Create instances
VDivider(
name = "divider1",
vin = Net("INPUT"),
vout = Net("OUTPUT"),
gnd = Net("GND"),
r1 = "100k",
r2 = "47k"
)
```