[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,
|
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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue