I'd like to order an arbitrary number of elements into an arbitrarily shaped matrix (matrix_a
) based on values in a binary matrix (element_map
) that describes attributes of the elements. matrix_b
defines which elements
can be adjacent to each other in matrix_a
. "Adjacent" in this case includes diagonal. A concrete, but toy example:
import numpy as np
#This is a just a mask of the final matrix, to show the shape.
#The center position (5) is adjacent to all other positions whereas
#position (1) is adjacent to 4, 2 and 5, etc.
matrix_a = np.array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
elements = range(0, 10)
#each 'row' corresponds to an element
#each 'column' corresponds to an attribute of the various elements
#here, each element has 5 attributes, which are always 0 or 1
element_map = np.array([[0, 0, 1, 0, 1], #element 1
[1, 1, 1, 0, 1], #element 2
[1, 0, 0, 1, 0], #element 3
[1, 0, 1, 0, 0], #etc.
[1, 1, 1, 0, 0],
[1, 1, 0, 1, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 0, 0],
[1, 1, 0, 0, 0]]) #element 9
The desired output is that the nine elements
are placed in matrix_a
, each only appearing once, based on an adjacency rule for which matrix_b
is needed. The rule is that for any element
placed in matrix_a
, the element
's value for attribute x
(can be any attribute) must be zero, and the value of all adjacent (adjacent in matrix_a
) elements
must be 1 for attribute x
.
Assume also that we have a function that can accept a matrix and coordinates and return all the adjacent values:
def adj(m, x, y):
''' find adjacent values to coordinates x,y in a matrix, m'''
if x == 0:
if y == 0:
r = m[0:2,0:2] # x==0, y==0
else:
r = m[0:2,y-1:y+2] # x==0, y!=0
elif y == 0: # x != 0, y == 0
r = m[x-1:x+2,0:2]
else: #any other position
r = m[(x-1):(x+2),(y-1):(y+2)]
return [i for i in r.flatten().tolist() if i != m[x,y]]
#example:
q = np.arange(1,97).reshape(12,8).T
array([[ 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89],
[ 2, 10, 18, 26, 34, 42, 50, 58, 66, 74, 82, 90],
[ 3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91],
[ 4, 12, 20, 28, 36, 44, 52, 60, 68, 76, 84, 92],
[ 5, 13, 21, 29, 37, 45, 53, 61, 69, 77, 85, 93],
[ 6, 14, 22, 30, 38, 46, 54, 62, 70, 78, 86, 94],
[ 7, 15, 23, 31, 39, 47, 55, 63, 71, 79, 87, 95],
[ 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96]])
adj(q, 5, 5)
[37, 45, 53, 38, 54, 39, 47, 55]
As a side note, the possible search space for an 8x12 matrix and 96 elements is 10**149 = 96!
Maybe this would be a job for linear/integer programming, but I'm not sure how to formulate the adjacency constraints
This can be done with an ILP (Integer Linear Program). A rough cut is shown below. It seems that you have no weighting declared to consider one result superior to another, so you might also have luck with a CP (Constraint Programming) approach. The ILP below uses the number of successful element fills as an objective. There may be many solutions (or none), and it will just grab the first discovered.
Of note, the example you provided has no solution for a 3x3 target matrix. I used the segment below that to make elements from the identity matrix (flipped 1/0), which--by inspection--is the best at fitting element list possible.
I tweaked your
adj()
function a bit because you had a mixed schema where you were taking in (x, y) and returning a numbered position.... I just made it all (x, y)If there is a solution, this tends to find it very quickly for moderate sizes of
matrix_a
, especially if the solution set is large.I used
cbc
solver here, which is nice freeware, but there are other MIP solvers that work fine (glpk, highs, ...)There is still some "meat on the bone" here to optimize this a bit, mostly by domain trimming the
[e, p, a]
tuples down to legal assignments based on thea
value, meaning anyplace[e, p, a]
where elemente
has a non-zero for attributea
is not legal and should be removed from the domain forplace
. Then you might look at pre-computing the possible neighbors for any given element to reduce the size of theadjacency
constraint a bit.Code:
Output (truncated):