[AutoTool] enable custom sbend endpoint_at
This commit is contained in:
parent
7083edc7ca
commit
0230fb49a6
3 changed files with 101 additions and 13 deletions
|
|
@ -48,9 +48,11 @@ from .tools import (
|
|||
PathTool as PathTool,
|
||||
RenderStep as RenderStep,
|
||||
PrimitiveKind as PrimitiveKind,
|
||||
GeneratedEndpointFn as GeneratedEndpointFn,
|
||||
PrimitiveOffer as PrimitiveOffer,
|
||||
StraightOffer as StraightOffer,
|
||||
BendOffer as BendOffer,
|
||||
SOffer as SOffer,
|
||||
UOffer as UOffer,
|
||||
circular_arc_sbend_endpoint as circular_arc_sbend_endpoint,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from collections import ChainMap
|
|||
from collections.abc import Sequence, Callable, Mapping
|
||||
from abc import ABC, abstractmethod
|
||||
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
|
||||
from numpy.typing import NDArray
|
||||
|
|
@ -706,6 +706,32 @@ class Tool(ABC):
|
|||
|
||||
|
||||
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
|
||||
|
|
@ -853,23 +879,29 @@ class AutoTool(Tool):
|
|||
out_port_name: str,
|
||||
*,
|
||||
jog_range: tuple[float, float] = (0, numpy.inf),
|
||||
endpoint: GeneratedEndpointFn | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
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(
|
||||
jog: float,
|
||||
*,
|
||||
fn: GeneratedPrimitiveFn = fn,
|
||||
in_port_name: str = in_port_name,
|
||||
out_port_name: str = out_port_name,
|
||||
) -> Port:
|
||||
`endpoint`, when supplied, describes the generated S-bend output port
|
||||
directly during planning and avoids instantiating `fn()` inside
|
||||
`endpoint_at()`.
|
||||
"""
|
||||
if endpoint is None:
|
||||
def endpoint_at(jog: float) -> Port:
|
||||
jog_magnitude = abs(jog)
|
||||
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)
|
||||
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:
|
||||
return self.GeneratedData(
|
||||
|
|
@ -881,7 +913,7 @@ class AutoTool(Tool):
|
|||
|
||||
self._s_offers.append(SOffer.generated(
|
||||
ptype,
|
||||
endpoint_s,
|
||||
endpoint_at,
|
||||
data_at,
|
||||
priority_bias = priority_bias,
|
||||
bbox_for_data = self._bbox_for_data,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import pytest
|
|||
from numpy import pi
|
||||
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.library import Library
|
||||
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:
|
||||
pat = Pattern()
|
||||
y0, y1 = sorted((0, jog))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue