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/review-routed-dump-pulse.py
"""Conditional pulse screen of emitted dump wires in board-world mm, +Y up.
Uses minimum taper cross sections without heat-spreading credit. It does not
qualify via barrels, pad current crowding, repeated pulses or continuous loads.
"""
import hashlib
import json
from pathlib import Path
from copper_pulse_geometry import adiabatic_trace_rise
from power_copper_geometry import measure_trace_wire_segments
artifact = Path("dist/index/circuit.json").read_bytes()
artifact_sha256 = hashlib.sha256(artifact).hexdigest()
elements = json.loads(artifact)
if sum(element["type"] == "source_component" for element in elements) != 140 or not any(element["type"] == "pcb_trace" for element in elements):
raise ValueError("Pulse review requires the complete140-part routed artifact")
checks = json.loads(Path("evidence/native-routed-checks-A22.json").read_text())
connectivity = json.loads(Path("evidence/schematic-connectivity-audit-A22.json").read_text())
power_paths = json.loads(Path("evidence/routed-power-paths-A22.json").read_text())
for report in [checks, connectivity, power_paths]:
if report["artifactSha256"] != artifact_sha256:
raise ValueError("Pulse review requires identical routed-artifact evidence")
if checks["purchased"] != 140 or checks["traces"] <= 0 or checks["checks"] or connectivity["issues"]:
raise ValueError("Pulse review requires the complete checked routed circuit")
manifest = json.loads(Path("docs/design-manifest.json").read_text())
branches = {
"DUMP_MID_A": ("R45", "R46"),
"DUMP_MID_B": ("R47", "R48"),
"DUMP_MID_C": ("R69", "R70"),
"DUMP_MID_D": ("R71", "R72"),
}
for mid_net, (first_ref, second_ref) in branches.items():
first = next(part for part in manifest if part["ref"] == first_ref)
second = next(part for part in manifest if part["ref"] == second_ref)
if first["code"] != "C2991665" or second["code"] != "C2991665":
raise ValueError("Pulse branch supplier changed; reassess resistor bounds")
if first["pins"] != {"pin1": "VM", "pin2": mid_net} or second["pins"] != {"pin1": mid_net, "pin2": "DUMP_LOAD"}:
raise ValueError("Pulse branch topology differs from the four-series-pair bank")
envelope_bytes = Path("evidence/power-prototype-envelope-A22.json").read_bytes()
regeneration = json.loads(envelope_bytes)["regeneration"]
shared_current_a = regeneration["bank"]["maximumSinkCurrentA"]
# All four exact resistor pairs have the same declared minimum resistance.
# V / R_pair_min = I_bank_max / 4, also bounding one low-tolerance branch
# when the other branches have a different tolerance.
branch_current_a = shared_current_a / len(branches)
net_currents_a = {"DUMP_LOAD": shared_current_a, **{net: branch_current_a for net in branches}}
intent = json.loads(Path("evidence/copper-net-map-A22.json").read_text())
net_names_by_intent = {}
for net in elements:
if net["type"] == "source_net":
net_names_by_intent.setdefault(intent[net["source_net_id"]], set()).add(net["name"])
sections_by_net = {net: [] for net in net_currents_a}
for trace in elements:
if trace["type"] != "pcb_trace":
continue
candidates = net_names_by_intent[intent[trace["pcb_trace_id"]]] & net_currents_a.keys()
if not candidates:
continue
if len(candidates) != 1:
raise ValueError("Ambiguous dump-wire net ownership")
net = next(iter(candidates))
for section in measure_trace_wire_segments(trace):
pulse = {
"currentA": net_currents_a[net],
"durationSeconds": regeneration["pulseS"],
"widthMm": section["minimumWidthMm"],
"thicknessUm": section["copperThicknessUm"],
"resistivityOhmMetre": 3e-8,
"densityKgPerCubicMetre": 8800,
"specificHeatJPerKgK": 375,
"widthFactor": .8,
"thicknessFactor": .8,
}
sections_by_net[net].append({
"trace": trace["pcb_trace_id"],
"section": section,
"pulse": pulse,
"screen": adiabatic_trace_rise(pulse),
})
for net, sections in sections_by_net.items():
if not sections:
raise ValueError("Missing emitted dump-wire sections: " + net)
report = {
"revision": "A22",
"artifactSha256": artifact_sha256,
"powerEnvelopeSha256": hashlib.sha256(envelope_bytes).hexdigest(),
"sharedCurrentA": shared_current_a,
"maximumBranchCurrentA": branch_current_a,
"maximumWireRiseKByNet": {net: max(section["screen"]["adiabaticTemperatureRiseK"] for section in sections) for net, sections in sections_by_net.items()},
"sectionsByNet": sections_by_net,
"sharedReturnBound": "Full bank current on every DUMP_LOAD section conservatively overbounds branch-only sections; no current-division credit",
"status": "Conditional wire-only calculation; thermal, barrel, pad-entry, clamp-timing and repetitive braking qualification pending",
"limitations": "Initial stored motor energy remains <=1mJ. The expanded 0.5J/20ms/10s envelope requires physical qualification; this report cannot grant it or establish continuous ampacity. Material/process reserves are engineering assumptions, not manufacturing guarantees.",
}
Path("evidence/routed-dump-pulse-A22.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps({key: report[key] for key in ["artifactSha256", "sharedCurrentA", "maximumBranchCurrentA", "maximumWireRiseKByNet", "status"]}, indent=2))