Model slotted (oblong) THT holes as stadiums, not circles
The drill's y dimension was discarded (padstack.drill.diameter.x only), so a milled slot became a round hole of its x size - contact rings, drill mouths and lead cones painted circles larger than the oblong pad itself, and the cone was skipped outright (pad_min <= drill). Slots now keep their true stadium shape, rotated with the pad (KiCad CCW, y down): Electrode/ViaLink carry the end-cap offset vector, drill_nm becomes the slot WIDTH, and a shared slot_distance() reduces to the plain radius for round holes. The barrel wall ring, mouth coverage, cone taper and the solver's attachment search all follow the slot; barrel_resistance uses the stadium perimeter and bore area. The stitching-coat fallback becomes a capsule along the slot (inscribed disc when the axis is unknown) instead of a largest-dimension disc. Slot fields round-trip through the JSON dumps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,11 @@ SWIG API. Requires KiCad **10.0.1+**.
|
|||||||
worth far more than the foil, so this is conservative. The pad face
|
worth far more than the foil, so this is conservative. The pad face
|
||||||
gets the average-thickness solder coat (exact pad shape) and the
|
gets the average-thickness solder coat (exact pad shape) and the
|
||||||
protruding-lead cone (see barrel contacts below; on oblong pads the
|
protruding-lead cone (see barrel contacts below; on oblong pads the
|
||||||
cone tapers within the inscribed circle). Whether a hole is a via or
|
cone tapers to the pad's short dimension). **Slotted (oval) holes**
|
||||||
|
keep their true stadium shape: the barrel wall, drill mouth, contact
|
||||||
|
ring and lead cone all follow the slot (rotated with the pad), and
|
||||||
|
the barrel conducts over the slot's real perimeter/bore area — not a
|
||||||
|
circle of the slot's long dimension. Whether a hole is a via or
|
||||||
a THT pad, the owning footprint's side, and its **Do not populate**
|
a THT pad, the owning footprint's side, and its **Do not populate**
|
||||||
flag are all read from KiCad. **DNP pads** get an **open hole** and
|
flag are all read from KiCad. **DNP pads** get an **open hole** and
|
||||||
a plating-only barrel, no joint. At f > 0 the thickness scaling is
|
a plating-only barrel, no joint. At f > 0 the thickness scaling is
|
||||||
@@ -159,7 +163,8 @@ SWIG API. Requires KiCad **10.0.1+**.
|
|||||||
physically enters through the lead/wire soldered into the hole, so
|
physically enters through the lead/wire soldered into the hole, so
|
||||||
the spreading resistance across the pad and surrounding pour is part
|
the spreading resistance across the pad and surrounding pour is part
|
||||||
of the result (both contact models; verified against
|
of the result (both contact models; verified against
|
||||||
R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet). A
|
R = ρ/(π·t)·acosh(d/2a) for two circular contacts on a sheet).
|
||||||
|
Slotted holes inject along the stadium-shaped slot wall. A
|
||||||
soldered **THT joint** additionally assumes the **hole is filled with
|
soldered **THT joint** additionally assumes the **hole is filled with
|
||||||
solder** (core in parallel with the plating) and the **pad face on
|
solder** (core in parallel with the plating) and the **pad face on
|
||||||
the solder side carries an average-thickness solder coat**
|
the solder side carries an average-thickness solder coat**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ KiCad to extract without the dialog (all layers of the net, defaults).
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -139,11 +140,33 @@ def _convert_poly(poly_with_holes) -> Polygon:
|
|||||||
holes=[ring(h) for h in poly_with_holes.holes])
|
holes=[ring(h) for h in poly_with_holes.holes])
|
||||||
|
|
||||||
|
|
||||||
def _pad_drill_nm(pad_or_via) -> int:
|
def _drill_info(pad_or_via) -> tuple[int, int, int]:
|
||||||
|
"""(width_nm, slot_dx_nm, slot_dy_nm) of a padstack drill. Round
|
||||||
|
holes: (diameter, 0, 0). Slotted (oblong) holes: width is the
|
||||||
|
NARROW dimension, (slot_dx, slot_dy) the board-frame offset from
|
||||||
|
the drill center to each end-cap center of the slot. The slot
|
||||||
|
follows the pad rotation (KiCad rotates CCW with y down:
|
||||||
|
x' = x cos + y sin, y' = y cos - x sin)."""
|
||||||
try:
|
try:
|
||||||
return int(pad_or_via.padstack.drill.diameter.x)
|
d = pad_or_via.padstack.drill.diameter
|
||||||
|
dx, dy = int(d.x), int(d.y)
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0, 0, 0
|
||||||
|
if dx <= 0 or dy <= 0 or dx == dy:
|
||||||
|
return max(dx, 0), 0, 0
|
||||||
|
half = (max(dx, dy) - min(dx, dy)) / 2.0
|
||||||
|
try:
|
||||||
|
th = math.radians(pad_or_via.padstack.angle.degrees)
|
||||||
|
except Exception:
|
||||||
|
th = 0.0
|
||||||
|
ux, uy = (1.0, 0.0) if dx > dy else (0.0, 1.0)
|
||||||
|
return (min(dx, dy),
|
||||||
|
int(round(half * (ux * math.cos(th) + uy * math.sin(th)))),
|
||||||
|
int(round(half * (uy * math.cos(th) - ux * math.sin(th)))))
|
||||||
|
|
||||||
|
|
||||||
|
def _pad_drill_nm(pad_or_via) -> int:
|
||||||
|
return _drill_info(pad_or_via)[0]
|
||||||
|
|
||||||
|
|
||||||
def _pad_default_contact(pad: Pad) -> str:
|
def _pad_default_contact(pad: Pad) -> str:
|
||||||
@@ -250,7 +273,7 @@ def _to_electrode(board: Board, item, stackup: StackupInfo | None = None,
|
|||||||
if box is None:
|
if box is None:
|
||||||
raise SelectionError(f"Could not get the bounding box of {label}.")
|
raise SelectionError(f"Could not get the bounding box of {label}.")
|
||||||
rect = _box2_to_rect(box, "pad")
|
rect = _box2_to_rect(box, "pad")
|
||||||
drill = _pad_drill_nm(pad)
|
drill, slot_dx, slot_dy = _drill_info(pad)
|
||||||
return Electrode(rect=rect, contact=contact,
|
return Electrode(rect=rect, contact=contact,
|
||||||
polygons=_pad_polygons(board, pad, contact), label=label,
|
polygons=_pad_polygons(board, pad, contact), label=label,
|
||||||
# through-hole pad: current enters at the soldered
|
# through-hole pad: current enters at the soldered
|
||||||
@@ -258,6 +281,7 @@ def _to_electrode(board: Board, item, stackup: StackupInfo | None = None,
|
|||||||
# with a solder cone around the protruding lead
|
# with a solder cone around the protruding lead
|
||||||
drill_nm=drill, pad_nm=_padstack_pad_nm(pad),
|
drill_nm=drill, pad_nm=_padstack_pad_nm(pad),
|
||||||
pad_min_nm=_padstack_pad_min_nm(pad),
|
pad_min_nm=_padstack_pad_min_nm(pad),
|
||||||
|
slot_dx_nm=slot_dx, slot_dy_nm=slot_dy,
|
||||||
center=(pad.position.x, pad.position.y),
|
center=(pad.position.x, pad.position.y),
|
||||||
solder=drill > 0,
|
solder=drill > 0,
|
||||||
protrusion_side=(_tht_protrusion_side(pad, pad_map or {})
|
protrusion_side=(_tht_protrusion_side(pad, pad_map or {})
|
||||||
@@ -547,12 +571,14 @@ def gather_barrels(board: Board, net_name: str,
|
|||||||
populated = not fp.attributes.do_not_populate
|
populated = not fp.attributes.do_not_populate
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
drill, slot_dx, slot_dy = _drill_info(pad)
|
||||||
barrels.append(ViaLink(
|
barrels.append(ViaLink(
|
||||||
x=pad.position.x, y=pad.position.y,
|
x=pad.position.x, y=pad.position.y,
|
||||||
drill_nm=_pad_drill_nm(pad), z_top_nm=-1,
|
drill_nm=drill, z_top_nm=-1,
|
||||||
z_bot_nm=stackup.z_bot_nm + 1, kind="pad",
|
z_bot_nm=stackup.z_bot_nm + 1, kind="pad",
|
||||||
pad_nm=_padstack_pad_nm(pad),
|
pad_nm=_padstack_pad_nm(pad),
|
||||||
pad_min_nm=_padstack_pad_min_nm(pad),
|
pad_min_nm=_padstack_pad_min_nm(pad),
|
||||||
|
slot_dx_nm=slot_dx, slot_dy_nm=slot_dy,
|
||||||
solder_filled=populated,
|
solder_filled=populated,
|
||||||
protrusion_side=(_tht_protrusion_side(pad, pad_map,
|
protrusion_side=(_tht_protrusion_side(pad, pad_map,
|
||||||
quiet=True)
|
quiet=True)
|
||||||
|
|||||||
@@ -113,11 +113,16 @@ class Electrode:
|
|||||||
contact: str = "all"
|
contact: str = "all"
|
||||||
polygons: list[Polygon] | None = None
|
polygons: list[Polygon] | None = None
|
||||||
label: str = "rect"
|
label: str = "rect"
|
||||||
drill_nm: int = 0 # >0: barrel contact
|
drill_nm: int = 0 # >0: barrel contact (slotted
|
||||||
|
# holes: the slot WIDTH)
|
||||||
pad_nm: int = 0 # pad diameter (search bound;
|
pad_nm: int = 0 # pad diameter (search bound;
|
||||||
# largest dimension if oblong)
|
# largest dimension if oblong)
|
||||||
pad_min_nm: int = 0 # smallest pad dimension (cone
|
pad_min_nm: int = 0 # smallest pad dimension (cone
|
||||||
# taper bound); 0 = pad_nm
|
# taper bound); 0 = pad_nm
|
||||||
|
slot_dx_nm: int = 0 # slotted (oblong) hole: offset
|
||||||
|
slot_dy_nm: int = 0 # from `center` to each end-cap
|
||||||
|
# center of the slot, board
|
||||||
|
# frame; (0, 0) = round drill
|
||||||
center: tuple[int, int] | None = None # drill center; None = rect center
|
center: tuple[int, int] | None = None # drill center; None = rect center
|
||||||
barrel_z: tuple[int, int] | None = None # (z_top, z_bot); None = full stack
|
barrel_z: tuple[int, int] | None = None # (z_top, z_bot); None = full stack
|
||||||
solder: bool = False # soldered THT joint (see above)
|
solder: bool = False # soldered THT joint (see above)
|
||||||
@@ -134,7 +139,7 @@ class ViaLink:
|
|||||||
layers whose z lies within [z_top_nm, z_bot_nm]."""
|
layers whose z lies within [z_top_nm, z_bot_nm]."""
|
||||||
x: int
|
x: int
|
||||||
y: int
|
y: int
|
||||||
drill_nm: int
|
drill_nm: int # slotted holes: the slot WIDTH
|
||||||
z_top_nm: int
|
z_top_nm: int
|
||||||
z_bot_nm: int
|
z_bot_nm: int
|
||||||
kind: str = "via" # "via" | "pad"
|
kind: str = "via" # "via" | "pad"
|
||||||
@@ -144,6 +149,10 @@ class ViaLink:
|
|||||||
pad_min_nm: int = 0 # smallest pad dimension (bounds
|
pad_min_nm: int = 0 # smallest pad dimension (bounds
|
||||||
# the lead-cone taper on oblong
|
# the lead-cone taper on oblong
|
||||||
# pads); 0 = same as pad_nm
|
# pads); 0 = same as pad_nm
|
||||||
|
slot_dx_nm: int = 0 # slotted (oblong) hole: offset
|
||||||
|
slot_dy_nm: int = 0 # from (x, y) to each end-cap
|
||||||
|
# center of the slot, board
|
||||||
|
# frame; (0, 0) = round drill
|
||||||
solder_filled: bool = False # populated THT pad: the hole
|
solder_filled: bool = False # populated THT pad: the hole
|
||||||
# holds lead + solder (in parallel
|
# holds lead + solder (in parallel
|
||||||
# with the plating); False for
|
# with the plating); False for
|
||||||
@@ -162,19 +171,22 @@ class ViaLink:
|
|||||||
lead_nm: float = 0,
|
lead_nm: float = 0,
|
||||||
lead_rho_ohm_m: float | None = None) -> float:
|
lead_rho_ohm_m: float | None = None) -> float:
|
||||||
"""Barrel segment resistance over length_nm: thin-wall annulus of
|
"""Barrel segment resistance over length_nm: thin-wall annulus of
|
||||||
plating around the drill. With solder_rho_ohm_m the hole holds a
|
plating around the drill (slotted holes: thin wall around the
|
||||||
|
stadium-shaped slot). With solder_rho_ohm_m the hole holds a
|
||||||
soldered THT joint: the component lead (a cylinder of lead_nm
|
soldered THT joint: the component lead (a cylinder of lead_nm
|
||||||
diameter, resistivity lead_rho_ohm_m) and the solder filling the
|
diameter, resistivity lead_rho_ohm_m) and the solder filling the
|
||||||
remaining annulus conduct in parallel with the plating."""
|
remaining bore conduct in parallel with the plating."""
|
||||||
ga = math.pi * (self.drill_nm * 1e-9) * (plating_nm * 1e-9) \
|
ext = 2.0 * math.hypot(self.slot_dx_nm, self.slot_dy_nm) * 1e-9
|
||||||
/ rho_ohm_m # conductance-area [m^2/ohm-m]
|
wall = math.pi * (self.drill_nm * 1e-9) + 2.0 * ext
|
||||||
|
ga = wall * (plating_nm * 1e-9) / rho_ohm_m
|
||||||
|
# conductance-area [m^2/ohm-m]
|
||||||
if solder_rho_ohm_m is not None:
|
if solder_rho_ohm_m is not None:
|
||||||
r_core = max(self.drill_nm / 2.0 - plating_nm, 0.0) * 1e-9
|
r_core = max(self.drill_nm / 2.0 - plating_nm, 0.0) * 1e-9
|
||||||
r_lead = min(lead_nm * 1e-9 / 2.0, r_core)
|
r_lead = min(lead_nm * 1e-9 / 2.0, r_core)
|
||||||
if lead_rho_ohm_m is not None and r_lead > 0:
|
if lead_rho_ohm_m is not None and r_lead > 0:
|
||||||
ga += math.pi * r_lead * r_lead / lead_rho_ohm_m
|
ga += math.pi * r_lead * r_lead / lead_rho_ohm_m
|
||||||
ga += math.pi * (r_core * r_core - r_lead * r_lead) \
|
ga += (math.pi * r_core * r_core + 2.0 * r_core * ext
|
||||||
/ solder_rho_ohm_m
|
- math.pi * r_lead * r_lead) / solder_rho_ohm_m
|
||||||
return (length_nm * 1e-9) / ga
|
return (length_nm * 1e-9) / ga
|
||||||
|
|
||||||
|
|
||||||
@@ -261,6 +273,19 @@ def contact_solder_buildups(problem: Problem) -> list[str]:
|
|||||||
return sorted(set(touched))
|
return sorted(set(touched))
|
||||||
|
|
||||||
|
|
||||||
|
def slot_distance(xg, yg, dx_nm: int, dy_nm: int):
|
||||||
|
"""Distance from points (xg, yg) (numpy-broadcastable, coordinates
|
||||||
|
RELATIVE to the hole center) to a slotted hole's axis - the segment
|
||||||
|
(-dx, -dy)..(+dx, +dy) between the end-cap centers. The slot wall
|
||||||
|
sits at distance width/2. Round drills (dx = dy = 0) reduce to the
|
||||||
|
plain radius, so callers need no special case."""
|
||||||
|
if dx_nm == 0 and dy_nm == 0:
|
||||||
|
return np.hypot(xg, yg)
|
||||||
|
l2 = float(dx_nm) * dx_nm + float(dy_nm) * dy_nm
|
||||||
|
t = np.clip((xg * dx_nm + yg * dy_nm) / l2, -1.0, 1.0)
|
||||||
|
return np.hypot(xg - t * dx_nm, yg - t * dy_nm)
|
||||||
|
|
||||||
|
|
||||||
def _disc_polygon(x_nm: float, y_nm: float, r_nm: float,
|
def _disc_polygon(x_nm: float, y_nm: float, r_nm: float,
|
||||||
n: int = 32) -> Polygon:
|
n: int = 32) -> Polygon:
|
||||||
th = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
|
th = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
|
||||||
@@ -269,6 +294,19 @@ def _disc_polygon(x_nm: float, y_nm: float, r_nm: float,
|
|||||||
axis=1)).astype(np.int64))
|
axis=1)).astype(np.int64))
|
||||||
|
|
||||||
|
|
||||||
|
def _capsule_polygon(x_nm: float, y_nm: float, dx_nm: float, dy_nm: float,
|
||||||
|
r_nm: float, n: int = 16) -> Polygon:
|
||||||
|
"""Stadium: two half-circle caps of radius r_nm centered at
|
||||||
|
(x +- dx, y +- dy), joined by straight flanks."""
|
||||||
|
a0 = math.atan2(dy_nm, dx_nm)
|
||||||
|
th = np.linspace(-0.5 * math.pi, 0.5 * math.pi, n) + a0
|
||||||
|
cap1 = np.stack([x_nm + dx_nm + r_nm * np.cos(th),
|
||||||
|
y_nm + dy_nm + r_nm * np.sin(th)], axis=1)
|
||||||
|
cap2 = np.stack([x_nm - dx_nm + r_nm * np.cos(th + math.pi),
|
||||||
|
y_nm - dy_nm + r_nm * np.sin(th + math.pi)], axis=1)
|
||||||
|
return Polygon(outline=np.round(np.vstack([cap1, cap2])).astype(np.int64))
|
||||||
|
|
||||||
|
|
||||||
def tht_joint_buildups(problem: Problem,
|
def tht_joint_buildups(problem: Problem,
|
||||||
shapes: dict | None = None) -> list[str]:
|
shapes: dict | None = None) -> list[str]:
|
||||||
"""Solder coat of the net's populated STITCHING through-hole pads
|
"""Solder coat of the net's populated STITCHING through-hole pads
|
||||||
@@ -292,7 +330,16 @@ def tht_joint_buildups(problem: Problem,
|
|||||||
if polys is None:
|
if polys is None:
|
||||||
if v.pad_nm <= v.drill_nm:
|
if v.pad_nm <= v.drill_nm:
|
||||||
continue
|
continue
|
||||||
polys = [_disc_polygon(v.x, v.y, v.pad_nm / 2.0)]
|
# oblong pads: never coat past the pad - a capsule along the
|
||||||
|
# slot axis, or the inscribed disc when the axis is unknown
|
||||||
|
w = v.pad_min_nm or v.pad_nm
|
||||||
|
hl = math.hypot(v.slot_dx_nm, v.slot_dy_nm)
|
||||||
|
if hl > 0.0 and v.pad_nm > w:
|
||||||
|
s = (v.pad_nm - w) / 2.0 / hl
|
||||||
|
polys = [_capsule_polygon(v.x, v.y, v.slot_dx_nm * s,
|
||||||
|
v.slot_dy_nm * s, w / 2.0)]
|
||||||
|
else:
|
||||||
|
polys = [_disc_polygon(v.x, v.y, w / 2.0)]
|
||||||
problem.buildups.append(
|
problem.buildups.append(
|
||||||
SurfaceBuildup(layer_name=v.protrusion_side,
|
SurfaceBuildup(layer_name=v.protrusion_side,
|
||||||
polygons=list(polys)))
|
polygons=list(polys)))
|
||||||
@@ -451,6 +498,8 @@ def _electrode_to_json(e: Electrode) -> dict:
|
|||||||
"drill_nm": e.drill_nm,
|
"drill_nm": e.drill_nm,
|
||||||
"pad_nm": e.pad_nm,
|
"pad_nm": e.pad_nm,
|
||||||
"pad_min_nm": e.pad_min_nm,
|
"pad_min_nm": e.pad_min_nm,
|
||||||
|
"slot_dx_nm": e.slot_dx_nm,
|
||||||
|
"slot_dy_nm": e.slot_dy_nm,
|
||||||
"center": (None if e.center is None else list(e.center)),
|
"center": (None if e.center is None else list(e.center)),
|
||||||
"barrel_z": (None if e.barrel_z is None else list(e.barrel_z)),
|
"barrel_z": (None if e.barrel_z is None else list(e.barrel_z)),
|
||||||
"solder": e.solder,
|
"solder": e.solder,
|
||||||
@@ -468,6 +517,8 @@ def _electrode_from_json(d: dict) -> Electrode:
|
|||||||
drill_nm=int(d.get("drill_nm", 0)),
|
drill_nm=int(d.get("drill_nm", 0)),
|
||||||
pad_nm=int(d.get("pad_nm", 0)),
|
pad_nm=int(d.get("pad_nm", 0)),
|
||||||
pad_min_nm=int(d.get("pad_min_nm", 0)),
|
pad_min_nm=int(d.get("pad_min_nm", 0)),
|
||||||
|
slot_dx_nm=int(d.get("slot_dx_nm", 0)),
|
||||||
|
slot_dy_nm=int(d.get("slot_dy_nm", 0)),
|
||||||
center=(None if d.get("center") is None
|
center=(None if d.get("center") is None
|
||||||
else (int(d["center"][0]), int(d["center"][1]))),
|
else (int(d["center"][0]), int(d["center"][1]))),
|
||||||
barrel_z=(None if d.get("barrel_z") is None
|
barrel_z=(None if d.get("barrel_z") is None
|
||||||
@@ -564,6 +615,8 @@ def problem_from_json(d: dict) -> Problem:
|
|||||||
kind=vd.get("kind", "via"),
|
kind=vd.get("kind", "via"),
|
||||||
pad_nm=int(vd.get("pad_nm", 0)),
|
pad_nm=int(vd.get("pad_nm", 0)),
|
||||||
pad_min_nm=int(vd.get("pad_min_nm", 0)),
|
pad_min_nm=int(vd.get("pad_min_nm", 0)),
|
||||||
|
slot_dx_nm=int(vd.get("slot_dx_nm", 0)),
|
||||||
|
slot_dy_nm=int(vd.get("slot_dy_nm", 0)),
|
||||||
# older dumps: every THT pad counted as solder-filled
|
# older dumps: every THT pad counted as solder-filled
|
||||||
solder_filled=bool(vd.get(
|
solder_filled=bool(vd.get(
|
||||||
"solder_filled", vd.get("kind", "via") == "pad")),
|
"solder_filled", vd.get("kind", "via") == "pad")),
|
||||||
|
|||||||
+28
-22
@@ -23,7 +23,7 @@ from scipy import ndimage
|
|||||||
|
|
||||||
from . import config
|
from . import config
|
||||||
from .errors import ElectrodeError, GridSizeError
|
from .errors import ElectrodeError, GridSizeError
|
||||||
from .geometry import Electrode, Problem, Rect
|
from .geometry import Electrode, Problem, Rect, slot_distance
|
||||||
|
|
||||||
# 4-connectivity: matches the in-plane 5-point stencil of the solver
|
# 4-connectivity: matches the in-plane 5-point stencil of the solver
|
||||||
_STRUCT4 = ndimage.generate_binary_structure(2, 1)
|
_STRUCT4 = ndimage.generate_binary_structure(2, 1)
|
||||||
@@ -270,29 +270,31 @@ def _paint_lead_fillets(stack: RasterStack, problem: Problem) -> None:
|
|||||||
y = (e.rect.y0 + e.rect.y1) / 2.0
|
y = (e.rect.y0 + e.rect.y1) / 2.0
|
||||||
seen.add((int(x), int(y)))
|
seen.add((int(x), int(y)))
|
||||||
if e.solder and e.protrusion_side:
|
if e.solder and e.protrusion_side:
|
||||||
# oblong pads: taper to the inscribed circle (conservative)
|
# oblong pads: taper from the (slot) wall to the inscribed
|
||||||
|
# dimension (conservative)
|
||||||
jobs.append((x, y, e.drill_nm, e.pad_min_nm or e.pad_nm,
|
jobs.append((x, y, e.drill_nm, e.pad_min_nm or e.pad_nm,
|
||||||
e.protrusion_side))
|
e.protrusion_side, e.slot_dx_nm, e.slot_dy_nm))
|
||||||
for v in problem.vias:
|
for v in problem.vias:
|
||||||
if v.kind == "pad" and v.solder_filled and v.protrusion_side \
|
if v.kind == "pad" and v.solder_filled and v.protrusion_side \
|
||||||
and (v.x, v.y) not in seen:
|
and (v.x, v.y) not in seen:
|
||||||
jobs.append((v.x, v.y, v.drill_nm, v.pad_min_nm or v.pad_nm,
|
jobs.append((v.x, v.y, v.drill_nm, v.pad_min_nm or v.pad_nm,
|
||||||
v.protrusion_side))
|
v.protrusion_side, v.slot_dx_nm, v.slot_dy_nm))
|
||||||
|
|
||||||
for x, y, drill_nm, pad_nm, side in jobs:
|
for x, y, drill_nm, pad_nm, side, sdx, sdy in jobs:
|
||||||
li = index.get(side)
|
li = index.get(side)
|
||||||
if li is None or pad_nm <= drill_nm:
|
if li is None or pad_nm <= drill_nm:
|
||||||
continue
|
continue
|
||||||
ra, rb = drill_nm / 2.0, pad_nm / 2.0
|
ra, rb = drill_nm / 2.0, pad_nm / 2.0
|
||||||
j0 = max(0, math.floor((x - rb - stack.x0_nm) / h))
|
ex, ey = rb + abs(sdx), rb + abs(sdy)
|
||||||
j1 = min(nx, math.floor((x + rb - stack.x0_nm) / h) + 1)
|
j0 = max(0, math.floor((x - ex - stack.x0_nm) / h))
|
||||||
i0 = max(0, math.floor((y - rb - stack.y0_nm) / h))
|
j1 = min(nx, math.floor((x + ex - stack.x0_nm) / h) + 1)
|
||||||
i1 = min(ny, math.floor((y + rb - stack.y0_nm) / h) + 1)
|
i0 = max(0, math.floor((y - ey - stack.y0_nm) / h))
|
||||||
|
i1 = min(ny, math.floor((y + ey - stack.y0_nm) / h) + 1)
|
||||||
if i0 >= i1 or j0 >= j1:
|
if i0 >= i1 or j0 >= j1:
|
||||||
continue
|
continue
|
||||||
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - x
|
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - x
|
||||||
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - y
|
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - y
|
||||||
r = np.sqrt(ys[:, None] ** 2 + xs[None, :] ** 2)
|
r = slot_distance(xs[None, :], ys[:, None], sdx, sdy)
|
||||||
t_sn = H * np.clip((rb - r) / (rb - ra), 0.0, 1.0)
|
t_sn = H * np.clip((rb - r) / (rb - ra), 0.0, 1.0)
|
||||||
t_eq = t_sn * (problem.rho_ohm_m / problem.solder_rho_ohm_m)
|
t_eq = t_sn * (problem.rho_ohm_m / problem.solder_rho_ohm_m)
|
||||||
factor = 1.0 + t_eq / problem.layers[li].thickness_nm
|
factor = 1.0 + t_eq / problem.layers[li].thickness_nm
|
||||||
@@ -353,18 +355,20 @@ def _apply_via_mouths(stack: RasterStack, problem: Problem) -> None:
|
|||||||
if via.kind == "pad" and via.solder_filled:
|
if via.kind == "pad" and via.solder_filled:
|
||||||
continue
|
continue
|
||||||
r = via.drill_nm / 2.0
|
r = via.drill_nm / 2.0
|
||||||
j0 = max(0, math.floor((via.x - r - stack.x0_nm) / h))
|
ex, ey = r + abs(via.slot_dx_nm), r + abs(via.slot_dy_nm)
|
||||||
j1 = min(nx, math.floor((via.x + r - stack.x0_nm) / h) + 1)
|
j0 = max(0, math.floor((via.x - ex - stack.x0_nm) / h))
|
||||||
i0 = max(0, math.floor((via.y - r - stack.y0_nm) / h))
|
j1 = min(nx, math.floor((via.x + ex - stack.x0_nm) / h) + 1)
|
||||||
i1 = min(ny, math.floor((via.y + r - stack.y0_nm) / h) + 1)
|
i0 = max(0, math.floor((via.y - ey - stack.y0_nm) / h))
|
||||||
|
i1 = min(ny, math.floor((via.y + ey - stack.y0_nm) / h) + 1)
|
||||||
if i0 >= i1 or j0 >= j1:
|
if i0 >= i1 or j0 >= j1:
|
||||||
continue
|
continue
|
||||||
xs = stack.x0_nm + (np.arange(j0, j1)[:, None] + sub[None, :]) * h \
|
xs = stack.x0_nm + (np.arange(j0, j1)[:, None] + sub[None, :]) * h \
|
||||||
- via.x
|
- via.x
|
||||||
ys = stack.y0_nm + (np.arange(i0, i1)[:, None] + sub[None, :]) * h \
|
ys = stack.y0_nm + (np.arange(i0, i1)[:, None] + sub[None, :]) * h \
|
||||||
- via.y
|
- via.y
|
||||||
cov = ((ys[:, None, :, None] ** 2 + xs[None, :, None, :] ** 2)
|
cov = (slot_distance(xs[None, :, None, :], ys[:, None, :, None],
|
||||||
<= r * r).mean(axis=(2, 3))
|
via.slot_dx_nm, via.slot_dy_nm)
|
||||||
|
<= r).mean(axis=(2, 3))
|
||||||
if not (cov > 0).any():
|
if not (cov > 0).any():
|
||||||
continue # mouth far smaller than h
|
continue # mouth far smaller than h
|
||||||
if stack.thick_scale is None:
|
if stack.thick_scale is None:
|
||||||
@@ -477,7 +481,8 @@ def _electrode_cells2d(stack: RasterStack, e: Electrode) -> np.ndarray:
|
|||||||
def _barrel_ring2d(stack: RasterStack, e: Electrode,
|
def _barrel_ring2d(stack: RasterStack, e: Electrode,
|
||||||
mask2d: np.ndarray) -> np.ndarray:
|
mask2d: np.ndarray) -> np.ndarray:
|
||||||
"""Contact cells of a barrel electrode on one layer: the copper ring
|
"""Contact cells of a barrel electrode on one layer: the copper ring
|
||||||
at the drill wall (cell centers within one cell of radius drill/2),
|
at the drill wall (cell centers within one cell of radius drill/2;
|
||||||
|
slotted holes: within one cell of the stadium-shaped slot wall),
|
||||||
where the lead/wire soldered into the hole actually meets the layer.
|
where the lead/wire soldered into the hole actually meets the layer.
|
||||||
If rasterization or an antipad leaves no copper there, fall back to
|
If rasterization or an antipad leaves no copper there, fall back to
|
||||||
the nearest copper ring within the pad footprint (+1 cell of slop) -
|
the nearest copper ring within the pad footprint (+1 cell of slop) -
|
||||||
@@ -491,16 +496,17 @@ def _barrel_ring2d(stack: RasterStack, e: Electrode,
|
|||||||
y = (e.rect.y0 + e.rect.y1) / 2.0
|
y = (e.rect.y0 + e.rect.y1) / 2.0
|
||||||
r = e.drill_nm / 2.0
|
r = e.drill_nm / 2.0
|
||||||
rw = max(e.pad_nm, e.drill_nm + 300_000) / 2.0 + h
|
rw = max(e.pad_nm, e.drill_nm + 300_000) / 2.0 + h
|
||||||
|
ex, ey = rw + abs(e.slot_dx_nm), rw + abs(e.slot_dy_nm)
|
||||||
out = np.zeros((ny, nx), dtype=bool)
|
out = np.zeros((ny, nx), dtype=bool)
|
||||||
j0 = max(0, math.floor((x - rw - stack.x0_nm) / h))
|
j0 = max(0, math.floor((x - ex - stack.x0_nm) / h))
|
||||||
j1 = min(nx, math.floor((x + rw - stack.x0_nm) / h) + 1)
|
j1 = min(nx, math.floor((x + ex - stack.x0_nm) / h) + 1)
|
||||||
i0 = max(0, math.floor((y - rw - stack.y0_nm) / h))
|
i0 = max(0, math.floor((y - ey - stack.y0_nm) / h))
|
||||||
i1 = min(ny, math.floor((y + rw - stack.y0_nm) / h) + 1)
|
i1 = min(ny, math.floor((y + ey - stack.y0_nm) / h) + 1)
|
||||||
if i0 >= i1 or j0 >= j1:
|
if i0 >= i1 or j0 >= j1:
|
||||||
return out
|
return out
|
||||||
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - x
|
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - x
|
||||||
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - y
|
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - y
|
||||||
d = np.sqrt(ys[:, None] ** 2 + xs[None, :] ** 2)
|
d = slot_distance(xs[None, :], ys[:, None], e.slot_dx_nm, e.slot_dy_nm)
|
||||||
m = mask2d[i0:i1, j0:j1]
|
m = mask2d[i0:i1, j0:j1]
|
||||||
ring = m & (np.abs(d - r) <= h)
|
ring = m & (np.abs(d - r) <= h)
|
||||||
if not ring.any():
|
if not ring.any():
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from scipy.sparse import linalg as sla
|
|||||||
|
|
||||||
from . import config, skin
|
from . import config, skin
|
||||||
from .errors import ConnectivityError, ElectrodeError, SolverError
|
from .errors import ConnectivityError, ElectrodeError, SolverError
|
||||||
from .geometry import Problem
|
from .geometry import Problem, slot_distance
|
||||||
from .raster import RasterStack, electrodes_touch
|
from .raster import RasterStack, electrodes_touch
|
||||||
|
|
||||||
|
|
||||||
@@ -156,12 +156,14 @@ def _barrel_links(stack: RasterStack, problem: Problem
|
|||||||
span = [li for li, layer in enumerate(problem.layers)
|
span = [li for li, layer in enumerate(problem.layers)
|
||||||
if via.spans(layer.z_nm)]
|
if via.spans(layer.z_nm)]
|
||||||
r_nm = max(via.pad_nm, via.drill_nm + 300_000) / 2.0 + h
|
r_nm = max(via.pad_nm, via.drill_nm + 300_000) / 2.0 + h
|
||||||
win = int(r_nm // h) + 1
|
win_j = int((r_nm + abs(via.slot_dx_nm)) // h) + 1
|
||||||
i0, i1 = max(0, i - win), min(ny, i + win + 1)
|
win_i = int((r_nm + abs(via.slot_dy_nm)) // h) + 1
|
||||||
j0, j1 = max(0, j - win), min(nx, j + win + 1)
|
i0, i1 = max(0, i - win_i), min(ny, i + win_i + 1)
|
||||||
|
j0, j1 = max(0, j - win_j), min(nx, j + win_j + 1)
|
||||||
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - via.x
|
xs = stack.x0_nm + (np.arange(j0, j1) + 0.5) * h - via.x
|
||||||
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - via.y
|
ys = stack.y0_nm + (np.arange(i0, i1) + 0.5) * h - via.y
|
||||||
d2 = ys[:, None] ** 2 + xs[None, :] ** 2
|
d2 = slot_distance(xs[None, :], ys[:, None],
|
||||||
|
via.slot_dx_nm, via.slot_dy_nm) ** 2
|
||||||
d2 = np.where(d2 <= r_nm * r_nm, d2, np.inf)
|
d2 = np.where(d2 <= r_nm * r_nm, d2, np.inf)
|
||||||
present = [] # (layer, i, j) per layer
|
present = [] # (layer, i, j) per layer
|
||||||
for li in span:
|
for li in span:
|
||||||
|
|||||||
@@ -318,6 +318,162 @@ def test_vialink_solder_json():
|
|||||||
assert problem_from_json(d).vias[0].solder_filled is False
|
assert problem_from_json(d).vias[0].solder_filled is False
|
||||||
|
|
||||||
|
|
||||||
|
# --- slotted (oblong) holes --------------------------------------------------
|
||||||
|
# The lead/barrel of a slotted hole is a stadium, not a circle: modeling
|
||||||
|
# it as a circle of the slot's LONG dimension painted contact rings,
|
||||||
|
# mouths and cones bigger than the oblong pad itself.
|
||||||
|
|
||||||
|
def _slot_dist_mm(stack, ii, jj, x_mm, y_mm, dx_nm):
|
||||||
|
"""Distance of cells (ii, jj) to a slot axis (+-dx_nm along x)."""
|
||||||
|
xs = stack.x0_nm + (jj + 0.5) * stack.h_nm - x_mm * NM
|
||||||
|
ys = stack.y0_nm + (ii + 0.5) * stack.h_nm - y_mm * NM
|
||||||
|
t = np.clip(xs / dx_nm, -1.0, 1.0)
|
||||||
|
return np.hypot(xs - t * dx_nm, ys), xs, ys
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_ring_hugs_slot_wall():
|
||||||
|
"""The contact ring of a slotted THT pad follows the stadium-shaped
|
||||||
|
slot wall: it reaches around the end caps but never pokes past the
|
||||||
|
oblong pad's short side (the old circular model of the slot's long
|
||||||
|
dimension put cells at radius 1.5 mm straight above/below)."""
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
e = _barrel(10, 10, drill_mm=1.0, pad_mm=3.6) # slot 3.0 x 1.0 mm
|
||||||
|
e.pad_min_nm = int(1.6 * NM) # pad 3.6 x 1.6 mm
|
||||||
|
e.slot_dx_nm = 1 * NM
|
||||||
|
p.electrodes1 = [e]
|
||||||
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
||||||
|
e1, _ = raster.electrode_masks(stack, p)
|
||||||
|
ii, jj = np.nonzero(e1[0])
|
||||||
|
d, xs, ys = _slot_dist_mm(stack, ii, jj, 10, 10, 1 * NM)
|
||||||
|
assert len(ii) >= 16
|
||||||
|
assert (np.abs(d - 0.5 * NM) <= stack.h_nm + 1).all()
|
||||||
|
assert xs.max() > 1.2 * NM and xs.min() < -1.2 * NM # rings the caps
|
||||||
|
assert np.abs(ys).max() < 0.8 * NM # stays inside the 1.6 mm side
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_mouth_is_stadium():
|
||||||
|
"""A DNP slotted pad cuts a stadium-shaped hole: open along the whole
|
||||||
|
slot, copper kept just past the slot width and the end caps."""
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
v = _pad_link(populated=False)
|
||||||
|
v.slot_dx_nm = 1 * NM # slot 3.0 x 1.0 mm along x
|
||||||
|
p.vias = [v]
|
||||||
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
||||||
|
m = stack.masks[0]
|
||||||
|
assert not m[stack.cell_of(10 * NM, 10 * NM)]
|
||||||
|
assert not m[stack.cell_of(int(10.9 * NM), 10 * NM)] # slot end: open
|
||||||
|
assert not m[stack.cell_of(int(9.1 * NM), 10 * NM)]
|
||||||
|
assert m[stack.cell_of(10 * NM, int(10.8 * NM))] # past the width: copper
|
||||||
|
assert m[stack.cell_of(10 * NM, int(9.2 * NM))]
|
||||||
|
assert m[stack.cell_of(int(11.8 * NM), 10 * NM)] # past the cap: copper
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_cone_follows_slot():
|
||||||
|
"""The lead cone of a slotted oblong pad tapers from the slot WALL
|
||||||
|
to the pad's short dimension. The old circular-drill model (diameter
|
||||||
|
= the slot's long dimension) skipped the cone entirely
|
||||||
|
(pad_min <= drill) and, for the mouth, ate the pad's short side."""
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
e = _barrel(10, 10, drill_mm=1.0, pad_mm=3.6, solder=True)
|
||||||
|
e.pad_min_nm = int(1.6 * NM)
|
||||||
|
e.slot_dx_nm = 1 * NM
|
||||||
|
e.protrusion_side = "F.Cu"
|
||||||
|
p.electrodes1 = [e]
|
||||||
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
||||||
|
assert stack.thick_scale is not None
|
||||||
|
ny, nx = stack.shape2d
|
||||||
|
jj, ii = np.meshgrid(np.arange(nx), np.arange(ny))
|
||||||
|
r, _, _ = _slot_dist_mm(stack, ii, jj, 10, 10, 1 * NM)
|
||||||
|
ra, rb, H = 0.5 * NM, 0.8 * NM, p.tht_protrusion_nm
|
||||||
|
t_eq = H * np.clip((rb - r) / (rb - ra), 0, 1) \
|
||||||
|
* (p.rho_ohm_m / p.solder_rho_ohm_m)
|
||||||
|
expect = np.where(stack.masks[0],
|
||||||
|
1.0 + t_eq / p.layers[0].thickness_nm, 1.0)
|
||||||
|
assert np.allclose(stack.thick_scale[0], expect, rtol=1e-12)
|
||||||
|
assert stack.thick_scale[0].max() > 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_barrel_resistance():
|
||||||
|
"""Slotted barrel: plating wall = stadium perimeter, solder core =
|
||||||
|
stadium bore area (both reduce to the circle for dx = dy = 0)."""
|
||||||
|
v = ViaLink(x=0, y=0, drill_nm=1_000_000, z_top_nm=-1, z_bot_nm=1,
|
||||||
|
slot_dx_nm=800_000, slot_dy_nm=600_000) # ext = 2 mm
|
||||||
|
rho, sn = 1.68e-8, 1.32e-7
|
||||||
|
ga = (math.pi * 1e-3 + 2 * 2e-3) * 18e-6 / rho
|
||||||
|
r_plain = v.barrel_resistance(1_600_000, rho, 18_000)
|
||||||
|
assert r_plain == pytest.approx(1.6e-3 / ga, rel=1e-12)
|
||||||
|
rc = 0.5e-3 - 18e-6
|
||||||
|
ga += (math.pi * rc * rc + 2 * rc * 2e-3) / sn
|
||||||
|
r_fill = v.barrel_resistance(1_600_000, rho, 18_000, solder_rho_ohm_m=sn)
|
||||||
|
assert r_fill == pytest.approx(1.6e-3 / ga, rel=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_coat_fallback_within_pad():
|
||||||
|
"""Without an exact pad shape the stitching coat falls back to a
|
||||||
|
capsule along the slot (width = pad_min), not the old pad_nm disc
|
||||||
|
that stuck out past an oblong pad's short side."""
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
v = _pad_link() # pad_nm = 2.4 mm
|
||||||
|
v.pad_min_nm = 1_600_000
|
||||||
|
v.slot_dx_nm = 1 * NM
|
||||||
|
p.vias = [v]
|
||||||
|
assert tht_joint_buildups(p) == ["F.Cu"]
|
||||||
|
pts = p.buildups[0].polygons[0].outline.astype(float)
|
||||||
|
xs, ys = pts[:, 0] - 10 * NM, pts[:, 1] - 10 * NM
|
||||||
|
t = np.clip(xs / (0.4 * NM), -1.0, 1.0) # caps at +-(2.4-1.6)/2 mm
|
||||||
|
d = np.hypot(xs - t * 0.4 * NM, ys)
|
||||||
|
assert np.allclose(d, 0.8 * NM, atol=2)
|
||||||
|
assert np.abs(xs).max() <= 1.2 * NM + 2 # never past pad_nm / 2
|
||||||
|
assert np.abs(ys).max() <= 0.8 * NM + 2 # never past pad_min / 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_slot_json_roundtrip():
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
e = _barrel(10, 10, drill_mm=1.0, pad_mm=3.6)
|
||||||
|
e.slot_dx_nm, e.slot_dy_nm = 700_000, -700_000
|
||||||
|
p.electrodes1 = [e]
|
||||||
|
v = _pad_link()
|
||||||
|
v.slot_dx_nm = 1 * NM
|
||||||
|
p.vias = [v]
|
||||||
|
q = problem_from_json(problem_to_json(p))
|
||||||
|
assert (q.electrodes1[0].slot_dx_nm, q.electrodes1[0].slot_dy_nm) \
|
||||||
|
== (700_000, -700_000)
|
||||||
|
assert (q.vias[0].slot_dx_nm, q.vias[0].slot_dy_nm) == (1 * NM, 0)
|
||||||
|
# legacy dumps: round drills
|
||||||
|
d = problem_to_json(p)
|
||||||
|
for vd in d["vias"]:
|
||||||
|
del vd["slot_dx_nm"], vd["slot_dy_nm"]
|
||||||
|
assert problem_from_json(d).vias[0].slot_dx_nm == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_drill_info_slot_rotation():
|
||||||
|
"""_drill_info: slot axis from the drill x/y sizes, rotated with the
|
||||||
|
pad (KiCad angles are CCW with y down: 90 deg sends +x to -y)."""
|
||||||
|
from types import SimpleNamespace as NS
|
||||||
|
|
||||||
|
from fill_resistance.board_io import _drill_info
|
||||||
|
|
||||||
|
def pad(dx_mm, dy_mm, angle_deg):
|
||||||
|
return NS(padstack=NS(
|
||||||
|
drill=NS(diameter=NS(x=int(dx_mm * NM), y=int(dy_mm * NM))),
|
||||||
|
angle=NS(degrees=angle_deg)))
|
||||||
|
|
||||||
|
assert _drill_info(pad(1.0, 1.0, 0.0)) == (1 * NM, 0, 0) # round
|
||||||
|
assert _drill_info(pad(3.0, 1.0, 0.0)) == (1 * NM, 1 * NM, 0)
|
||||||
|
assert _drill_info(pad(1.0, 3.0, 0.0)) == (1 * NM, 0, 1 * NM)
|
||||||
|
w, dx, dy = _drill_info(pad(3.0, 1.0, 90.0))
|
||||||
|
assert (w, dx, dy) == (1 * NM, 0, -1 * NM)
|
||||||
|
w, dx, dy = _drill_info(pad(3.0, 1.0, 45.0))
|
||||||
|
assert w == 1 * NM
|
||||||
|
assert dx == pytest.approx(1 * NM / math.sqrt(2), abs=2)
|
||||||
|
assert dy == pytest.approx(-1 * NM / math.sqrt(2), abs=2)
|
||||||
|
|
||||||
|
|
||||||
def test_barrel_electrode_json_roundtrip(tmp_path):
|
def test_barrel_electrode_json_roundtrip(tmp_path):
|
||||||
p = make_problem([(PLATE20, [])],
|
p = make_problem([(PLATE20, [])],
|
||||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
|||||||
Reference in New Issue
Block a user