AnasSarkiz/usb-c-pd-brushed-motor

USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.

Version
0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
License
unset
Stars
0

scripts/power_copper_geometry.py

"""Outgoing PCB wire segment metrics; widths/lengths in board-world millimetres.

Stackup-specific copper thickness, resistivity 2e-8 ohm m at the declared elevated copper temperature.
This isolated segment resistance screen does not establish temperature rise.
"""
import math
import json
from pathlib import Path

STACKUP = json.loads((Path(__file__).resolve().parent.parent / "docs/STACKUP-A31.json").read_text())
COPPER_THICKNESS_UM = {layer["name"]: layer["thicknessUm"] for layer in STACKUP["copperLayers"]}


def measure_wire_segment(segment, options=None):
    first, second = segment
    options = options or {}
    route_thickness_mode = options.get("routeThicknessMode", "constant")
    thickness_um = options.get("copperThicknessUm", 35)
    if not math.isfinite(thickness_um) or thickness_um <= 0:
        raise ValueError("Copper thickness must be finite and positive")
    length_mm = math.hypot(second["x"] - first["x"], second["y"] - first["y"])
    interpolation = first.get("width_interpolation_mode")
    if interpolation is None and route_thickness_mode == "interpolated":
        interpolation = "linear"
    if interpolation not in (None, "linear"):
        raise ValueError("Unsupported width interpolation; do not infer ampacity")
    start_width_mm = first.get("start_width", first["width"])
    end_width_mm = first.get("end_width", second["width"]) if interpolation else start_width_mm
    if not all(math.isfinite(w) and w > 0 for w in (start_width_mm, end_width_mm)):
        raise ValueError("Copper widths must be finite and positive")
    width_change_mm = end_width_mm - start_width_mm
    resistance_ohm = (2e-8 / (thickness_um * 1e-6)) * length_mm * (
        1 / start_width_mm if abs(width_change_mm) < 1e-12
        else math.log(end_width_mm / start_width_mm) / width_change_mm
    )
    if min(start_width_mm, end_width_mm) >= 1:
        length_below_1mm = 0
    elif max(start_width_mm, end_width_mm) <= 1:
        length_below_1mm = length_mm
    else:
        crossing_fraction = (1 - start_width_mm) / width_change_mm
        length_below_1mm = length_mm * (crossing_fraction if width_change_mm > 0 else 1 - crossing_fraction)
    return {
        "lengthMm": length_mm,
        "minimumWidthMm": min(start_width_mm, end_width_mm),
        "lengthBelow1mm": length_below_1mm,
        "resistanceOhm": resistance_ohm,
        "copperThicknessUm": thickness_um,
    }


def measure_trace_wire_segments(trace):
    """Measure all physical wire sections, including wire/barrel boundaries.

    Barrels themselves require a separate via resistance/ampacity screen. A
    taper ending at a barrel must specify its end width; no width is guessed.
    """
    mode = trace.get("route_thickness_mode", "constant")
    if mode not in ("constant", "interpolated"):
        raise ValueError("Unsupported trace thickness mode")
    route = trace["route"]
    if any(point["route_type"] not in ("wire", "via") for point in route):
        raise ValueError("Unsupported power copper primitive")
    measured = []
    for first, second in zip(route, route[1:]):
        if not all(math.isfinite(point[axis]) for point in (first, second) for axis in ("x", "y")):
            raise ValueError("Power copper coordinates must be finite")
        same_position = (first["x"], first["y"]) == (second["x"], second["y"])
        if first["route_type"] == second["route_type"] == "via":
            if same_position:
                continue
            raise ValueError("Power route moves between barrels without wire")
        start_layer = first["layer"] if first["route_type"] == "wire" else first["to_layer"]
        end_layer = second["layer"] if second["route_type"] == "wire" else second["from_layer"]
        if start_layer != end_layer:
            raise ValueError("Power wire changes layer without barrel")
        if same_position:
            continue
        start = first
        end = second
        if first["route_type"] == "via":
            start = {"x": first["x"], "y": first["y"], "width": second.get("start_width", second["width"])}
            # The following wire defines the constant copper at its barrel
            # entrance. Its own taper begins at that wire's outgoing segment.
            segment_mode = "constant"
        else:
            segment_mode = mode
        if second["route_type"] == "via":
            if (first.get("width_interpolation_mode") or mode == "interpolated") and "end_width" not in first:
                raise ValueError("Taper ending at barrel lacks end width")
            end = {"x": second["x"], "y": second["y"], "width": first.get("end_width", first["width"])}
        measured.append({"layer": start_layer, "start": {"x": first["x"], "y": first["y"]}, "end": {"x": second["x"], "y": second["y"]}, **measure_wire_segment([start, end], {"routeThicknessMode": segment_mode, "copperThicknessUm": COPPER_THICKNESS_UM[start_layer]})})
    return measured