Search code examples
pythonarrayspython-3.xindexingfinite-element-analysis

How to determine neighboring elements from a 1D array?


I am working on a Finite element analysis code and I currently have 1D array listing the element density values like so:
x = np.ones(12) where the index is the element number 0, 1, 2, ..., 10, 11
The elements when plotted are like so:

0 - 3 - 6 - 9
1 - 4 - 7 - 10
2 - 5 - 8 - 11

I set the number of elements in the x and y direction (for this case 4 in the x and 3 in the y) however am having difficulty determining the surround elements. I need to find a way to determine the 3, 5 or 8 elements which surround a given elements. For example, if I select element 0 the surrounding elements are 1, 3, 4 or if I select element 6 the surrounding elements are 3, 4, 7, 9, 10 or if if I select element 7 the surround elements are 3, 4, 5, 6, 8, 9, 10, 11...
The end goal here would be to put in a radius and based on it determine the element numbers surrounding a selected element. Any advice or help with this would be greatly appreciated. For some reason I am unable to determine the logic to do this in python.


Solution

  • determine the logic to do this

    • A 1-d array with 6 items has the indices - [0,1,2,3,4,5]
    • A proposed shape is 2 rows and 3 columns - M,N = 2,3.
    • for any item's index (i) its row and column are c,r = divmod(i,M)
    • The neighbor column,row indices will be
      • cplus,cminus = c + 1, c - 1
        rplus, rminus = r + 1, r - 1
        cplus,r
        cminus,r
        c,rplus
        c,rminus
        cplus,rplus
        cplus,rminus
        cminus,rplus
        cminus,rminus
        
    • those 2d indices need to be converted to 1d indices with (col * M) + row

    For example

    [0,1,2,3,4,5]
    M,N = 2,3
    '''
    0  2  4
    1  3  5
    '''
    
    • item 4's 2d index is c,r = divmod(4,M) --> (2,0) (col,row)

    • one of its neighbor's 2d index is c,rplus --> (2,1)

    • that neighbor's 1d index is (2 * M) + 1 --> 5

    • after converting the neighbors' 2d indices to 1d you will need to check for and discard some that don't make sense.

      • item 4 is in the top-right a corner and doesn't have some neighbors like c,rminus which would be (2,-1) which does not make sense. Or cplus,r ... (3,0) which also does not make sense.

    Caveat - I did NOT try to thoroughly test this.


    Here is a function that returns a callable.

    import operator
    def get_neighbors(index, shape=(M,N)):
        '''Returns a callable.
    
        (M,N) --> (number_of_rows, number_of_columns)
        '''
        M, N = shape
        # 2d index
        c, r = divmod(index, M)
        print(f'2d index: {(c,r)}')
        # neighbors
        cplus, cminus = c + 1, c - 1
        rplus, rminus = r + 1, r - 1
        # dot product of (c,cplus,cminus) and (r,rplus,rminus)?
        neighbors = [
            (cminus, rminus),
            (cminus, r),
            (cminus, rplus),
            (c, rplus),
            (cplus, rplus),
            (cplus, r),
            (cplus, rminus),
            (c, rminus),
        ]
        # print(neighbors)
    
        # validate/filter
        neighbors = [
            (col, row) for col, row in neighbors if (0 <= col < N) and (0 <= row < M)
        ]
        # print(neighbors)
    
        # 1d indices
        one_d = [(col * M) + row for col,row in neighbors]
        # print(one_d)
        return operator.itemgetter(*one_d)
    

    Try it out.

    >>> a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't']
    
    >>> M,N = 4,5    # nrows, ncols
    
    '''
    [['a' 'e' 'i' 'm' 'q']
     ['b' 'f' 'j' 'n' 'r']
     ['c' 'g' 'k' 'o' 's']
     ['d' 'h' 'l' 'p' 't']]
    '''
    
    >>> # i's neighbors
    >>> q = get_neighbors(a.index('i'),(M,N))
    2d index: (2, 0)
    >>> q(a)
    ('e', 'f', 'j', 'n', 'm')
    >>>
    >>> # k's neighbors
    >>> q = get_neighbors(a.index('k'),(M,N))
    2d index: (2, 2)
    >>> q(a)
    ('f', 'g', 'h', 'l', 'p', 'o', 'n', 'j')
    >>>
    >>> # q's neighbors
    >>> q = get_neighbors(a.index('q'),(M,N))
    2d index: (4, 0)
    >>> q(a)
    ('m', 'n', 'r')
    >>>
    

    i's neighbors for different shapes

    >>> M,N = 5,4
    >>> q = get_neighbors(a.index('i'),(M,N))
    2d index: (1, 3)
    >>> q(a)
    ('c', 'd', 'e', 'j', 'o', 'n', 'm', 'h')
    >>> M,N = 10,2
    >>> q = get_neighbors(a.index('i'),(M,N))
    2d index: (0, 8)
    >>> q(a)
    ('j', 't', 's', 'r', 'h')
    >>> M,N = 2,10
    >>> q = get_neighbors(a.index('i'),(M,N))
    2d index: (4, 0)
    >>> q(a)
    ('g', 'h', 'j', 'l', 'k')
    >>>
    

    There is a nice discussion in the Numpy docs about making/treating a 1d thing as a Nd thing - Internal memory layout of an ndarray

    The way you depicted your 1d --> 2d transformation used a column major scheme. I'm used to thinking row-major - I wrote the function to accept/expect a (nrows,ncols) shape argument but inside the function I kinda switched to column major processing. I was having to be careful so maybe that was a bad design.