0

I have a matrix defined mxn 128x128. And I have translated my 2d x,y positions onto this 1D matrix grid. My 2d coordinates accept positions using numbers 0->127 i.e. any combo in ranges {x=0,y=0}-->{x=127,y=127}. I'm implementing algorithms that take the neighboring positions of these nodes. Specifically the 8 surrounding positions of distance i (lets say i=1). So considering node={0,0}, my neighbours are generated by adding these vectors to said node:

two_d_nodes={
        {0,i*1},{0,-i*1},{-i*1,0},{i*1,0},
        {i*1,i*1},{i*1,-i*1},{-i*1,-i*1},{-i*1,i*1}
    }

In terms of 2d though I am excluding neighbours outside the boundary. So in the above for node={0,0}, only neighours {0,1},{1,1}{1,0} are generated. Setting the boundary is basically just implementing some form of:

if x>=0 and y>=0 and x<=127 and y<=127 then... 

The 1d translation of node={0,0} is node={0} and my vector additions translated to 1d are:

one_d_nodes={{128},{-128},{-1},{1},{129},{-127},{-129},{127}}

However the relationship with the 2d boundary expressions doesn't hold true here. Or at least I don't know how to translate it. In response I tried generating all the loose cases of the grid:

{0,127,16256,16383} --the 4 corner positions  
node%128==0 --right-side boundary  
node%128==1 --left-side boundary  
node>1 and node<128 --top-side boundary  
node>127*128 and node<128*128 --bottom-side boundary    

Then tried implementing special cases....where I just ignored generating the specific out of bounds neighbours. That was messy, and didn't even work for some reason. Regardless I feel I am missing a much cleaner method.

So my question is: How do I translate my 2d boundaries onto my 1d grid for the purposes of only generating neighbours within the boundary?


The following is in regards to the answer below:

function newmatrix(node) --node={x=0,y=0}
    local matrix={}
    add(matrix,{(node.y<<8)+node.x}) --matrix= {{0},...}
--lets say [1 2 3] is a width=3; height=1 matrix, 
--then the above line maps my 2d coord to a matrix of width=256, height=128 
    matrix.height, matrix.width = #node,#node[1] --1,1
    return matrix
 end

function indexmatrix(matrix, r,c)
    if r > 1 and r <= matrix.height and c > 1 and c <= matrix.width then
        return matrix[matrix.width * r + c]
    else
        return false
    end
end

function getneighbors(matrix, r, c)
    local two_d_nodes={
        {0,1},{0,-1},{-1,0},{1,0},
        {1,1},{1,-1},{-1,-1},{-1,1}
    }
    local neighbors = {}
    for index, node in ipairs(two_d_nodes) do
        table.insert(neighbors, indexmatrix(matrix, r + node[1], c + node[2]))
    end
    return neighbors
end
--Usage:
m={x=0,y=0}
matrix=newmatrix(m) --{{0}}
--here's where I'm stuck, cause idk what r and c are
--normally I'd grab my neighbors next....
neighbors=getneighbors(matrix)
--then I have indexmatrix for...?
--my understanding is that I am using indexmatrix to
--check if the nieghbors are within the bounds or not, is that right?
--can you illustrate how it would work for my code here, it should
--toss out anything with x less then 0 and y less than 0. Same as in OP's ex 
indexmatrix(matrix) ---not sure what to do here

Attempt 2 in regards to the comment sections below:

function indexmatrix(matrix, x ,y)
    if x > 1 and x <= matrix['height'] and y > 1 and y <= matrix['width'] then
        return matrix[matrix['width'] * x + y]
    else
        return false
    end
end
function getneighbors(matrix, pos_x, pos_y)
    local two_d_nodes={
        {0,1},{0,-1},{-1,0},{1,0},
        {1,1},{1,-1},{-1,-1},{-1,1}
    }
    local neighbors = {}
    for _, node in ipairs(two_d_nodes) do
        add(neighbors, indexmatrix(matrix, pos_x + node[1], pos_y + node[2]))
    end
    return neighbors
end

matrix={} --128 columns/width, 128 rows/height 
for k=1,128 do
add(matrix,{}) ----add() is same as table.insert()
    for i=1,128 do
        matrix[k][i]=i
    end
end

id_matrix={{}} --{ {1...16k}}
for j=1,128*128 do
    id_matrix[1][j]=j
end
id_matrix.height, id_matrix.width = 128,128 
    
position={x=0,y=0}
neighbors = getNeighbors(matrix, position.x, position.y)

