[AutoTool] enable custom sbend endpoint_at

This commit is contained in:
Jan Petykiewicz 2026-07-09 21:38:29 -07:00
commit 0230fb49a6
3 changed files with 101 additions and 13 deletions

View file

@ -48,9 +48,11 @@ from .tools import (
PathTool as PathTool, PathTool as PathTool,
RenderStep as RenderStep, RenderStep as RenderStep,
PrimitiveKind as PrimitiveKind, PrimitiveKind as PrimitiveKind,
GeneratedEndpointFn as GeneratedEndpointFn,
PrimitiveOffer as PrimitiveOffer, PrimitiveOffer as PrimitiveOffer,
StraightOffer as StraightOffer, StraightOffer as StraightOffer,
BendOffer as BendOffer, BendOffer as BendOffer,
SOffer as SOffer, SOffer as SOffer,
UOffer as UOffer, UOffer as UOffer,
circular_arc_sbend_endpoint as circular_arc_sbend_endpoint,
) )

View file

@ -39,7 +39,7 @@ from collections import ChainMap
from collections.abc import Sequence, Callable, Mapping from collections.abc import Sequence, Callable, Mapping
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from math import isclose as scalar_isclose, isfinite as scalar_isfinite, isnan as scalar_isnan from math import isclose as scalar_isclose, isfinite as scalar_isfinite, isnan as scalar_isnan, sqrt
import numpy import numpy
from numpy.typing import NDArray from numpy.typing import NDArray
@ -706,6 +706,32 @@ class Tool(ABC):
GeneratedPrimitiveFn = Callable[..., Pattern | Library] GeneratedPrimitiveFn = Callable[..., Pattern | Library]
GeneratedEndpointFn = Callable[[float], Port]
def circular_arc_sbend_endpoint(radius: float, ptype: str) -> GeneratedEndpointFn:
"""
Return an S-bend endpoint planner for two abutting circular arcs.
The returned callback assumes a pure generated S-bend made from two equal
circular arcs with no attached straight or non-circular pieces. Positive
and negative jogs are supported; the output rotation is always `pi`.
"""
rr = float(radius)
if not scalar_isfinite(rr) or rr <= 0:
raise BuildError(f'S-bend radius must be positive and finite, got {rr:g}')
def endpoint(jog: float) -> Port:
jj = float(jog)
jog_magnitude = abs(jj)
if scalar_isclose(jog_magnitude, 0.0, rel_tol=1e-9, abs_tol=1e-12):
return Port((0, 0), rotation=pi, ptype=ptype)
if jog_magnitude > 2 * rr and not scalar_isclose(jog_magnitude, 2 * rr, rel_tol=1e-9, abs_tol=1e-12):
raise BuildError(f'S-bend jog magnitude {jog_magnitude:g} exceeds diameter {2 * rr:g}')
dx = sqrt(max(0.0, 4 * rr * jog_magnitude - jog_magnitude ** 2))
return Port((dx, jj), rotation=pi, ptype=ptype)
return endpoint
@dataclass @dataclass
@ -853,23 +879,29 @@ class AutoTool(Tool):
out_port_name: str, out_port_name: str,
*, *,
jog_range: tuple[float, float] = (0, numpy.inf), jog_range: tuple[float, float] = (0, numpy.inf),
endpoint: GeneratedEndpointFn | None = None,
) -> Self: ) -> Self:
""" """
Register a generated S-bend primitive. Register a generated S-bend primitive.
"""
for jog_domain in self._signed_jog_domains(jog_range):
priority_bias = len(self._s_offers) * BUILTIN_PRIORITY_STEP
def endpoint_s( `endpoint`, when supplied, describes the generated S-bend output port
jog: float, directly during planning and avoids instantiating `fn()` inside
*, `endpoint_at()`.
fn: GeneratedPrimitiveFn = fn, """
in_port_name: str = in_port_name, if endpoint is None:
out_port_name: str = out_port_name, def endpoint_at(jog: float) -> Port:
) -> Port:
jog_magnitude = abs(jog) jog_magnitude = abs(jog)
sbend_dxy = self._sbend2dxy(fn, in_port_name, out_port_name, jog_magnitude) sbend_dxy = self._sbend2dxy(fn, in_port_name, out_port_name, jog_magnitude)
return Port((float(sbend_dxy[0]), float(jog)), rotation=pi, ptype=ptype) return Port((float(sbend_dxy[0]), float(jog)), rotation=pi, ptype=ptype)
else:
def endpoint_at(jog: float) -> Port:
out_port = endpoint(jog)
if not ptypes_compatible(out_port.ptype, ptype):
raise BuildError('S-bend endpoint ptype does not match registered ptype')
return out_port
for jog_domain in self._signed_jog_domains(jog_range):
priority_bias = len(self._s_offers) * BUILTIN_PRIORITY_STEP
def data_at(jog: float) -> AutoTool.GeneratedData: def data_at(jog: float) -> AutoTool.GeneratedData:
return self.GeneratedData( return self.GeneratedData(
@ -881,7 +913,7 @@ class AutoTool(Tool):
self._s_offers.append(SOffer.generated( self._s_offers.append(SOffer.generated(
ptype, ptype,
endpoint_s, endpoint_at,
data_at, data_at,
priority_bias = priority_bias, priority_bias = priority_bias,
bbox_for_data = self._bbox_for_data, bbox_for_data = self._bbox_for_data,

View file

@ -5,7 +5,7 @@ import pytest
from numpy import pi from numpy import pi
from numpy.testing import assert_allclose from numpy.testing import assert_allclose
from masque.builder.tools import AutoTool, PrimitiveOffer, RenderStep, UOffer from masque.builder.tools import AutoTool, PrimitiveOffer, RenderStep, UOffer, circular_arc_sbend_endpoint
from masque.builder.pather import Pather from masque.builder.pather import Pather
from masque.library import Library from masque.library import Library
from masque.pattern import Pattern from masque.pattern import Pattern
@ -368,6 +368,60 @@ def make_sbend_tool(jog_range: tuple[float, float]) -> AutoTool:
) )
def test_autotool_sbend_custom_endpoint_avoids_generator_during_planning() -> None:
calls = 0
def make_counted_sbend(jog: float) -> Pattern:
nonlocal calls
calls += 1
return make_sbend(jog, ptype="core")
def endpoint(jog: float) -> Port:
return Port((20, jog), rotation=pi, ptype="core")
tool = AutoTool().add_sbend(
"core",
make_counted_sbend,
"A",
"B",
jog_range=(0, 1e8),
endpoint=endpoint,
)
offer = tool.primitive_offers("s", in_ptype="core")[0]
out_port = offer.endpoint_at(4)
assert calls == 0
assert_allclose(out_port.offset, [20, 4])
assert out_port.rotation == pi
assert out_port.ptype == "core"
data = offer.commit(4)
assert data.parameter == 4
assert data.mirrored is False
def test_circular_arc_sbend_endpoint() -> None:
endpoint = circular_arc_sbend_endpoint(radius=5, ptype="core")
out_port = endpoint(4)
assert_allclose(out_port.offset, [8, 4])
assert out_port.rotation == pi
assert out_port.ptype == "core"
mirrored_port = endpoint(-4)
assert_allclose(mirrored_port.offset, [8, -4])
assert mirrored_port.rotation == pi
assert mirrored_port.ptype == "core"
zero_port = endpoint(0)
assert_allclose(zero_port.offset, [0, 0])
assert zero_port.rotation == pi
with pytest.raises(BuildError, match="exceeds diameter"):
endpoint(11)
def make_uturn_pattern(length: float = 10, jog: float = 4, ptype: str = "core") -> Pattern: def make_uturn_pattern(length: float = 10, jog: float = 4, ptype: str = "core") -> Pattern:
pat = Pattern() pat = Pattern()
y0, y1 = sorted((0, jog)) y0, y1 = sorted((0, jog))