Compare commits

...

9 Commits

Author SHA1 Message Date
jan
9026103b51 Cleanup based on flake8 lint 2023-10-13 02:36:23 -07:00
jan
4ae98a94ed some further work on Tool interface 2023-10-13 02:32:07 -07:00
jan
808766f5a9 No need for Builder 2023-10-13 02:31:52 -07:00
jan
973b70ee07 path() should return a tree 2023-10-13 02:31:12 -07:00
jan
aa839facdc doc updates 2023-10-13 02:29:39 -07:00
jan
837f42b9ed more design pattern docs 2023-10-13 00:50:01 -07:00
jan
4699d5c14f doc updates 2023-10-13 00:31:00 -07:00
jan
ccd8a2270a Add some notes on shorthand 2023-10-12 23:43:10 -07:00
jan
a82b1d4dcf comment grammar 2023-10-12 23:40:24 -07:00
9 changed files with 332 additions and 43 deletions

107
README.md
View File

@ -73,6 +73,7 @@ Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, and `Lab
## Glossary
- `Library`: A collection of named cells. OASIS or GDS "library" or file.
- "tree": Any Library which has only one topcell.
- `Pattern`: A collection of geometry, text labels, and reference to other patterns.
OASIS or GDS "Cell", DXF "Block".
- `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement".
@ -83,6 +84,112 @@ Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, and `Lab
- `annotation`: Additional metadata. OASIS or GDS "property".
## Syntax, shorthand, and design patterns
Most syntax and behavior should follow normal python conventions.
There are a few exceptions, either meant to catch common mistakes or to provide a shorthand for common operations:
### `Library` objects don't allow overwriting already-existing patterns
```python3
library['mycell'] = pattern0
library['mycell'] = pattern1 # Error! 'mycell' already exists and can't be overwritten
del library['mycell'] # We can explicitly delete it
library['mycell'] = pattern1 # And now it's ok to assign a new value
library.delete('mycell') # This also deletes all refs pointing to 'mycell' by default
```
### Insert a newly-made hierarchical pattern (with children) into a layout
```python3
# Let's say we have a function which returns a new library containing one topcell (and possibly children)
tree = make_tree(...)
# To reference this cell in our layout, we have to add all its children to our `library` first:
top_name = tree.top() # get the name of the topcell
name_mapping = library.add(tree) # add all patterns from `tree`, renaming elgible conflicting patterns
new_name = name_mapping.get(top_name, top_name) # get the new name for the cell (in case it was auto-renamed)
my_pattern.ref(new_name, ...) # instantiate the cell
# This can be accomplished as follows
new_name = library << tree # Add `tree` into `library` and return the top cell's new name
my_pattern.ref(new_name, ...) # instantiate the cell
# In practice, you may do lots of
my_pattern.ref(lib << make_tree(...), ...)
```
We can also use this shorthand to quickly add and reference a single flat (as yet un-named) pattern:
```python3
anonymous_pattern = Pattern(...)
my_pattern.ref(lib << {'_tentative_name': anonymous_pattern}, ...)
```
### Place a hierarchical pattern into a layout, preserving its port info
```python3
# As above, we have a function that makes a new library containing one topcell (and possibly children)
tree = make_tree(...)
# We need to go get its port info to `place()` it into our existing layout,
new_name = library << tree # Add the tree to the library and return its name (see `<<` above)
abstract = library.abstract(tree) # An `Abstract` stores a pattern's name and its ports (but no geometry)
my_pattern.place(abstract, ...)
# With shorthand,
abstract = library <= tree
my_pattern.place(abstract, ...)
# or
my_pattern.place(library << make_tree(...), ...)
### Quickly add geometry, labels, or refs:
The long form for adding elements can be overly verbose:
```python3
my_pattern.shapes[layer].append(Polygon(vertices, ...))
my_pattern.labels[layer] += [Label('my text')]
my_pattern.refs[target_name].append(Ref(offset=..., ...))
```
There is shorthand for the most common elements:
```python3
my_pattern.polygon(layer=layer, vertices=vertices, ...)
my_pattern.rect(layer=layer, xctr=..., xmin=..., ymax=..., ly=...) # rectangle; pick 4 of 6 constraints
my_pattern.rect(layer=layer, ymin=..., ymax=..., xctr=..., lx=...)
my_pattern.path(...)
my_pattern.label(layer, 'my_text')
my_pattern.ref(target_name, offset=..., ...)
```
### Accessing ports
```python3
# Square brackets pull from the underlying `.ports` dict:
assert pattern['input'] is pattern.ports['input']
# And you can use them to read multiple ports at once:
assert pattern[('input', 'output')] == {
'input': pattern.ports['input'],
'output': pattern.ports['output'],
}
# But you shouldn't use them for anything except reading
pattern['input'] = Port(...) # Error!
has_input = ('input' in pattern) # Error!
```
### Building patterns
```python3
library = Library(...)
my_pattern_name, my_pattern = library.mkpat(some_name_generator())
...
def _make_my_subpattern() -> str:
# This function can draw from the outer scope (e.g. `library`) but will not pollute the outer scope
# (e.g. the variable `subpattern` will not be accessible from outside the function; you must load it
# from within `library`).
subpattern_name, subpattern = library.mkpat(...)
subpattern.rect(...)
...
return subpattern_name
my_pattern.ref(_make_my_subpattern(), offset=..., ...)
```
## TODO