Attempt 3: A working dumbed down version of the code given. Not what I wanted at all.

function indexmatrix(x,y)
    if x>=0 and y>=0 and x<127 and y<127 then
        return 128 * x + y
    else
        return false
    end
end
function getneighbors(posx,posy)
    local two_d_nodes={
        {0,1},{0,-1},{-1,0},{1,0},
        {1,1},{1,-1},{-1,-1},{-1,1}
    }
    local neighbors = {}
    for _, node in pairs(two_d_nodes) do
        add(neighbors, indexmatrix(posx+node[1], posy + node[2]))
    end
    return neighbors
end

pos={x=0,y=10}
neighbors = getneighbors(pos.x,pos.y)
kite
  • 309
  • 2
  • 12
  • Just to clarify, you have a function (like `mat:index(0,0)`) that converts given coordinates to 1d position? If so, you should be able to validate `two_d_nodes` for a given element and return the valid ones. An easy way would be for the method that accesses the matrix to check if the arguments are "in bounds" and return nil if not. – Optimum Aug 06 '21 at 15:45
  • how do i check if the 1d are 'in bounds'? that's the question. – kite Aug 07 '21 at 00:28
  • 1
    This is impossible in general, no? All 1d indexes from 0..16383 are "in bounds" in that they correspond to an in-bounds location on the 128x128 matrix. You can't tell from a 1d index on its own whether it's out of bounds. You need to know not just the index but also the movement that got you there. By the time you have only the 1d index it's too late. Consider extreme cases like `i=64` and `i=127`. – Weeble Aug 17 '21 at 13:10
  • @Weeble correct, multiple 2D positions can map to the same 1D index without constraining the ranges of the 2D representation. It's intuitive if you examine the function to map 2D to 1D and realize that it's a multivariable eqn (y = m*x + z). For it to be solvable, a system of equations is needed, but x and z are independent of one another. The only relations available are `0 <= x <= 128` and `0 <= z <= 128`, which cannot be used in a 1D context. @kite I have added this explanation to my answer. – Optimum Aug 18 '21 at 14:48
  • 1
    I'm wondering, given the "pico-8" tag, is there perhaps an unstated requirement for the code to be as succinct as possible (since pico 8 has both a token limit and a compressed size limit), and an unstated assumption that working with 1D coordinates will result in shorter code? I think that is worth stating up-front, as a) that's not obvious, and b) there might be ways to address those concerns. – Weeble Aug 18 '21 at 16:41

1 Answers1

1

Edit: The equation to map 2D coordinates to 1D, y = mx + z, is a function of two variables. It is not possible for a multivariable equation to have a single solution unless a system of equations is given that gets x or z in terms of the other variable. Because x and z are independent of one another, the short answer to the question is: no

Instead, the constraints on x and z must be used to ensure integrity of the 1D coordinates.

What follows is an example of how to work with a 1D array as if it were a 2D matrix.

Let's say we have a constructor that maps a 2D table to a 1D matrix

local function newMatrix(m) -- m is 128x128 Matrix
    local Matrix = {}
    --logic to map m to 1D array
    -- ...
   return Matrix -- Matrix is m 1x16384 Array
end

The numeric indices are reserved, but we can add non-numeric keys to store information about the matrix. Let's store the number of rows and columns as height and width. We can do this in the constructor

local function newMatrix(m)
    local Matrix = {}
    --logic to map to 1D array
    -- ...
    -- Store row and column info in the matrix
    Matrix.height, Matrix.width = #m, #m[1] -- Not the best way
    return Matrix
end

Although the matrix is now a 1x16384 array, we can create a function that allows us to interact with the 1D array like it's still a 2D matrix. This function will get the value of a position in the matrix, but we return false/nil if the indices are out of bounds.

To be clear, the formula to map 2D coordinates to a 1D coordinate for a matrix, and can be found here:

1D position = 2D.x * Matrix-Width + 2D.y

And here's what that function could look like:

local function indexMatrix(Matrix, r,c)
    if r >= 1 and r <= Matrix.height and c >= 1 and c <= Matrix.width then
        return Matrix[Matrix.width * r + c] -- the above formula
    else
        return false -- out of bounds
    end
end

We can now index our Matrix with any bounds without fear of returning an incorrect element.

Finally, we can make a function to grab the neighbors given a 2D position. In this function, we add vectors to the given 2D position to get surrounding positions, and then index the matrix using the indexMatrix function. Because indexMatrix checks if a 2D position is within the bounds of the original Matrix (before it was converted), we only get neighbors that exist.

