Giter Club home page Giter Club logo

Comments (7)

tsrasmussen avatar tsrasmussen commented on May 27, 2024

n-polycube and n-cell inheritance scheme

n-cells can inherit (n-1)-polycubes from all (n-1)-cells that fit into n-cell.

Make a dictionary with keys of cells and values of lists of possible cells to inherit polycubes from.

cell_inheritance_dict = {}

# Put cells in dict as keys
n = 4
for i in range(1, n + 1):
    cells = find_unique_cells(i)
    for cell in cells:
        if cell not in cell_inheritance_dict:
            cell_inheritance_dict[cell] = None

# Update values for keys in dict
for key in cell_inheritance_dict:
    cell = key
    previous_cells = []
    if cell[2] > 1:
        previous_cell = (cell[0], cell[1], cell[2] - 1)
        previous_cells.append(previous_cell)
    if cell[1] > cell[2]:
        previous_cell = (cell[0], cell[1] - 1, cell[2])
        previous_cells.append(previous_cell)
    if cell[0] > cell[1]:
        previous_cell = (cell[0] - 1, cell[1], cell[2])
        previous_cells.append(previous_cell)

    cell_inheritance_dict.update({cell: previous_cells})

Example - Polycubes and cells of dimension n = 4

Current cell: (1, 1, 1) Inherits polycybes from: []
Current cell: (2, 1, 1) Inherits polycybes from: [(1, 1, 1)]
Current cell: (3, 1, 1) Inherits polycybes from: [(2, 1, 1)]
Current cell: (2, 2, 1) Inherits polycybes from: [(2, 1, 1)]
Current cell: (4, 1, 1) Inherits polycybes from: [(3, 1, 1)]
Current cell: (3, 2, 1) Inherits polycybes from: [(3, 1, 1), (2, 2, 1)]
Current cell: (2, 2, 2) Inherits polycybes from: [(2, 2, 1)]

from cubes.

jamesalewis avatar jamesalewis commented on May 27, 2024

This was my thought as well, noting that all possible polycubes will be contained in a space roughly 1/6 the size of its bounding cube. This space, being a right tetrahedron (3D version of a right triangle) can be a 1kb datum (969 bits) for a 17-polycube. This tetrahedronal array will be n×(n+1)×(n+2)/6 cells.

We can then run it through all its possible rotations and store the lowest value, treating it like a really big int. That really big int is a lossless representation of the shape which we can use to look up in the list of found shapes.

Alternatively, we can use a hash of that datum, and store that in a lookup and double check with the source if we find a match. That hashing step may also save on memory by not needing the whole array of found shapes in memory, only their hashes, when checking for matches.

Rotating and translating to find the "minimum" will eliminate any need for rotation later.

