Integrate ImpedenceFinder closed-form Z0 into the project Impedance tab.
Use the vendored Hammerstad-Jensen/Cohn engine for microstrip, stripline, and coupled-diff advice. Skip OpenEMS/pcbnew and refuse CPWG rather than inventing a number. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+157
@@ -0,0 +1,157 @@
|
||||
"""Samples a net's routed segments into evenly-spaced points along a
|
||||
continuous distance axis.
|
||||
|
||||
Segments are chained by endpoint coincidence: two segments that share an
|
||||
exact (x, y) point are treated as connected, regardless of layer. This
|
||||
means a via is handled for free -- the segment ending on one layer and the
|
||||
segment starting on the other share the via's exact position, so the
|
||||
distance axis carries straight through without any via-specific code.
|
||||
|
||||
A net with a single point-to-point route becomes one NetBranch. A
|
||||
T-topology net (any point where 3+ segments meet) is split into one branch
|
||||
per spoke leaving that point, each restarting its distance axis at zero
|
||||
there -- callers that want a single unified axis across the whole net will
|
||||
need to stitch branches together themselves; this module only guarantees
|
||||
that each individual branch's axis is correct and continuous.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from impedancefinder.model import NetBranch, Point2D, SamplePoint, TraceSegment
|
||||
|
||||
_COORDINATE_PRECISION_MM = 6 # matches pcbnew's nm-to-mm conversion exactly
|
||||
|
||||
|
||||
def sample_net(segments: tuple[TraceSegment, ...], pitch_mm: float) -> tuple[NetBranch, ...]:
|
||||
"""Sample every branch of a net at pitch_mm, plus each segment's exact
|
||||
endpoint. All segments are assumed to belong to the same net; callers
|
||||
should pre-filter board_model.BoardData.segments by net name first.
|
||||
"""
|
||||
if pitch_mm <= 0:
|
||||
raise ValueError(f"pitch_mm must be positive, got {pitch_mm}")
|
||||
branches = _group_into_branches(segments)
|
||||
return tuple(_sample_branch(branch, pitch_mm) for branch in branches)
|
||||
|
||||
|
||||
def _endpoint_key(point: Point2D) -> tuple[float, float]:
|
||||
return (round(point.x_mm, _COORDINATE_PRECISION_MM), round(point.y_mm, _COORDINATE_PRECISION_MM))
|
||||
|
||||
|
||||
def _build_adjacency(
|
||||
segments: tuple[TraceSegment, ...]
|
||||
) -> dict[tuple[float, float], list[TraceSegment]]:
|
||||
adjacency: dict[tuple[float, float], list[TraceSegment]] = {}
|
||||
for segment in segments:
|
||||
for endpoint in (segment.start, segment.end):
|
||||
adjacency.setdefault(_endpoint_key(endpoint), []).append(segment)
|
||||
return adjacency
|
||||
|
||||
|
||||
def _orient_from(segment: TraceSegment, from_key: tuple[float, float]) -> TraceSegment:
|
||||
if _endpoint_key(segment.start) == from_key:
|
||||
return segment
|
||||
return TraceSegment(
|
||||
net=segment.net, layer=segment.layer, start=segment.end, end=segment.start, width_mm=segment.width_mm
|
||||
)
|
||||
|
||||
|
||||
def _walk_branch(
|
||||
entry_key: tuple[float, float],
|
||||
entry_segment: TraceSegment,
|
||||
adjacency: dict[tuple[float, float], list[TraceSegment]],
|
||||
visited: set,
|
||||
) -> tuple[TraceSegment, ...]:
|
||||
ordered: list[TraceSegment] = []
|
||||
current_key, current_segment = entry_key, entry_segment
|
||||
while True:
|
||||
visited.add(id(current_segment))
|
||||
oriented = _orient_from(current_segment, current_key)
|
||||
ordered.append(oriented)
|
||||
next_key = _endpoint_key(oriented.end)
|
||||
neighbors = [s for s in adjacency[next_key] if id(s) not in visited]
|
||||
if len(neighbors) != 1 or len(adjacency[next_key]) != 2:
|
||||
break
|
||||
current_key, current_segment = next_key, neighbors[0]
|
||||
return tuple(ordered)
|
||||
|
||||
|
||||
def _group_into_branches(segments: tuple[TraceSegment, ...]) -> tuple[tuple[TraceSegment, ...], ...]:
|
||||
# Junctions (degree >= 3) are walked in a full first pass, before any
|
||||
# leaf is considered -- otherwise a leaf reached first in dict-iteration
|
||||
# order would claim a spoke and the branch would start at the leaf
|
||||
# instead of the junction, leaving sibling spokes of the same junction
|
||||
# inconsistently zeroed (one from the leaf, the rest from the junction).
|
||||
adjacency = _build_adjacency(segments)
|
||||
visited: set = set()
|
||||
branches = []
|
||||
for key, segments_at_node in adjacency.items():
|
||||
if len(segments_at_node) >= 3:
|
||||
branches.extend(_walk_unvisited(key, segments_at_node, adjacency, visited))
|
||||
for key, segments_at_node in adjacency.items():
|
||||
if len(segments_at_node) == 1:
|
||||
branches.extend(_walk_unvisited(key, segments_at_node, adjacency, visited))
|
||||
branches.extend(_group_remaining_loops(segments, adjacency, visited))
|
||||
return tuple(branches)
|
||||
|
||||
|
||||
def _walk_unvisited(
|
||||
key: tuple[float, float],
|
||||
segments_at_node: list[TraceSegment],
|
||||
adjacency: dict[tuple[float, float], list[TraceSegment]],
|
||||
visited: set,
|
||||
) -> list[tuple[TraceSegment, ...]]:
|
||||
return [
|
||||
_walk_branch(key, segment, adjacency, visited)
|
||||
for segment in segments_at_node
|
||||
if id(segment) not in visited
|
||||
]
|
||||
|
||||
|
||||
def _group_remaining_loops(
|
||||
segments: tuple[TraceSegment, ...],
|
||||
adjacency: dict[tuple[float, float], list[TraceSegment]],
|
||||
visited: set,
|
||||
) -> tuple[tuple[TraceSegment, ...], ...]:
|
||||
# Anything left unvisited lies entirely on degree-2 nodes -- a pure loop
|
||||
# with no leaf or junction to start from. Walk each remaining loop once,
|
||||
# starting arbitrarily from one of its segments.
|
||||
loops = []
|
||||
for segment in segments:
|
||||
if id(segment) not in visited:
|
||||
loops.append(_walk_branch(_endpoint_key(segment.start), segment, adjacency, visited))
|
||||
return tuple(loops)
|
||||
|
||||
|
||||
def _sample_branch(branch_segments: tuple[TraceSegment, ...], pitch_mm: float) -> NetBranch:
|
||||
samples: list[SamplePoint] = []
|
||||
cumulative_mm = 0.0
|
||||
for segment in branch_segments:
|
||||
samples.extend(_sample_segment(segment, pitch_mm, cumulative_mm))
|
||||
cumulative_mm += segment.length_mm
|
||||
return NetBranch(samples=tuple(samples))
|
||||
|
||||
|
||||
def _sample_segment(
|
||||
segment: TraceSegment, pitch_mm: float, offset_mm: float
|
||||
) -> tuple[SamplePoint, ...]:
|
||||
length_mm = segment.length_mm
|
||||
if length_mm == 0:
|
||||
return (_sample_at(segment, 0.0, offset_mm),)
|
||||
step_count = max(1, int(length_mm // pitch_mm))
|
||||
local_distances = [i * pitch_mm for i in range(step_count + 1) if i * pitch_mm < length_mm]
|
||||
local_distances.append(length_mm)
|
||||
return tuple(_sample_at(segment, distance, offset_mm + distance) for distance in local_distances)
|
||||
|
||||
|
||||
def _sample_at(segment: TraceSegment, local_distance_mm: float, cumulative_distance_mm: float) -> SamplePoint:
|
||||
fraction = 0.0 if segment.length_mm == 0 else local_distance_mm / segment.length_mm
|
||||
position = Point2D(
|
||||
x_mm=segment.start.x_mm + fraction * (segment.end.x_mm - segment.start.x_mm),
|
||||
y_mm=segment.start.y_mm + fraction * (segment.end.y_mm - segment.start.y_mm),
|
||||
)
|
||||
return SamplePoint(
|
||||
net=segment.net,
|
||||
layer=segment.layer,
|
||||
distance_along_net_mm=cumulative_distance_mm,
|
||||
position=position,
|
||||
width_mm=segment.width_mm,
|
||||
)
|
||||
Reference in New Issue
Block a user