local function getNeighbors(Matrix, r, c) -- r,c = row, column (2D position)
    local two_d_nodes={
        {0,1},{0,-1},{-1,0},{1,0},
        {1,1},{1,-1},{-1,-1},{-1,1}
    }
    local neighbors = {}
    for index, node in ipairs(two_d_nodes) do
        -- Add each vector to the given position and get the node from the Matrix
        table.insert(neighbors, indexMatrix(Matrix, r + node[1], c + node[2]))
    end
    return neighbors
end

You can either skip elements that return false from indexMatrix or remove them after the fact. Or anything else that sounds better to you (this code is not great, it's just meant to be an example). Wrap it in a for i ... do loop and you can go out an arbitrary distance.

I hope I haven't assumed too much and that this is helpful. Just know it's not foolproof (the # operator stops counting at the first nil, for instance)

Edit: Usage

Matrix = {
    {1,2,3...128}, -- row 1
    {1,2,3...128},
    ...
    {1,2,3...128}, -- row 128
}

Array = newMatrix(Matrix) -- Convert to 1D Array ({1,2,3,...,16384})
--Array.width = 128, Array.height = 128
position = {x=0, y=0}
neighbors = getNeighbors(Array, position.x, position.y)
-- neighbors is: {{0,1}, false, false, {1,0}, {1,1}, false, false, false}
Optimum
  • 146
  • 2
  • 11
  • I don't understand what r and c are. Its checking if, whatever they are, are within the bounds of the matrix. Then there's this line" return matrix[matrix.width * r + c]" which also has me confused. I'm returning a key from the matrix with extremely high value, but isnt that out of the matrix scope aka nil...? I'm missing something here. I added code to the OP outlining an example with the functions you provided. Could you please walk me through the remaining parts. – kite Aug 12 '21 at 01:24
  • @kite I added example usage at the end and cleaned up the answer to add clarity. – Optimum Aug 12 '21 at 14:43
  • You define Array with Array.width and Array.height, but you don't use Array for anything. I don't understand, do you mean to call the 1d matrix instead of the 2d matrix in your functions....? You send me link to convert a 2d position into 1d by defining a grid of certain size....that's not what this code is saying though. This code is defining an entire 2d matrix into an entire 1d matrix. I don't understand. I reposted in OP my second attempt of code, please tell me what is wrong, I remove ur first function cause its just confusing. – kite Aug 13 '21 at 01:24
  • Okay. You didn't answer the question. I provided above a third version of your code....this one is working, outputting what yours is apparently doing for you? I can't get your code working as per the above. Regardless though it makes no sense. Why define a 2d matrix, why define a 1d matrix. Why reference the key of the 1d matrix to output the value which is the same as the key!?! WHy? THe OP was asking if, when in 1d, you could carry over the 2d boundary conditions. Converting back from 1d to 2d just to check boundary conditions was what I wanted to avoid. – kite Aug 13 '21 at 02:55
  • Given a 2d boundary and coordinates, convert the coordinates to 1d and convert its boundary condition to 1d so you don't have to deal with 2d anymore. If I'm looping the checking of neighbours I have to convert from 2d to 1d to 2d to 1d over...and over...and over to check my boundary...I want to avoid that. The neighbours generated will be in 1d! THe neighbours of neighbours of neighbours willl be in 1d! Only the initial position is 2d, the 2d corrdinates all die off after that point. – kite Aug 13 '21 at 03:03
  • @kite Fixed the Array/Matrix error (Also, not assigning Array.width/Array.height - that's a lua comment to illustrate the values. Chill). That being said, it's not possible to convert a 2D boundary to 1D with abandon because multiple positions can map to the same index. (1,129) and (2,1) equate to the same index, but the first is out-of-bounds. You need to validate your inputs, and a simple x*w+y check is the price that has to be paid. It's a fairly standard practice for 1D matrix representations. I answered the question from this assumption, so forgive me for that. – Optimum Aug 13 '21 at 12:41
  • 1
    Note the pico-8 tag. Pico 8 doesn't provide the full Lua standard library and renames some of the functions it does provide. That's probably the source of a lot of confusion and frustration here. For example instead of `table.insert` there is the `add` function. A list of differences can be found here: https://gist.github.com/josefnpat/bfe4aaa5bbb44f572cd0 – Weeble Aug 18 '21 at 16:44