(PS: because of treating it like a big int, any rotation we do will either totally change the int value or reveal symmetry, so we don't need to worry about having to store multiple rotational versions of a found shape.)

(PPS: In reality, that maximum cellular array can be trimmed down yet more, since a rotation of a poly cube will favor one axis over the others, and the rotationally minimum shape will never reach two, let allone all three, extremes. For example, take a 10-polycube with legs stretching out along each axis equally: this is bound not to the 10-unit tretrahedron but 4 units, and moving any of the ends further out will only find a minimum rotational position along the primary axis. I have yet do a little more thinking on this to see if we can reliably represent in a tighter array than in n×(n+1)×(n+2)/6 cells)

from cubes.

jamesalewis avatar jamesalewis commented on May 27, 2024

Doing a little more thinking on this, we may be able to trim the cellular array further by about half. The reason is that we will find the "minimum rotation" will be contained in a space somewhat like this:

image

This is for a 10-polycube. With some good spatial reasoning, you'll find that any arrangement which reaches outside these bounds has a rotation that fits within it.

from cubes.

MyrddinE avatar MyrddinE commented on May 27, 2024

If the goal is to find a unique rotation for every permutation, then it seems that this would be the appropriate algorithm:

  1. Create a working space that is n^3 (naieve storage), generate the 24 permutations, and shift each permutation to the minimum corner of the cube.
  2. Convert each rotation to a 1d bit array.
  3. Find the 'minimum' array, when treated like an integer.

Once you've done that, you'll AUTOMATICALLY have selected the rotation that fits within the bounding box described, with the long dimension corresponding to the low (1s) coordinate, and the two medium dimensions corresponding to the 10s and 100s 'place' on the flattened coordinate.

As an example let's look at a 10 polycube. The line, when shifted to the minimum position, results in 3 unique results. The smallest is where the binary 1s extend through the 1s position, so you get {0,0,0,0,0,[985 more 0s],1,1,1,1,1,1,1,1,1,1}. The medium sized result (again, when treated like a binary number) is when the digits extend along the second axis; if the digits were numbered from the lowest value to the highest 1s would appear at position 1, 11, 21, 31, 41, 51, 61, 71, 81, and 91. In binary, it would look like {0,0,0,0,0,[905 more 0s],1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,[repeat 7 more times],1,0,0,0,0,0,0,0,0,0,1}
And finally, if it extended along the 100s axis, the digits would appear at positions 1, 101, 201, 301, etc.

The point being that if you convert all 24 rotations to binary in the n^3 cube, taking the minimum of the 24 results (viewed as a binary number) will automatically get us two things:

  1. A unique rotation, so we only have to store it once.
  2. A rotation that automatically fits within the minimum shape described by @jamesalewis, so we can use fewer bits to store it.

In fact, once we have determined this minimum rotation we can renumber it in binary as fitting within the minimum shape. It would need to be 'unpacked' before use (since the bit array wouldn't correspond to cuboid coordinate points).

from cubes.

jamesalewis avatar jamesalewis commented on May 27, 2024

Another thing to note is that what I described at first is the container for all rotations of every valid polycube which intersects with the three origin planes, but the "minimum rotation" will fit in a significantly smaller space. I've done a little more thinking, and I believe a right tetrahedronal space is still the correct shape, but the three dimensions will be roughly n × ½n × ⅓n (the limit as n→∞, obviously rounding up for the finite cases) because any polycube that extends outside this space can be rotated, transposed, and/or flipped to fit within it. To demonstrate, let's consider the extremities of a 2D polysquare for n=5 (square L, Long L, and Line):

image

I haven't yet proven the hypothesis that all polycubes will fall within this n × ½n × ⅓n tetrahedronal space, but it appears true. This will quickly turn from a computerphile into a numberphile topic as we go into the third dimension, but I am increasingly convinced of it.

Here's the 2D cases for n=1-6, along with a persistent ID scheme (yes, I have an error in the screenshot, holdover from an ID scheme change):

image

EDIT: What this might look like in 3D:

image

from cubes.

tsrasmussen avatar tsrasmussen commented on May 27, 2024

Doing a little more thinking on this, we may be able to trim the cellular array further by about half. The reason is that we will find the "minimum rotation" will be contained in a space somewhat like this:

image

This is for a 10-polycube. With some good spatial reasoning, you'll find that any arrangement which reaches outside these bounds has a rotation that fits within it.

How do you construct that space for a given n? What are you boundary conditions?

Looking at the cells proposed earlier, for the n = 10 case you would get the "supercell" or space that will be guaranteed to fit all possible 10-polycubes as:
generation_10

It looks as if there is an in-between with this and the other structure, that will be a bit smaller than either. For the n=10 case, it looks likes it is trimming a row of the top layer, but I'd like to know why we can do this.

The other supercell/spaces up to n = 12:
all_shapes

I've tried writing some code that orients the polycubes in the *.npy files in a way to force them into a rotation that prioritizes the x >= y and y >= z constraints. This effectivly shifts them to the minimum corner as MyrddinE mentioned.

The steps for a given polycube are:

  1. Ensure polycube.shape : x >= y and y >= z
  2. Perform all rotations and select all that have a cube in the lowest corner, polycube[0,0,0,] == 1
    If no rotation satisfies this, choose the original polycube
  3. Select all rotations that still ensure polycube.shape : x >= y and y >= z
  4. Select all rotations that have the highest value of ones in the first row
    • This selects all those that favour a generation scheme that prioritizes x >= y and y >= z
  5. Discard identical polycubes
  6. Select the polycube that has 1s at the lowest indices, indices = np.argwhere(polycube == 1)
    • This selects the polycube oriented with most cubes towards the minimum corner

I haven't thought too much of optimizing this, nor the code used which is rather messy, but was a way for me to keep track.
I've included the code below. The python render_shapes2 function is just something used to visualize the superposition of all oriented polycubes from an appropriate point of view. This superposition must fit within the supercell.

def render_shapes2(shapes, path, elev=30, azim=45, title=None):
    n = len(shapes)
    dim = max(max(a.shape) for a in shapes)
    i = int(np.ceil(np.sqrt(n)))
    voxel_dim = dim * i
    voxel_array = np.zeros((voxel_dim + i, voxel_dim + i, dim), dtype=np.byte)
    pad = 1
    for idx, shape in enumerate(shapes):
        x = (idx % i) * dim + (idx % i)
        y = (idx // i) * dim + (idx // i)
        xpad = x * pad
        ypad = y * pad
        s = shape.shape
        voxel_array[x : x + s[0], y : y + s[1], 0 : s[2]] = shape

    voxel_array = voxel_array[:voxel_dim, :voxel_dim, :]
    colors = np.empty(voxel_array.shape, dtype=object)
    colors[:] = "#FFD65DC0"

    ax = plt.figure(figsize=(20, 16), dpi=600).add_subplot(projection="3d")
    ax.view_init(elev=elev, azim=azim)  # Set the elevation and azimuth angles
    ax.voxels(voxel_array, facecolors=colors, edgecolor="k", linewidth=0.1)

    ax.set_xlim([0, voxel_array.shape[0]])
    ax.set_ylim([0, voxel_array.shape[1]])
    ax.set_zlim([0, voxel_array.shape[2]])
    ax.set_box_aspect((1, 1, voxel_array.shape[2] / voxel_array.shape[0]))

    # Add the title if provided
    if title:
        ax.set_title(title, fontsize=50)

    plt.axis("off")
    plt.savefig(path + ".png", bbox_inches="tight", pad_inches=0)

    plt.close()


def orient_polycube_for_shape(polycube):
    x, y, z = polycube.shape

    # Rotate the polycube to have x >= y and y >= z
    if x < y:
        polycube = np.rot90(
            polycube, k=1, axes=(1, 0)
        )  # Rotate 90 degrees in the x-y plane
    if y < z:
        polycube = np.rot90(
            polycube, k=1, axes=(2, 1)
        )  # Rotate 90 degrees in the y-z plane

    return polycube


def are_arrays_equal(array1, array2):
    """
    Checks if two arrays are equal element-wise.

    Args:
      array1: NumPy array.
      array2: NumPy array.

    Returns:
      True if the arrays are equal element-wise, False otherwise.
    """
    return np.array_equal(array1, array2)



def orient_polycubes(polycubes):
    best_rotations = []

    for polycube in polycubes:
        oriented_polycube = orient_polycube_for_shape(polycube)
        possible_rotations = []

        for rotation in all_rotations(oriented_polycube):
            if rotation[0, 0, 0] == 1:
                possible_rotations.append(rotation)

        if not possible_rotations:
            possible_rotations.append(oriented_polycube)

        # Filter possible_rotations based on shape constraint x >= y and y >= z
        filtered_rotations = [
            rotation
            for rotation in possible_rotations
            if rotation.shape[0] >= rotation.shape[1]
            and rotation.shape[1] >= rotation.shape[2]
        ]

        # Calculate sum of ones in the first row for each filtered rotation
        sums = [np.sum(rotation[:, 0, 0]) for rotation in filtered_rotations]

        # Find the maximum sum and corresponding rotations
        max_sum = max(sums)
        max_sum_rotations = [
            filtered_rotations[i] for i, s in enumerate(sums) if s == max_sum
        ]

        # Remove duplicates from max_sum_rotations
        unique_rotations = []
        for rotation in max_sum_rotations:
            if not any(
                are_arrays_equal(rotation, unique_rot)
                for unique_rot in unique_rotations
            ):
                unique_rotations.append(rotation)

        # Select the rotation with the most 1s at the lowest indices
        best_rotation = None
        lowest_index_sum = None
        for unique_rotation in unique_rotations:
            indices_sum = np.sum(np.argwhere(unique_rotation == 1))
            if lowest_index_sum is None or indices_sum < lowest_index_sum:
                best_rotation = unique_rotation
                lowest_index_sum = indices_sum

        best_rotations.append(best_rotation)

    return best_rotations


def generate_superposition(polycubes):
    # Find the dimensions of the bounding box that encompass all polycubes
    max_x = max(polycube.shape[0] for polycube in polycubes)
    max_y = max(polycube.shape[1] for polycube in polycubes)
    max_z = max(polycube.shape[2] for polycube in polycubes)

    # Create an empty 3D numpy array for the superposition
    superposition = np.zeros((max_x, max_y, max_z), dtype=np.byte)

    # Place each polycube in the superposition
    for polycube in polycubes:
        indices = np.argwhere(polycube == 1)
        for index in indices:
            x, y, z = index
            superposition[x, y, z] = 1

    return superposition


arrays = np.load("cubes_6.npy", allow_pickle=True)
oriented_arrays = orient_polycubes(arrays)
super_pos = [generate_superposition(oriented_arrays)]
render_shapes2(super_pos, "super_pos_6", title="Superposition of n = 6 polycubes")

It's a bit messy, but when creating a super position of the resulting polycubes I get these shapes for n = 4, 5, 6 (pardon the size of the pictures):
super_pos_4
super_pos_5
super_pos_6

Aside from n = 4, the superposition of the polycubes seem to fill the entire supercell.
I'm not sure why, but I could not use the *.npy files for n larger than 6, and as of yet I have no idea why. I get the following error:

line 196, in orient_polycubes
    max_sum = max(sums)
              ^^^^^^^^^
ValueError: max() arg is an empty sequence

I'm unsure of the feasibility of a scheme implementing something like the algorithm for orienting a polycube.
You wouldn't have to check each rotation against all other polycubes, as you always choose a specific orientation, but you would have to generate all rotations of a polycube and filter those in a way that ensures uniqueness.

from cubes.

MyrddinE avatar MyrddinE commented on May 27, 2024

@tsrasmussen, note that finding the minimum dimension (x>y>z) is not the same as finding the actual minimum orientation. It will also not guarantee a unique orientation for a given shape, making duplicate detection harder.

7cube-rotation

For example, both these (identical) n=7 polycubes are the same x, y, and z dimensions but they are not both the minimum rotation when interpreting the bit representation as an integer. Looking at your code, I did not see anything that would ensure one orientation was preferred over the other (I may have missed it) when they had the same dimensions. Without that, duplicate detection becomes more difficult.

from cubes.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.