reasoning-gym/reasoning_gym/arc/rearc_utils/dsl.py
2025-02-08 19:07:28 +01:00

1175 lines
34 KiB
Python

# types
from typing import Any, Callable, Container, FrozenSet, Tuple, Union
Boolean = bool
Integer = int
IntegerTuple = Tuple[Integer, Integer]
Numerical = Union[Integer, IntegerTuple]
IntegerSet = FrozenSet[Integer]
Grid = Tuple[Tuple[Integer]]
Cell = Tuple[Integer, IntegerTuple]
Object = FrozenSet[Cell]
Objects = FrozenSet[Object]
Indices = FrozenSet[IntegerTuple]
IndicesSet = FrozenSet[Indices]
Patch = Union[Object, Indices]
Element = Union[Object, Grid]
Piece = Union[Grid, Patch]
TupleTuple = Tuple[Tuple]
ContainerContainer = Container[Container]
# constants
ZERO = 0
ONE = 1
TWO = 2
THREE = 3
FOUR = 4
FIVE = 5
SIX = 6
SEVEN = 7
EIGHT = 8
NINE = 9
TEN = 10
F = False
T = True
NEG_ONE = -1
ORIGIN = (0, 0)
UNITY = (1, 1)
DOWN = (1, 0)
RIGHT = (0, 1)
UP = (-1, 0)
LEFT = (0, -1)
NEG_TWO = -2
NEG_UNITY = (-1, -1)
UP_RIGHT = (-1, 1)
DOWN_LEFT = (1, -1)
ZERO_BY_TWO = (0, 2)
TWO_BY_ZERO = (2, 0)
TWO_BY_TWO = (2, 2)
THREE_BY_THREE = (3, 3)
# primitives
def identity(x: Any) -> Any:
"""identity function"""
return x
def add(a: Numerical, b: Numerical) -> Numerical:
"""addition"""
if isinstance(a, int) and isinstance(b, int):
return a + b
elif isinstance(a, tuple) and isinstance(b, tuple):
return (a[0] + b[0], a[1] + b[1])
elif isinstance(a, int) and isinstance(b, tuple):
return (a + b[0], a + b[1])
return (a[0] + b, a[1] + b)
def subtract(a: Numerical, b: Numerical) -> Numerical:
"""subtraction"""
if isinstance(a, int) and isinstance(b, int):
return a - b
elif isinstance(a, tuple) and isinstance(b, tuple):
return (a[0] - b[0], a[1] - b[1])
elif isinstance(a, int) and isinstance(b, tuple):
return (a - b[0], a - b[1])
return (a[0] - b, a[1] - b)
def multiply(a: Numerical, b: Numerical) -> Numerical:
"""multiplication"""
if isinstance(a, int) and isinstance(b, int):
return a * b
elif isinstance(a, tuple) and isinstance(b, tuple):
return (a[0] * b[0], a[1] * b[1])
elif isinstance(a, int) and isinstance(b, tuple):
return (a * b[0], a * b[1])
return (a[0] * b, a[1] * b)
def divide(a: Numerical, b: Numerical) -> Numerical:
"""floor division"""
if isinstance(a, int) and isinstance(b, int):
return a // b
elif isinstance(a, tuple) and isinstance(b, tuple):
return (a[0] // b[0], a[1] // b[1])
elif isinstance(a, int) and isinstance(b, tuple):
return (a // b[0], a // b[1])
return (a[0] // b, a[1] // b)
def invert(n: Numerical) -> Numerical:
"""inversion with respect to addition"""
return -n if isinstance(n, int) else (-n[0], -n[1])
def even(n: Integer) -> Boolean:
"""evenness"""
return n % 2 == 0
def double(n: Numerical) -> Numerical:
"""scaling by two"""
return n * 2 if isinstance(n, int) else (n[0] * 2, n[1] * 2)
def halve(n: Numerical) -> Numerical:
"""scaling by one half"""
return n // 2 if isinstance(n, int) else (n[0] // 2, n[1] // 2)
def flip(b: Boolean) -> Boolean:
"""logical not"""
return not b
def equality(a: Any, b: Any) -> Boolean:
"""equality"""
return a == b
def contained(value: Any, container: Container) -> Boolean:
"""element of"""
return value in container
def combine(a: Container, b: Container) -> Container:
"""union"""
return type(a)((*a, *b))
def intersection(a: FrozenSet, b: FrozenSet) -> FrozenSet:
"""returns the intersection of two containers"""
return a & b
def difference(a: Container, b: Container) -> Container:
"""difference"""
return type(a)(e for e in a if e not in b)
def dedupe(iterable: Tuple) -> Tuple:
"""remove duplicates"""
return tuple(e for i, e in enumerate(iterable) if iterable.index(e) == i)
def order(container: Container, compfunc: Callable) -> Tuple:
"""order container by custom key"""
return tuple(sorted(container, key=compfunc))
def repeat(item: Any, num: Integer) -> Tuple:
"""repetition of item within vector"""
return tuple(item for i in range(num))
def greater(a: Integer, b: Integer) -> Boolean:
"""greater"""
return a > b
def size(container: Container) -> Integer:
"""cardinality"""
return len(container)
def merge(containers: ContainerContainer) -> Container:
"""merging"""
return type(containers)(e for c in containers for e in c)
def maximum(container: IntegerSet) -> Integer:
"""maximum"""
return max(container, default=0)
def minimum(container: IntegerSet) -> Integer:
"""minimum"""
return min(container, default=0)
def valmax(container: Container, compfunc: Callable) -> Integer:
"""maximum by custom function"""
return compfunc(max(container, key=compfunc, default=0))
def valmin(container: Container, compfunc: Callable) -> Integer:
"""minimum by custom function"""
return compfunc(min(container, key=compfunc, default=0))
def argmax(container: Container, compfunc: Callable) -> Any:
"""largest item by custom order"""
return max(container, key=compfunc, default=None)
def argmin(container: Container, compfunc: Callable) -> Any:
"""smallest item by custom order"""
return min(container, key=compfunc, default=None)
def mostcommon(container: Container) -> Any:
"""most common item"""
return max(set(container), key=container.count)
def leastcommon(container: Container) -> Any:
"""least common item"""
return min(set(container), key=container.count)
def initset(value: Any) -> FrozenSet:
"""initialize container"""
return frozenset({value})
def both(a: Boolean, b: Boolean) -> Boolean:
"""logical and"""
return a and b
def either(a: Boolean, b: Boolean) -> Boolean:
"""logical or"""
return a or b
def increment(x: Numerical) -> Numerical:
"""incrementing"""
return x + 1 if isinstance(x, int) else (x[0] + 1, x[1] + 1)
def decrement(x: Numerical) -> Numerical:
"""decrementing"""
return x - 1 if isinstance(x, int) else (x[0] - 1, x[1] - 1)
def crement(x: Numerical) -> Numerical:
"""incrementing positive and decrementing negative"""
if isinstance(x, int):
return 0 if x == 0 else (x + 1 if x > 0 else x - 1)
return (
0 if x[0] == 0 else (x[0] + 1 if x[0] > 0 else x[0] - 1),
0 if x[1] == 0 else (x[1] + 1 if x[1] > 0 else x[1] - 1),
)
def sign(x: Numerical) -> Numerical:
"""sign"""
if isinstance(x, int):
return 0 if x == 0 else (1 if x > 0 else -1)
return (0 if x[0] == 0 else (1 if x[0] > 0 else -1), 0 if x[1] == 0 else (1 if x[1] > 0 else -1))
def positive(x: Integer) -> Boolean:
"""positive"""
return x > 0
def toivec(i: Integer) -> IntegerTuple:
"""vector pointing vertically"""
return (i, 0)
def tojvec(j: Integer) -> IntegerTuple:
"""vector pointing horizontally"""
return (0, j)
def sfilter(container: Container, condition: Callable) -> Container:
"""keep elements in container that satisfy condition"""
return type(container)(e for e in container if condition(e))
def mfilter(container: Container, function: Callable) -> FrozenSet:
"""filter and merge"""
return merge(sfilter(container, function))
def extract(container: Container, condition: Callable) -> Any:
"""first element of container that satisfies condition"""
return next(e for e in container if condition(e))
def totuple(container: FrozenSet) -> Tuple:
"""conversion to tuple"""
return tuple(container)
def first(container: Container) -> Any:
"""first item of container"""
return next(iter(container))
def last(container: Container) -> Any:
"""last item of container"""
return max(enumerate(container))[1]
def insert(value: Any, container: FrozenSet) -> FrozenSet:
"""insert item into container"""
return container.union(frozenset({value}))
def remove(value: Any, container: Container) -> Container:
"""remove item from container"""
return type(container)(e for e in container if e != value)
def other(container: Container, value: Any) -> Any:
"""other value in the container"""
return first(remove(value, container))
def interval(start: Integer, stop: Integer, step: Integer) -> Tuple:
"""range"""
return tuple(range(start, stop, step))
def astuple(a: Integer, b: Integer) -> IntegerTuple:
"""constructs a tuple"""
return (a, b)
def product(a: Container, b: Container) -> FrozenSet:
"""cartesian product"""
return frozenset((i, j) for j in b for i in a)
def pair(a: Tuple, b: Tuple) -> TupleTuple:
"""zipping of two tuples"""
return tuple(zip(a, b))
def branch(condition: Boolean, if_value: Any, else_value: Any) -> Any:
"""if else branching"""
return if_value if condition else else_value
def compose(outer: Callable, inner: Callable) -> Callable:
"""function composition"""
return lambda x: outer(inner(x))
def chain(h: Callable, g: Callable, f: Callable) -> Callable:
"""function composition with three functions"""
return lambda x: h(g(f(x)))
def matcher(function: Callable, target: Any) -> Callable:
"""construction of equality function"""
return lambda x: function(x) == target
def rbind(function: Callable, fixed: Any) -> Callable:
"""fix the rightmost argument"""
n = function.__code__.co_argcount
if n == 2:
return lambda x: function(x, fixed)
elif n == 3:
return lambda x, y: function(x, y, fixed)
else:
return lambda x, y, z: function(x, y, z, fixed)
def lbind(function: Callable, fixed: Any) -> Callable:
"""fix the leftmost argument"""
n = function.__code__.co_argcount
if n == 2:
return lambda y: function(fixed, y)
elif n == 3:
return lambda y, z: function(fixed, y, z)
else:
return lambda y, z, a: function(fixed, y, z, a)
def power(function: Callable, n: Integer) -> Callable:
"""power of function"""
if n == 1:
return function
return compose(function, power(function, n - 1))
def fork(outer: Callable, a: Callable, b: Callable) -> Callable:
"""creates a wrapper function"""
return lambda x: outer(a(x), b(x))
def apply(function: Callable, container: Container) -> Container:
"""apply function to each item in container"""
return type(container)(function(e) for e in container)
def rapply(functions: Container, value: Any) -> Container:
"""apply each function in container to value"""
return type(functions)(function(value) for function in functions)
def mapply(function: Callable, container: ContainerContainer) -> FrozenSet:
"""apply and merge"""
return merge(apply(function, container))
def papply(function: Callable, a: Tuple, b: Tuple) -> Tuple:
"""apply function on two vectors"""
return tuple(function(i, j) for i, j in zip(a, b))
def mpapply(function: Callable, a: Tuple, b: Tuple) -> Tuple:
"""apply function on two vectors and merge"""
return merge(papply(function, a, b))
def prapply(function: Callable, a: Container, b: Container) -> FrozenSet:
"""apply function on cartesian product"""
return frozenset(function(i, j) for j in b for i in a)
def mostcolor(element: Element) -> Integer:
"""most common color"""
values = [v for r in element for v in r] if isinstance(element, tuple) else [v for v, _ in element]
return max(set(values), key=values.count)
def leastcolor(element: Element) -> Integer:
"""least common color"""
values = [v for r in element for v in r] if isinstance(element, tuple) else [v for v, _ in element]
return min(set(values), key=values.count)
def height(piece: Piece) -> Integer:
"""height of grid or patch"""
if len(piece) == 0:
return 0
if isinstance(piece, tuple):
return len(piece)
return lowermost(piece) - uppermost(piece) + 1
def width(piece: Piece) -> Integer:
"""width of grid or patch"""
if len(piece) == 0:
return 0
if isinstance(piece, tuple):
return len(piece[0])
return rightmost(piece) - leftmost(piece) + 1
def shape(piece: Piece) -> IntegerTuple:
"""height and width of grid or patch"""
return (height(piece), width(piece))
def portrait(piece: Piece) -> Boolean:
"""whether height is greater than width"""
return height(piece) > width(piece)
def colorcount(element: Element, value: Integer) -> Integer:
"""number of cells with color"""
if isinstance(element, tuple):
return sum(row.count(value) for row in element)
return sum(v == value for v, _ in element)
def colorfilter(objs: Objects, value: Integer) -> Objects:
"""filter objects by color"""
return frozenset(obj for obj in objs if next(iter(obj))[0] == value)
def sizefilter(container: Container, n: Integer) -> FrozenSet:
"""filter items by size"""
return frozenset(item for item in container if len(item) == n)
def asindices(grid: Grid) -> Indices:
"""indices of all grid cells"""
return frozenset((i, j) for i in range(len(grid)) for j in range(len(grid[0])))
def ofcolor(grid: Grid, value: Integer) -> Indices:
"""indices of all grid cells with value"""
return frozenset((i, j) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value)
def ulcorner(patch: Patch) -> IntegerTuple:
"""index of upper left corner"""
return tuple(map(min, zip(*toindices(patch))))
def urcorner(patch: Patch) -> IntegerTuple:
"""index of upper right corner"""
return tuple(map(lambda ix: {0: min, 1: max}[ix[0]](ix[1]), enumerate(zip(*toindices(patch)))))
def llcorner(patch: Patch) -> IntegerTuple:
"""index of lower left corner"""
return tuple(map(lambda ix: {0: max, 1: min}[ix[0]](ix[1]), enumerate(zip(*toindices(patch)))))
def lrcorner(patch: Patch) -> IntegerTuple:
"""index of lower right corner"""
return tuple(map(max, zip(*toindices(patch))))
def crop(grid: Grid, start: IntegerTuple, dims: IntegerTuple) -> Grid:
"""subgrid specified by start and dimension"""
return tuple(r[start[1] : start[1] + dims[1]] for r in grid[start[0] : start[0] + dims[0]])
def toindices(patch: Patch) -> Indices:
"""indices of object cells"""
if len(patch) == 0:
return frozenset()
if isinstance(next(iter(patch))[1], tuple):
return frozenset(index for value, index in patch)
return patch
def recolor(value: Integer, patch: Patch) -> Object:
"""recolor patch"""
return frozenset((value, index) for index in toindices(patch))
def shift(patch: Patch, directions: IntegerTuple) -> Patch:
"""shift patch"""
if len(patch) == 0:
return patch
di, dj = directions
if isinstance(next(iter(patch))[1], tuple):
return frozenset((value, (i + di, j + dj)) for value, (i, j) in patch)
return frozenset((i + di, j + dj) for i, j in patch)
def normalize(patch: Patch) -> Patch:
"""moves upper left corner to origin"""
if len(patch) == 0:
return patch
return shift(patch, (-uppermost(patch), -leftmost(patch)))
def dneighbors(loc: IntegerTuple) -> Indices:
"""directly adjacent indices"""
return frozenset({(loc[0] - 1, loc[1]), (loc[0] + 1, loc[1]), (loc[0], loc[1] - 1), (loc[0], loc[1] + 1)})
def ineighbors(loc: IntegerTuple) -> Indices:
"""diagonally adjacent indices"""
return frozenset(
{(loc[0] - 1, loc[1] - 1), (loc[0] - 1, loc[1] + 1), (loc[0] + 1, loc[1] - 1), (loc[0] + 1, loc[1] + 1)}
)
def neighbors(loc: IntegerTuple) -> Indices:
"""adjacent indices"""
return dneighbors(loc) | ineighbors(loc)
def objects(grid: Grid, univalued: Boolean, diagonal: Boolean, without_bg: Boolean) -> Objects:
"""objects occurring on the grid"""
bg = mostcolor(grid) if without_bg else None
objs = set()
occupied = set()
h, w = len(grid), len(grid[0])
unvisited = asindices(grid)
diagfun = neighbors if diagonal else dneighbors
for loc in unvisited:
if loc in occupied:
continue
val = grid[loc[0]][loc[1]]
if val == bg:
continue
obj = {(val, loc)}
cands = {loc}
while len(cands) > 0:
neighborhood = set()
for cand in cands:
v = grid[cand[0]][cand[1]]
if (val == v) if univalued else (v != bg):
obj.add((v, cand))
occupied.add(cand)
neighborhood |= {(i, j) for i, j in diagfun(cand) if 0 <= i < h and 0 <= j < w}
cands = neighborhood - occupied
objs.add(frozenset(obj))
return frozenset(objs)
def partition(grid: Grid) -> Objects:
"""each cell with the same value part of the same object"""
return frozenset(
frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value)
for value in palette(grid)
)
def fgpartition(grid: Grid) -> Objects:
"""each cell with the same value part of the same object without background"""
return frozenset(
frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value)
for value in palette(grid) - {mostcolor(grid)}
)
def uppermost(patch: Patch) -> Integer:
"""row index of uppermost occupied cell"""
return min(i for i, j in toindices(patch))
def lowermost(patch: Patch) -> Integer:
"""row index of lowermost occupied cell"""
return max(i for i, j in toindices(patch))
def leftmost(patch: Patch) -> Integer:
"""column index of leftmost occupied cell"""
return min(j for i, j in toindices(patch))
def rightmost(patch: Patch) -> Integer:
"""column index of rightmost occupied cell"""
return max(j for i, j in toindices(patch))
def square(piece: Piece) -> Boolean:
"""whether the piece forms a square"""
return (
len(piece) == len(piece[0])
if isinstance(piece, tuple)
else height(piece) * width(piece) == len(piece) and height(piece) == width(piece)
)
def vline(patch: Patch) -> Boolean:
"""whether the piece forms a vertical line"""
return height(patch) == len(patch) and width(patch) == 1
def hline(patch: Patch) -> Boolean:
"""whether the piece forms a horizontal line"""
return width(patch) == len(patch) and height(patch) == 1
def hmatching(a: Patch, b: Patch) -> Boolean:
"""whether there exists a row for which both patches have cells"""
return len(set(i for i, j in toindices(a)) & set(i for i, j in toindices(b))) > 0
def vmatching(a: Patch, b: Patch) -> Boolean:
"""whether there exists a column for which both patches have cells"""
return len(set(j for i, j in toindices(a)) & set(j for i, j in toindices(b))) > 0
def manhattan(a: Patch, b: Patch) -> Integer:
"""closest manhattan distance between two patches"""
return min(abs(ai - bi) + abs(aj - bj) for ai, aj in toindices(a) for bi, bj in toindices(b))
def adjacent(a: Patch, b: Patch) -> Boolean:
"""whether two patches are adjacent"""
return manhattan(a, b) == 1
def bordering(patch: Patch, grid: Grid) -> Boolean:
"""whether a patch is adjacent to a grid border"""
return (
uppermost(patch) == 0
or leftmost(patch) == 0
or lowermost(patch) == len(grid) - 1
or rightmost(patch) == len(grid[0]) - 1
)
def centerofmass(patch: Patch) -> IntegerTuple:
"""center of mass"""
return tuple(map(lambda x: sum(x) // len(patch), zip(*toindices(patch))))
def palette(element: Element) -> IntegerSet:
"""colors occurring in object or grid"""
if isinstance(element, tuple):
return frozenset({v for r in element for v in r})
return frozenset({v for v, _ in element})
def numcolors(element: Element) -> IntegerSet:
"""number of colors occurring in object or grid"""
return len(palette(element))
def color(obj: Object) -> Integer:
"""color of object"""
return next(iter(obj))[0]
def toobject(patch: Patch, grid: Grid) -> Object:
"""object from patch and grid"""
h, w = len(grid), len(grid[0])
return frozenset((grid[i][j], (i, j)) for i, j in toindices(patch) if 0 <= i < h and 0 <= j < w)
def asobject(grid: Grid) -> Object:
"""conversion of grid to object"""
return frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r))
def rot90(grid: Grid) -> Grid:
"""quarter clockwise rotation"""
return tuple(row for row in zip(*grid[::-1]))
def rot180(grid: Grid) -> Grid:
"""half rotation"""
return tuple(tuple(row[::-1]) for row in grid[::-1])
def rot270(grid: Grid) -> Grid:
"""quarter anticlockwise rotation"""
return tuple(tuple(row[::-1]) for row in zip(*grid[::-1]))[::-1]
def hmirror(piece: Piece) -> Piece:
"""mirroring along horizontal"""
if isinstance(piece, tuple):
return piece[::-1]
d = ulcorner(piece)[0] + lrcorner(piece)[0]
if isinstance(next(iter(piece))[1], tuple):
return frozenset((v, (d - i, j)) for v, (i, j) in piece)
return frozenset((d - i, j) for i, j in piece)
def vmirror(piece: Piece) -> Piece:
"""mirroring along vertical"""
if isinstance(piece, tuple):
return tuple(row[::-1] for row in piece)
d = ulcorner(piece)[1] + lrcorner(piece)[1]
if isinstance(next(iter(piece))[1], tuple):
return frozenset((v, (i, d - j)) for v, (i, j) in piece)
return frozenset((i, d - j) for i, j in piece)
def dmirror(piece: Piece) -> Piece:
"""mirroring along diagonal"""
if isinstance(piece, tuple):
return tuple(zip(*piece))
a, b = ulcorner(piece)
if isinstance(next(iter(piece))[1], tuple):
return frozenset((v, (j - b + a, i - a + b)) for v, (i, j) in piece)
return frozenset((j - b + a, i - a + b) for i, j in piece)
def cmirror(piece: Piece) -> Piece:
"""mirroring along counterdiagonal"""
if isinstance(piece, tuple):
return tuple(zip(*(r[::-1] for r in piece[::-1])))
return vmirror(dmirror(vmirror(piece)))
def fill(grid: Grid, value: Integer, patch: Patch) -> Grid:
"""fill value at indices"""
h, w = len(grid), len(grid[0])
grid_filled = list(list(row) for row in grid)
for i, j in toindices(patch):
if 0 <= i < h and 0 <= j < w:
grid_filled[i][j] = value
return tuple(tuple(row) for row in grid_filled)
def paint(grid: Grid, obj: Object) -> Grid:
"""paint object to grid"""
h, w = len(grid), len(grid[0])
grid_painted = list(list(row) for row in grid)
for value, (i, j) in obj:
if 0 <= i < h and 0 <= j < w:
grid_painted[i][j] = value
return tuple(tuple(row) for row in grid_painted)
def underfill(grid: Grid, value: Integer, patch: Patch) -> Grid:
"""fill value at indices that are background"""
h, w = len(grid), len(grid[0])
bg = mostcolor(grid)
grid_filled = list(list(row) for row in grid)
for i, j in toindices(patch):
if 0 <= i < h and 0 <= j < w:
if grid_filled[i][j] == bg:
grid_filled[i][j] = value
return tuple(tuple(row) for row in grid_filled)
def underpaint(grid: Grid, obj: Object) -> Grid:
"""paint object to grid where there is background"""
h, w = len(grid), len(grid[0])
bg = mostcolor(grid)
grid_painted = list(list(row) for row in grid)
for value, (i, j) in obj:
if 0 <= i < h and 0 <= j < w:
if grid_painted[i][j] == bg:
grid_painted[i][j] = value
return tuple(tuple(row) for row in grid_painted)
def hupscale(grid: Grid, factor: Integer) -> Grid:
"""upscale grid horizontally"""
upscaled_grid = tuple()
for row in grid:
upscaled_row = tuple()
for value in row:
upscaled_row = upscaled_row + tuple(value for num in range(factor))
upscaled_grid = upscaled_grid + (upscaled_row,)
return upscaled_grid
def vupscale(grid: Grid, factor: Integer) -> Grid:
"""upscale grid vertically"""
upscaled_grid = tuple()
for row in grid:
upscaled_grid = upscaled_grid + tuple(row for num in range(factor))
return upscaled_grid
def upscale(element: Element, factor: Integer) -> Element:
"""upscale object or grid"""
if isinstance(element, tuple):
upscaled_grid = tuple()
for row in element:
upscaled_row = tuple()
for value in row:
upscaled_row = upscaled_row + tuple(value for num in range(factor))
upscaled_grid = upscaled_grid + tuple(upscaled_row for num in range(factor))
return upscaled_grid
else:
if len(element) == 0:
return frozenset()
di_inv, dj_inv = ulcorner(element)
di, dj = (-di_inv, -dj_inv)
normed_obj = shift(element, (di, dj))
upscaled_obj = set()
for value, (i, j) in normed_obj:
for io in range(factor):
for jo in range(factor):
upscaled_obj.add((value, (i * factor + io, j * factor + jo)))
return shift(frozenset(upscaled_obj), (di_inv, dj_inv))
def downscale(grid: Grid, factor: Integer) -> Grid:
"""downscale grid"""
h, w = len(grid), len(grid[0])
downscaled_grid = tuple()
for i in range(h):
downscaled_row = tuple()
for j in range(w):
if j % factor == 0:
downscaled_row = downscaled_row + (grid[i][j],)
downscaled_grid = downscaled_grid + (downscaled_row,)
h = len(downscaled_grid)
downscaled_grid2 = tuple()
for i in range(h):
if i % factor == 0:
downscaled_grid2 = downscaled_grid2 + (downscaled_grid[i],)
return downscaled_grid2
def hconcat(a: Grid, b: Grid) -> Grid:
"""concatenate two grids horizontally"""
return tuple(i + j for i, j in zip(a, b))
def vconcat(a: Grid, b: Grid) -> Grid:
"""concatenate two grids vertically"""
return a + b
def subgrid(patch: Patch, grid: Grid) -> Grid:
"""smallest subgrid containing object"""
return crop(grid, ulcorner(patch), shape(patch))
def hsplit(grid: Grid, n: Integer) -> Tuple:
"""split grid horizontally"""
h, w = len(grid), len(grid[0]) // n
offset = len(grid[0]) % n != 0
return tuple(crop(grid, (0, w * i + i * offset), (h, w)) for i in range(n))
def vsplit(grid: Grid, n: Integer) -> Tuple:
"""split grid vertically"""
h, w = len(grid) // n, len(grid[0])
offset = len(grid) % n != 0
return tuple(crop(grid, (h * i + i * offset, 0), (h, w)) for i in range(n))
def cellwise(a: Grid, b: Grid, fallback: Integer) -> Grid:
"""cellwise match of two grids"""
h, w = len(a), len(a[0])
resulting_grid = tuple()
for i in range(h):
row = tuple()
for j in range(w):
a_value = a[i][j]
value = a_value if a_value == b[i][j] else fallback
row = row + (value,)
resulting_grid = resulting_grid + (row,)
return resulting_grid
def replace(grid: Grid, replacee: Integer, replacer: Integer) -> Grid:
"""color substitution"""
return tuple(tuple(replacer if v == replacee else v for v in r) for r in grid)
def switch(grid: Grid, a: Integer, b: Integer) -> Grid:
"""color switching"""
return tuple(tuple(v if (v != a and v != b) else {a: b, b: a}[v] for v in r) for r in grid)
def center(patch: Patch) -> IntegerTuple:
"""center of the patch"""
return (uppermost(patch) + height(patch) // 2, leftmost(patch) + width(patch) // 2)
def position(a: Patch, b: Patch) -> IntegerTuple:
"""relative position between two patches"""
ia, ja = center(toindices(a))
ib, jb = center(toindices(b))
if ia == ib:
return (0, 1 if ja < jb else -1)
elif ja == jb:
return (1 if ia < ib else -1, 0)
elif ia < ib:
return (1, 1 if ja < jb else -1)
elif ia > ib:
return (-1, 1 if ja < jb else -1)
def index(grid: Grid, loc: IntegerTuple) -> Integer:
"""color at location"""
i, j = loc
h, w = len(grid), len(grid[0])
if not (0 <= i < h and 0 <= j < w):
return None
return grid[loc[0]][loc[1]]
def canvas(value: Integer, dimensions: IntegerTuple) -> Grid:
"""grid construction"""
return tuple(tuple(value for j in range(dimensions[1])) for i in range(dimensions[0]))
def corners(patch: Patch) -> Indices:
"""indices of corners"""
return frozenset({ulcorner(patch), urcorner(patch), llcorner(patch), lrcorner(patch)})
def connect(a: IntegerTuple, b: IntegerTuple) -> Indices:
"""line between two points"""
ai, aj = a
bi, bj = b
si = min(ai, bi)
ei = max(ai, bi) + 1
sj = min(aj, bj)
ej = max(aj, bj) + 1
if ai == bi:
return frozenset((ai, j) for j in range(sj, ej))
elif aj == bj:
return frozenset((i, aj) for i in range(si, ei))
elif bi - ai == bj - aj:
return frozenset((i, j) for i, j in zip(range(si, ei), range(sj, ej)))
elif bi - ai == aj - bj:
return frozenset((i, j) for i, j in zip(range(si, ei), range(ej - 1, sj - 1, -1)))
return frozenset()
def cover(grid: Grid, patch: Patch) -> Grid:
"""remove object from grid"""
return fill(grid, mostcolor(grid), toindices(patch))
def trim(grid: Grid) -> Grid:
"""trim border of grid"""
return tuple(r[1:-1] for r in grid[1:-1])
def move(grid: Grid, obj: Object, offset: IntegerTuple) -> Grid:
"""move object on grid"""
return paint(cover(grid, obj), shift(obj, offset))
def tophalf(grid: Grid) -> Grid:
"""upper half of grid"""
return grid[: len(grid) // 2]
def bottomhalf(grid: Grid) -> Grid:
"""lower half of grid"""
return grid[len(grid) // 2 + len(grid) % 2 :]
def lefthalf(grid: Grid) -> Grid:
"""left half of grid"""
return rot270(tophalf(rot90(grid)))
def righthalf(grid: Grid) -> Grid:
"""right half of grid"""
return rot270(bottomhalf(rot90(grid)))
def vfrontier(location: IntegerTuple) -> Indices:
"""vertical frontier"""
return frozenset((i, location[1]) for i in range(30))
def hfrontier(location: IntegerTuple) -> Indices:
"""horizontal frontier"""
return frozenset((location[0], j) for j in range(30))
def backdrop(patch: Patch) -> Indices:
"""indices in bounding box of patch"""
if len(patch) == 0:
return frozenset({})
indices = toindices(patch)
si, sj = ulcorner(indices)
ei, ej = lrcorner(patch)
return frozenset((i, j) for i in range(si, ei + 1) for j in range(sj, ej + 1))
def delta(patch: Patch) -> Indices:
"""indices in bounding box but not part of patch"""
if len(patch) == 0:
return frozenset({})
return backdrop(patch) - toindices(patch)
def gravitate(source: Patch, destination: Patch) -> IntegerTuple:
"""direction to move source until adjacent to destination"""
source_i, source_j = center(source)
destination_i, destination_j = center(destination)
i, j = 0, 0
if vmatching(source, destination):
i = 1 if source_i < destination_i else -1
else:
j = 1 if source_j < destination_j else -1
direction = (i, j)
gravitation_i, gravitation_j = i, j
maxcount = 42
c = 0
while not adjacent(source, destination) and c < maxcount:
c += 1
gravitation_i += i
gravitation_j += j
source = shift(source, direction)
return (gravitation_i - i, gravitation_j - j)
def inbox(patch: Patch) -> Indices:
"""inbox for patch"""
ai, aj = uppermost(patch) + 1, leftmost(patch) + 1
bi, bj = lowermost(patch) - 1, rightmost(patch) - 1
si, sj = min(ai, bi), min(aj, bj)
ei, ej = max(ai, bi), max(aj, bj)
vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)}
hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)}
return frozenset(vlines | hlines)
def outbox(patch: Patch) -> Indices:
"""outbox for patch"""
ai, aj = uppermost(patch) - 1, leftmost(patch) - 1
bi, bj = lowermost(patch) + 1, rightmost(patch) + 1
si, sj = min(ai, bi), min(aj, bj)
ei, ej = max(ai, bi), max(aj, bj)
vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)}
hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)}
return frozenset(vlines | hlines)
def box(patch: Patch) -> Indices:
"""outline of patch"""
if len(patch) == 0:
return patch
ai, aj = ulcorner(patch)
bi, bj = lrcorner(patch)
si, sj = min(ai, bi), min(aj, bj)
ei, ej = max(ai, bi), max(aj, bj)
vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)}
hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)}
return frozenset(vlines | hlines)
def shoot(start: IntegerTuple, direction: IntegerTuple) -> Indices:
"""line from starting point and direction"""
return connect(start, (start[0] + 42 * direction[0], start[1] + 42 * direction[1]))
def occurrences(grid: Grid, obj: Object) -> Indices:
"""locations of occurrences of object in grid"""
occurrences = set()
normed = normalize(obj)
h, w = len(grid), len(grid[0])
for i in range(h):
for j in range(w):
occurs = True
for v, (a, b) in shift(normed, (i, j)):
if 0 <= a < h and 0 <= b < w:
if grid[a][b] != v:
occurs = False
break
else:
occurs = False
break
if occurs:
occurrences.add((i, j))
return frozenset(occurrences)
def frontiers(grid: Grid) -> Objects:
"""set of frontiers"""
h, w = len(grid), len(grid[0])
row_indices = tuple(i for i, r in enumerate(grid) if len(set(r)) == 1)
column_indices = tuple(j for j, c in enumerate(dmirror(grid)) if len(set(c)) == 1)
hfrontiers = frozenset({frozenset({(grid[i][j], (i, j)) for j in range(w)}) for i in row_indices})
vfrontiers = frozenset({frozenset({(grid[i][j], (i, j)) for i in range(h)}) for j in column_indices})
return hfrontiers | vfrontiers
def compress(grid: Grid) -> Grid:
"""removes frontiers from grid"""
ri = tuple(i for i, r in enumerate(grid) if len(set(r)) == 1)
ci = tuple(j for j, c in enumerate(dmirror(grid)) if len(set(c)) == 1)
return tuple(tuple(v for j, v in enumerate(r) if j not in ci) for i, r in enumerate(grid) if i not in ri)
def hperiod(obj: Object) -> Integer:
"""horizontal periodicity"""
normalized = normalize(obj)
w = width(normalized)
for p in range(1, w):
offsetted = shift(normalized, (0, -p))
pruned = frozenset({(c, (i, j)) for c, (i, j) in offsetted if j >= 0})
if pruned.issubset(normalized):
return p
return w
def vperiod(obj: Object) -> Integer:
"""vertical periodicity"""
normalized = normalize(obj)
h = height(normalized)
for p in range(1, h):
offsetted = shift(normalized, (-p, 0))
pruned = frozenset({(c, (i, j)) for c, (i, j) in offsetted if i >= 0})
if pruned.issubset(normalized):
return p
return h