I have a 3D numpy array which represents a mask. The "1" elements of this mask are areas where no calculation is done (blue areas in the figure below). The "0" elements of the mask are areas for which a calculation is made.
Each calculation is imperatively realized on a cuboid block of minimal edges Nmin
. So I try to find the best distribution of cuboids to fill the "0" regions of this mask.
Here is an example of mask : mask.npy
And its 3D representation :
mask = np.load('mask.npy')
fig = plt.figure()
ax1 = fig.add_subplot(projection='3d')
ax1.voxels(mask)
plt.show()
It is a packing problem. The goal is to pack various cuboids into a larger cuboid with some constraints (the blue areas). There are a lot of possible combinations.
The way I see it to solve this problem is to determine first the largest "0" area and then repeat this process until the mask is completely filled.
I already did this kind of thing in 2D (see fdgrid), but I'm stuck on this 3D problem. how can I find the largest block (cuboid) of '0' ?
EDIT 1:
I have a draft solution that is a bit brutal and very slow with this 50x50x50 grid.
import time
def cuboid_list(mask, Nmin=3):
""" List of all cuboids than could fit in the domain.
List is formated as (volume, (Lx, Ly, Lz)). """
size = mask[mask==0].size
cuboids = [( i*j*k, (i, j, k)) for i, j, k in itertools.product(range(Nmin, nx), range(Nmin, ny), range(Nmin, nz)) if i*j*k <= size]
cuboids.sort(reverse=True)
return cuboids
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
for o in itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz)):
tested += 1
if (mask[o[0]:o[0]+Lx, o[1]:o[1]+Ly, o[2]:o[2]+Lz] == 0).all():
origin, size = o, (Lx, Ly, Lz)
break
if origin:
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
# Load mask
mask = np.load('mask.npy')
# List of all cuboids
cuboids = cuboid_list(mask)
# Search biggest
sub = search_biggest(mask, cuboids)
In this particular case, this leads to 2795610 configurations tested in 164.984 s ! The largest cuboid has origin=(16, 16, 0)
and size=(33, 33, 49)
.
To make the algorithm usable for larger geometries (e.g. 1024x1024x512 grid), I need to speed up this process.
EDIT 2:
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
# List of nonzero indexes
forbid = set([tuple(i) for i in np.argwhere(mask != 0)])
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
# remove cuboid with origin in another cuboid and iterate
for o in set(itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz))).difference(forbid):
tested += 1
if (mask[o[0]:o[0]+Lx, o[1]:o[1]+Ly, o[2]:o[2]+Lz] == 0).all():
origin, size = o, (Lx, Ly, Lz)
break
if origin:
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
Reducing the possibilities leads to 32806 configurations tested in ~2.6s.
Any ideas ?
EDIT 3:
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
# List of nonzero indexes
forbid = set([tuple(i) for i in np.argwhere(mask != 0)])
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
tested += 1
tmp = [i for i in itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz)) if i not in forbid]
tmp = [i for i in tmp if
all(x not in forbid for x in itertools.product(range(i[0], i[0]+Lx), range(i[1], i[1]+Ly), range(i[2], i[2]+Lz)))]
if tmp:
origin, size = tmp[0], (Lx, Ly, Lz)
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
Simple is better than complex ! Alternative implementation of search_biggest
leads to 5991 configurations tested in 1.017 s. !
Maybe, I can reduce the list of cuboids to speed up things, but I think this is not the way to go. I tried with a 500x500x500 grid and stopped the script after an hour without a result...
I have two other possible formulations in mind:
Fill the cuboid with elementary cuboids of size
Nmin x Nmin x Nmin
then merge the cuboids having common faces. The problem is that in some situations, it is not possible to fill an empty region only with cuboids of this size.Focus the search for empty rectangles on a plane (x, y, z=0) then extend them along z until a constraint is encountered. Repeat this operation for z $\in$ [1, nz-1] then merge cuboids that can be merged. The problem is that I can end up with some empty regions smaller than an elementary cuboid.
Always in search for a better way to solve this problem...