main
noah Add datasheet, figures, hero card, provenance, verification log and README a043b0e 10h ago
#!/usr/bin/env python3
"""Measure the 61300411121 3D model (step2glb GLB of the STEP) against the Wurth datasheet drawing.

    measure_model.py <model.glb> <out.json>

The GLB from step2glb is in metres. The two materials are the insulator (dark) and the contacts (gold). Every
dimension the datasheet prints is compared with its tolerance; the verdict per row is pass or fail.
"""
import json, sys
import numpy as np
from pygltflib import GLTF2

glb, out = sys.argv[1], sys.argv[2]
g = GLTF2().load(glb)
blob = g.binary_blob()


def acc(i):
    a = g.accessors[i]
    bv = g.bufferViews[a.bufferView]
    n = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4}[a.type]
    dt = {5126: np.float32, 5125: np.uint32, 5123: np.uint16, 5122: np.int16, 5121: np.uint8, 5120: np.int8}[a.componentType]
    off = (bv.byteOffset or 0) + (a.byteOffset or 0)
    size = n * np.dtype(dt).itemsize
    stride = bv.byteStride or size
    raw = np.frombuffer(blob, np.uint8, stride * (a.count - 1) + size, off)
    out = np.lib.stride_tricks.as_strided(raw, (a.count, size), (stride, 1)).copy().view(dt).reshape(a.count, n).astype(float)
    if a.normalized:  # KHR_mesh_quantization: normalized integers
        out /= {5122: 32767.0, 5120: 127.0, 5123: 65535.0, 5121: 255.0}[a.componentType]
    return out


def world(node_idx, parent=np.eye(4)):
    n = g.nodes[node_idx]
    m = np.eye(4)
    if n.matrix:
        m = np.array(n.matrix).reshape(4, 4).T
    else:
        t = np.eye(4); r = np.eye(4); s = np.eye(4)
        if n.translation: t[:3, 3] = n.translation
        if n.rotation:
            x, y, z, w = n.rotation
            r[:3, :3] = [[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
                         [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
                         [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)]]
        if n.scale: s[0, 0], s[1, 1], s[2, 2] = n.scale
        m = t @ r @ s
    m = parent @ m
    res = []
    if n.mesh is not None:
        res.append((n.mesh, m))
    for c in n.children or []:
        res += world(c, m)
    return res


pts = {}
for root in g.scenes[g.scene or 0].nodes:
    for mesh_i, m in world(root):
        for p in g.meshes[mesh_i].primitives:
            v = acc(p.attributes.POSITION).astype(float)
            v = (np.c_[v, np.ones(len(v))] @ m.T)[:, :3] * 1000.0  # mm
            name = g.materials[p.material].name if p.material is not None else "none"
            col = g.materials[p.material].pbrMetallicRoughness.baseColorFactor if p.material is not None else None
            pts.setdefault((name, tuple(round(c, 3) for c in col[:3]) if col else None), []).append(v)

groups = {}
for (name, col), vs in pts.items():
    v = np.vstack(vs)
    groups[name] = dict(colour=col, n=len(v), min=v.min(0).round(3).tolist(), max=v.max(0).round(3).tolist(), pts=v)

# which axis is up? the longest extent of the gold pins is the pin axis
gold = [k for k, d in groups.items() if d["colour"] and d["colour"][0] > 0.4]
body = [k for k, d in groups.items() if k not in gold]
P = groups[gold[0]]["pts"]; B = groups[body[0]]["pts"]
ext = P.max(0) - P.min(0)
up = int(np.argmax(ext))
axes = "XYZ"
# pin cross-section: vertices in the middle of the mating length (well above the insulator), cluster by position
hi = P[:, up].max()
top_of_body = B[:, up].max()
band = P[(P[:, up] > top_of_body + 0.5) & (P[:, up] < hi - 0.05)]  # full square section, below the tip chamfer
others = [i for i in range(3) if i != up]
# cluster pins along the longer of the two horizontal axes
long_ax = others[int(np.argmax([ext[i] for i in others]))]
short_ax = [i for i in others if i != long_ax][0]
order = np.sort(band[:, long_ax])
splits = np.where(np.diff(order) > 1.0)[0]
edges = [order[0]] + [order[s + 1] for s in splits]
pins = []
for k, e0 in enumerate(edges):
    e1 = order[splits[k]] if k < len(splits) else order[-1]
    sel = band[(band[:, long_ax] >= e0 - 1e-6) & (band[:, long_ax] <= e1 + 1e-6)]
    pins.append(dict(centre=round(float((sel[:, long_ax].min() + sel[:, long_ax].max()) / 2), 3),
                     w_long=round(float(sel[:, long_ax].max() - sel[:, long_ax].min()), 3),
                     w_short=round(float(sel[:, short_ax].max() - sel[:, short_ax].min()), 3)))
centres = [p["centre"] for p in pins]
pitch = [round(abs(b - a), 3) for a, b in zip(centres, centres[1:])]

zb0, zb1 = B[:, up].min(), B[:, up].max()
zp0, zp1 = P[:, up].min(), P[:, up].max()
m = dict(
    up_axis=axes[up],
    body_len=round(float(B[:, long_ax].max() - B[:, long_ax].min()), 3),
    body_wid=round(float(B[:, short_ax].max() - B[:, short_ax].min()), 3),
    body_bottom=round(float(zb0), 3), body_top=round(float(zb1), 3),
    insulator_h=round(float(zb1 - zb0), 3),
    pin_bottom=round(float(zp0), 3), pin_top=round(float(zp1), 3),
    pin_total=round(float(zp1 - zp0), 3),
    mating=round(float(zp1 - zb1), 3), tail=round(float(zb0 - zp0), 3),
    pins=pins, pitch=pitch,
)
DS = [  # (what, datasheet nominal, tol, measured key) from 61300411121 rev 003.000 p.1
    ("Pin square", 0.64, 0.15, [p["w_long"] for p in pins] + [p["w_short"] for p in pins]),
    ("Pitch", 2.54, 0.05, pitch),
    ("Total pin length", 11.54, 0.2, [m["pin_total"]]),
    ("Mating length above insulator", 6.0, 0.2, [m["mating"]]),
    ("Tail length below insulator", 3.0, 0.2, [m["tail"]]),
    ("Insulator height", 2.54, 0.15, [m["insulator_h"]]),
    ("Insulator width", 2.54, 0.15, [m["body_wid"]]),
    ("Insulator length L (4 pins)", 10.16, 0.35, [m["body_len"]]),
    ("Pin to pin pl (1 to 4)", 7.62, 0.15, [round(abs(centres[-1] - centres[0]), 3)]),
]
rows = []
for what, nom, tol, vals in DS:
    ok = all(abs(v - nom) <= tol + 1e-6 for v in vals)
    rows.append(dict(dimension=what, datasheet=f"{nom} +-{tol}", model=vals if len(vals) > 1 else vals[0],
                     result="pass" if ok else "fail"))
res = dict(model=glb, units="mm", source="Wurth Elektronik 61300411121 datasheet rev 003.000 (2023-01-08), page 1",
           method="vertices of the step2glb GLB per material (insulator, contacts); pin section from the shaft vertices between the insulator top (+0.5 mm) and the tip chamfer",
           measured=m, rows=rows, all_pass=all(r["result"] == "pass" for r in rows))
json.dump(res, open(out, "w"), indent=1)
print(json.dumps(res, indent=1))