View File

@ -1,17 +1,15 @@
"""
Simplified Pattern assembly (`Builder`)
"""
from typing import Self, Sequence, Mapping, Literal, overload
from typing import Self, Sequence, Mapping
import copy
import logging
from numpy import pi
from numpy.typing import ArrayLike
from ..pattern import Pattern
from ..ref import Ref
from ..library import ILibrary
from ..error import PortError, BuildError
from ..error import BuildError
from ..ports import PortList, Port
from ..abstract import Abstract

View File

@ -324,10 +324,10 @@ class Pather(Builder):
tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.pattern[portspec].ptype
pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
name = self.library.get_name(base_name)
self.library[name] = pat
return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]})
abstract = self.library << tree.rename_top(name)
return self.plug(abstract, {portspec: tool_port_names[0]})
def path_to(
self,
@ -399,12 +399,12 @@ class Pather(Builder):
is_horizontal = numpy.isclose(port.rotation % pi, 0)
if is_horizontal:
if y is not None:
raise BuildError(f'Asked to path to y-coordinate, but port is horizontal')
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
if position is None:
position = x
else:
if x is not None:
raise BuildError(f'Asked to path to x-coordinate, but port is vertical')
raise BuildError('Asked to path to x-coordinate, but port is vertical')
if position is None:
position = y

View File

@ -1,3 +1,6 @@
"""
Pather with batched (multi-step) rendering
"""
from typing import Self, Sequence, Mapping, MutableMapping
import copy
import logging
@ -9,15 +12,13 @@ from numpy import pi
from numpy.typing import ArrayLike
from ..pattern import Pattern
from ..ref import Ref
from ..library import ILibrary, Library
from ..library import ILibrary
from ..error import PortError, BuildError
from ..ports import PortList, Port
from ..abstract import Abstract
from ..utils import SupportsBool
from .tools import Tool, RenderStep
from .utils import ell
from .builder import Builder
logger = logging.getLogger(__name__)
@ -488,12 +489,12 @@ class RenderPather(PortList):
is_horizontal = numpy.isclose(port.rotation % pi, 0)
if is_horizontal:
if y is not None:
raise BuildError(f'Asked to path to y-coordinate, but port is horizontal')
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
if position is None:
position = x
else:
if x is not None:
raise BuildError(f'Asked to path to x-coordinate, but port is vertical')
raise BuildError('Asked to path to x-coordinate, but port is vertical')
if position is None:
position = y

View File

@ -17,16 +17,30 @@ from ..pattern import Pattern
from ..abstract import Abstract
from ..library import ILibrary, Library
from ..error import BuildError
from .builder import Builder
@dataclass(frozen=True, slots=True)
class RenderStep:
"""
Representation of a single saved operation, used by `RenderPather` and passed
to `Tool.render()` when `RenderPather.render()` is called.
"""
opcode: Literal['L', 'S', 'U', 'P']
""" What operation is being performed.
L: planL (straight, optionally with a single bend)
S: planS (s-bend)
U: planU (u-bend)
P: plug
"""
tool: 'Tool | None'
""" The current tool. May be `None` if `opcode='P'` """
start_port: Port
end_port: Port
data: Any
""" Arbitrary tool-specific data"""
def __post_init__(self) -> None:
if self.opcode != 'P' and self.tool is None:
@ -34,6 +48,13 @@ class RenderStep:
class Tool:
"""
Interface for path (e.g. wire or waveguide) generation.
Note that subclasses may implement only a subset of the methods and leave others
unimplemented (e.g. in cases where they don't make sense or the required components
are impractical or unavailable).
"""
def path(
self,
ccw: SupportsBool | None,
@ -43,7 +64,40 @@ class Tool:
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Pattern:
) -> Library:
"""
Create a wire or waveguide that travels exactly `length` distance along the axis
of its input port.
Used by `Pather`.
The output port must be exactly `length` away along the input port's axis, but
may be placed an additional (unspecified) distance away along the perpendicular
direction. The output port should be rotated (or not) based on the value of
`ccw`.
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively. They should also be named `port_names[0]` and
`port_names[1]`, respectively.
Args:
ccw: If `None`, the output should be along the same axis as the input.
Otherwise, cast to bool and turn counterclockwise if True
and clockwise otherwise.
length: The total distance from input to output, along the input's axis only.
(There may be a tool-dependent offset along the other axis.)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
port_names: The output pattern will have its input port named `port_names[0]` and
its output named `port_names[1]`.
kwargs: Custom tool-specific parameters.
Returns:
A pattern tree containing the requested L-shaped (or straight) wire or waveguide
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'path() not implemented for {type(self)}')
def planL(
@ -55,11 +109,41 @@ class Tool:
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, Any]:
"""
Plan a wire or waveguide that travels exactly `length` distance along the axis
of its input port.
Used by `RenderPather`.
The output port must be exactly `length` away along the input port's axis, but
may be placed an additional (unspecified) distance away along the perpendicular
direction. The output port should be rotated (or not) based on the value of
`ccw`.
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively.
Args:
ccw: If `None`, the output should be along the same axis as the input.
Otherwise, cast to bool and turn counterclockwise if True
and clockwise otherwise.
length: The total distance from input to output, along the input's axis only.
(There may be a tool-dependent offset along the other axis.)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
kwargs: Custom tool-specific parameters.
Returns:
The calculated output `Port` for the wire.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'planL() not implemented for {type(self)}')
def planS(
self,
ccw: SupportsBool | None,
length: float,
jog: float,
*,
@ -67,17 +151,91 @@ class Tool:
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, Any]:
"""
Plan a wire or waveguide that travels exactly `length` distance along the axis
of its input port and `jog` distance along the perpendicular axis (i.e. an S-bend).
Used by `RenderPather`.
The output port must have an orientation rotated by pi from the input port.
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively.
Args:
length: The total distance from input to output, along the input's axis only.
jog: The total offset from the input to output, along the perpendicular axis.
A positive number implies a rightwards shift (i.e. clockwise bend followed
by a counterclockwise bend)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
kwargs: Custom tool-specific parameters.
Returns:
The calculated output `Port` for the wire.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'planS() not implemented for {type(self)}')
def planU(
self,
jog: float,
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, Any]:
"""
# NOTE: TODO: U-bend is WIP; this interface may change in the future.
Plan a wire or waveguide that travels exactly `jog` distance along the axis
perpendicular to its input port (i.e. a U-bend).
Used by `RenderPather`.
The output port must have an orientation identical to the input port.
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively.
Args:
jog: The total offset from the input to output, along the perpendicular axis.
A positive number implies a rightwards shift (i.e. clockwise bend followed
by a counterclockwise bend)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
kwargs: Custom tool-specific parameters.
Returns:
The calculated output `Port` for the wire.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'planU() not implemented for {type(self)}')
def render(
self,
batch: Sequence[RenderStep],
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: Sequence[str] = ('A', 'B'),
**kwargs,
) -> ILibrary:
"""
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
(a Library with a single topcell).
Args:
batch: A sequence of `RenderStep` objects containing the ports and data
provided by this tool's `planL`/`planS`/`planU` functions.
port_names: The topcell's input and output ports should be named
`port_names[0]` and `port_names[1]` respectively.
kwargs: Custom tool-specific parameters.
"""
assert not batch or batch[0].tool == self
raise NotImplementedError(f'render() not implemented for {type(self)}')
@ -87,13 +245,26 @@ abstract_tuple_t = tuple[Abstract, str, str]
@dataclass
class BasicTool(Tool, metaclass=ABCMeta):
"""
A simple tool which relies on a single pre-rendered `bend` pattern, a function
for generating straight paths, and a table of pre-rendered `transitions` for converting
from non-native ptypes.
"""
straight: tuple[Callable[[float], Pattern], str, str]
""" `create_straight(length: float), in_port_name, out_port_name` """
bend: abstract_tuple_t # Assumed to be clockwise
""" `clockwise_bend_abstract, in_port_name, out_port_name` """
transitions: dict[str, abstract_tuple_t]
""" `{ptype: (transition_abstract`, ptype_port_name, other_port_name), ...}` """
default_out_ptype: str
""" Default value for out_ptype """
@dataclass(frozen=True, slots=True)
class LData:
""" Data for planL """
straight_length: float
ccw: SupportsBool | None
in_transition: abstract_tuple_t | None
@ -108,7 +279,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Pattern:
) -> Library:
_out_port, data = self.planL(
ccw,
length,
@ -117,22 +288,22 @@ class BasicTool(Tool, metaclass=ABCMeta):
)
gen_straight, sport_in, sport_out = self.straight
tree = Library()
bb = Builder(library=tree, name='_path').add_port_pair(names=port_names)
tree, pat = Library.mktree('_path')
pat.add_port_pair(names=port_names)
if data.in_transition:
ipat, iport_theirs, _iport_ours = data.in_transition
bb.plug(ipat, {port_names[1]: iport_theirs})
pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(data.straight_length, 0):
straight = tree << {'_straight': gen_straight(data.straight_length)}
bb.plug(straight, {port_names[1]: sport_in})
straight = tree <= {'_straight': gen_straight(data.straight_length)}
pat.plug(straight, {port_names[1]: sport_in})
if data.ccw is not None:
bend, bport_in, bport_out = self.bend
bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
if data.out_transition:
opat, oport_theirs, oport_ours = data.out_transition
bb.plug(opat, {port_names[1]: oport_ours})
pat.plug(opat, {port_names[1]: oport_ours})
return bb.pattern
return tree
def planL(
self,
@ -220,8 +391,8 @@ class BasicTool(Tool, metaclass=ABCMeta):
**kwargs,
) -> ILibrary:
tree = Library()
bb = Builder(library=tree, name='_path').add_port_pair(names=(port_names[0], port_names[1]))
tree, pat = Library.mktree('_path')
pat.add_port_pair(names=(port_names[0], port_names[1]))
gen_straight, sport_in, _sport_out = self.straight
for step in batch:
@ -231,28 +402,39 @@ class BasicTool(Tool, metaclass=ABCMeta):
if step.opcode == 'L':
if in_transition:
ipat, iport_theirs, _iport_ours = in_transition
bb.plug(ipat, {port_names[1]: iport_theirs})
pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(straight_length, 0):
straight_pat = gen_straight(straight_length)
if append:
bb.plug(straight_pat, {port_names[1]: sport_in}, append=True)
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
else:
straight = tree << {'_straight': straight_pat}
bb.plug(straight, {port_names[1]: sport_in}, append=True)
straight = tree <= {'_straight': straight_pat}
pat.plug(straight, {port_names[1]: sport_in}, append=True)
if ccw is not None:
bend, bport_in, bport_out = self.bend
bb.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
if out_transition:
opat, oport_theirs, oport_ours = out_transition
bb.plug(opat, {port_names[1]: oport_ours})
pat.plug(opat, {port_names[1]: oport_ours})
return tree
@dataclass
class PathTool(Tool, metaclass=ABCMeta):
"""
A tool which draws `Path` geometry elements.
If `planL` / `render` are used, the `Path` elements can cover >2 vertices;
with `path` only individual rectangles will be drawn.
"""
layer: layer_t
""" Layer to draw on """
width: float
""" `Path` width """
ptype: str = 'unk'
""" ptype for any ports in patterns generated by this tool """
#@dataclass(frozen=True, slots=True)
#class LData:
@ -273,7 +455,7 @@ class PathTool(Tool, metaclass=ABCMeta):
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Pattern:
) -> Library:
out_port, dxy = self.planL(
ccw,
length,
@ -281,7 +463,7 @@ class PathTool(Tool, metaclass=ABCMeta):
out_ptype=out_ptype,
)
pat = Pattern()
tree, pat = Library.mktree('_path')
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
if ccw is None:
@ -296,7 +478,7 @@ class PathTool(Tool, metaclass=ABCMeta):
port_names[1]: Port(dxy, rotation=out_rot, ptype=self.ptype),
}
return pat
return tree
def planL(
self,

View File

@ -122,6 +122,8 @@ def ell(
orig_offsets = numpy.array([p.offset for p in ports.values()])
rot_offsets = (rot_matrix @ orig_offsets.T).T
# ordering_base = rot_offsets.T * [[1], [-1 if ccw else 1]] # could work, but this is actually a more complex routing problem
# y_order = numpy.lexsort(ordering_base) # (need to make sure we don't collide with the next input port @ same y)
y_order = ((-1 if ccw else 1) * rot_offsets[:, 1]).argsort(kind='stable')
y_ind = numpy.empty_like(y_order, dtype=int)
y_ind[y_order] = numpy.arange(y_ind.shape[0])

View File

@ -765,7 +765,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def prune_layers(self) -> Self:
"""
Removes empty layers (empty lists) in `self.shapes` and `self.labels`.
Remove empty layers (empty lists) in `self.shapes` and `self.labels`.
Returns:
self

View File

@ -1,6 +1,5 @@
from typing import Any
import copy
import math
import numpy
from numpy import pi
@ -231,7 +230,7 @@ class Arc(Shape):
def get_thetas(inner: bool) -> NDArray[numpy.float64]:
""" Figure out the parameter values at which we should place vertices to meet the arclength constraint"""
dr = -self.width / 2.0 * (-1 if inner else 1)
#dr = -self.width / 2.0 * (-1 if inner else 1)
n_pts = numpy.ceil(2 * pi * max(self.radii) / max_arclen).astype(int)
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1])

View File

@ -30,7 +30,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
"""
Class specifying functions common to all shapes.
"""
__slots__ = () # Children should use AutoSlots
__slots__ = () # Children should use AutoSlots or set slots themselves
def __copy__(self) -> Self:
cls = self.__class__