From ecf37580c59d033c1e869265a84b04549a9d6cab Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 12 Oct 2023 01:30:23 -0700 Subject: [PATCH] improve docs and variable names --- masque/utils/pack2d.py | 96 +++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/masque/utils/pack2d.py b/masque/utils/pack2d.py index 75a7d59..d298404 100644 --- a/masque/utils/pack2d.py +++ b/masque/utils/pack2d.py @@ -17,8 +17,26 @@ def maxrects_bssf( allow_rejects: bool = True, ) -> tuple[NDArray[numpy.float64], set[int]]: """ - sizes should be Nx2 - regions should be Mx4 (xmin, ymin, xmax, ymax) + Pack rectangles `rects` into regions `containers` using the "maximal rectangles best short side fit" + algorithm (maxrects_bssf) from "A thousand ways to pack the bin", Jukka Jylanki, 2010. + + This algorithm gives the best results, but is asymptotically slower than `guillotine_bssf_sas`. + + Args: + rects: Nx2 array of rectangle sizes `[[x_size0, y_size0], ...]`. + containers: Mx4 array of regions into which `rects` will be placed, specified using their + corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`. + presort: If `True` (default), largest-shortest-side rectangles will be placed + first. Otherwise, they will be placed in the order provided. + allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed. + + Returns: + `[[x_min0, y_min0], ...]` placement locations for `rects`, with the same ordering. + The second argument is a set of indicies of `rects` entries which were rejected; their + corresponding placement locations should be ignored. + + Raises: + MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. """ regions = numpy.array(containers, copy=False, dtype=float) rect_sizes = numpy.array(rects, copy=False, dtype=float) @@ -96,10 +114,30 @@ def guillotine_bssf_sas( allow_rejects: bool = True, ) -> tuple[NDArray[numpy.float64], set[int]]: """ - sizes should be Nx2 - regions should be Mx4 (xmin, ymin, xmax, ymax) - #TODO: test me! - # TODO add rectangle-merge? + Pack rectangles `rects` into regions `containers` using the "guillotine best short side fit with + shorter axis split rule" algorithm (guillotine-BSSF-SAS) from "A thousand ways to pack the bin", + Jukka Jylanki, 2010. + + This algorithm gives the worse results than `maxrects_bssf`, but is asymptotically faster. + + # TODO consider adding rectangle-merge? + # TODO guillotine could use some additional testing + + Args: + rects: Nx2 array of rectangle sizes `[[x_size0, y_size0], ...]`. + containers: Mx4 array of regions into which `rects` will be placed, specified using their + corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`. + presort: If `True` (default), largest-shortest-side rectangles will be placed + first. Otherwise, they will be placed in the order provided. + allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed. + + Returns: + `[[x_min0, y_min0], ...]` placement locations for `rects`, with the same ordering. + The second argument is a set of indicies of `rects` entries which were rejected; their + corresponding placement locations should be ignored. + + Raises: + MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. """ regions = numpy.array(containers, copy=False, dtype=float) rect_sizes = numpy.array(rects, copy=False, dtype=float) @@ -133,15 +171,15 @@ def guillotine_bssf_sas( new_region0 = regions[rr].copy() new_region1 = new_region0.copy() - split_vert = loc + rect_size + split_vertex = loc + rect_size if split_horiz: - new_region0[2] = split_vert[0] - new_region0[1] = split_vert[1] - new_region1[0] = split_vert[0] + new_region0[2] = split_vertex[0] + new_region0[1] = split_vertex[1] + new_region1[0] = split_vertex[0] else: - new_region0[3] = split_vert[1] - new_region0[0] = split_vert[0] - new_region1[1] = split_vert[1] + new_region0[3] = split_vertex[1] + new_region0[0] = split_vertex[0] + new_region1[1] = split_vertex[1] regions = numpy.vstack((regions[:rr], regions[rr + 1:], new_region0, new_region1)) @@ -157,19 +195,45 @@ def guillotine_bssf_sas( def pack_patterns( library: Mapping[str, Pattern], patterns: Sequence[str], - regions: numpy.ndarray, + containers: ArrayLike, spacing: tuple[float, float], presort: bool = True, allow_rejects: bool = True, packer: Callable = maxrects_bssf, ) -> tuple[Pattern, list[str]]: - half_spacing = numpy.array(spacing) / 2 + """ + Pick placement locations for `patterns` inside the regions specified by `containers`. + No rotations are performed. + + Args: + library: Library from which `Pattern` objects will be drawn. + patterns: Sequence of pattern names which are to be placed. + containers: Mx4 array of regions into which `patterns` will be placed, specified using their + corner coordinates ` [[x_min0, y_min0, x_max0, y_max0], ...]`. + spacing: (x, y) spacing between adjacent patterns. Patterns are effectively expanded outwards + by `spacing / 2` prior to placement, so this also affects pattern position relative to + container edges. + presort: If `True` (default), largest-shortest-side rectangles will be placed + first. Otherwise, they will be placed in the order provided. + allow_rejects: If `False`, `MasqueError` will be raised if any rectangle cannot be placed. + packer: Bin-packing method; see the other functions in this module (namely `maxrects_bssf` + and `guillotine_bssf_sas`). + + Returns: + A `Pattern` containing one `Ref` for each entry in `patterns`. + A list of "rejected" pattern names, for which a valid placement location could not be found. + + Raises: + MasqueError if `allow_rejects` is `True` but some `rects` could not be placed. + """ + + half_spacing = numpy.array(spacing, copy=False, dtype=float) / 2 bounds = [library[pp].get_bounds() for pp in patterns] sizes = [bb[1] - bb[0] + spacing if bb is not None else spacing for bb in bounds] offsets = [half_spacing - bb[0] if bb is not None else (0, 0) for bb in bounds] - locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects) + locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects) pat = Pattern() for pp, oo, loc in zip(patterns, offsets, locations):