Repository: projectmesa/mesa

Issue:
[Bug Report] Wrong result got from get_distance method for torus continuous space
  ```
  def get_distance(self, pos_1, pos_2):
        """ Get the distance between two point, accounting for toroidal space.

        Args:
            pos_1, pos_2: Coordinate tuples for both points.

        """
        pos_1 = np.array(pos_1)
        pos_2 = np.array(pos_2)
        if self.torus:
            pos_1 = (pos_1 - self.center) % self.size
            pos_2 = (pos_2 - self.center) % self.size
        return np.linalg.norm(pos_1 - pos_2)
```
Above is the get_distance method for continuous space in mesa 0.8.1.

Although I'm not clear about the algorithm here, when my input is
```
pos_1 = (50, 40)
pos_2 = (50, 50)
size = (100, 100)
torus = True
```
I got the result 90 rather than 10. 
So I wonder if there is any problem about this algorithm.


Discussion:
I personally haven't used the built in get_distance method since I write my own based on agent capability: euclidean, manhattan, etc. I believe the following code might work, I ran it with a couple use cases.

```python
def get_distance(self, pos_1, pos_2):
      """ Get the distance between two point, accounting for toroidal space.

      Args:
          pos_1, pos_2: Coordinate tuples for both points.

      """
      pos_1 = np.array(pos_1)
      pos_2 = np.array(pos_2)
      dist_1 = np.linalg.norm(pos_1 - pos_2)
      if self.torus:
          pos_1 = (pos_1 - self.center) % self.size
          pos_2 = (pos_2 - self.center) % self.size
      dist_2 = np.linalg.norm(pos_1 - pos_2)
      
      return min(dist_1, dist_2)
```
Hope this helps. 
@clmerlos Thanks for your reply.
Yes, the version you provided works well. 
Hope mesa would fix it in the next version.
I ran the tests with both versions and they both pass.
https://github.com/projectmesa/mesa/blob/master/tests/test_space.py#L34-L43

I applied both versions to Flockers example and they both run in the same manner. 

It is impossible I made a mistake along the way, but I don't see the issue. 
Can you provide me with explicit instructions on how to recreate it? 

@jackiekazil Thanks for your reply.

```
pos_4 = (20, -5)
pos_5 = (20, -15)
assert self.space.get_distance(pos_4, pos_5) == 10
```
As I tried, this test failed. I got the result 40 rather than 10.

Hope this would help.
@simonchx  can you take a look at the change that I submitted? https://github.com/projectmesa/mesa/pull/425 ... does it look good to you?
@jackiekazil  Thanks for the change. This one seems to work well.
The reason why it is 40 for https://github.com/projectmesa/mesa/issues/410#issuecomment-343377912 test case, given the params x_max = 70, y_max = 20, x_min = -30, y_min = -30, x_center = 20, y_center = -5, width = 100, height = 50: for the y coordinate, y = -15 - (-5) = -10 and -10 modulo 50 is 40 instead of -10.

I think the less opaque solution is to multiply the size with a sign of the x and y coordinate.
```
if self.torus:
    pos_1 -= self.center
    pos_1 = pos_1 % np.sign(pos_1) * self.size
    pos_2 -= self.center
    pos_2 = pos_2 % np.sign(pos_2) * self.size
```
40 is outside the range of y_max = 20.
@rht Thanks for your reply. 

First, there are some bugs.
I got this first.
```
pos_1 -= self.center
TypeError: Cannot cast ufunc subtract output from dtype('float64') to dtype('int64') with casting rule 'same_kind'
```
And the computation order of 
```
pos_1 % np.sign(pos_1) * self.size
```
will go from left to right, which may not what you want, I think.

After change your code to 
```
pos_1 = np.array(pos_1)
pos_2 = np.array(pos_2)
if self.torus:
    pos_1 = np.abs(pos_1 - self.center) % self.size
    pos_2 = np.abs(pos_2 - self.center) % self.size
return np.linalg.norm(pos_1 - pos_2)
```
it started to work but failed at the test
```
pos_6 = (-30, -29)
pos_7 = (-30, -4)
assert self.space.get_distance(pos_6, pos_7) ==  25
```
Your code got the result 23 rather than 25.
@clmerlos @jackiekazil 
I just tried to do a complete test for @clmerlos 's code and the original version in mesa 0.77.
And the former failed at 
```
pos_6 = (-30, -29)
pos_7 = (21, -5)
assert self.space.get_distance(pos_6, pos_7) ==  np.sqrt(49 ** 2 + 24 ** 2)
```
It got the result `np.sqrt(49 ** 2 + 26 ** 2).`

Although I still cannot be sure whether the latter is always right.


The following is my test code.

```
import numpy as np
import math


def code_from_rth (center, size, torus, pos_1, pos_2):
    pos_1 = np.array(pos_1)
    pos_2 = np.array(pos_2)
    if torus:
        pos_1 = np.abs(pos_1 - center) % size
        pos_2 = np.abs(pos_2 - center) % size
    return np.linalg.norm(pos_1 - pos_2)


def code_from_clmerlos (center, size, torus, pos_1, pos_2):
    pos_1 = np.array(pos_1)
    pos_2 = np.array(pos_2)
    dist_1 = np.linalg.norm(pos_1 - pos_2)
    if torus:
        pos_1 = (pos_1 - center) % size
        pos_2 = (pos_2 - center) % size
    dist_2 = np.linalg.norm(pos_1 - pos_2)
    return min(dist_1, dist_2)


def code_from_mesa_077(center, size, torus, pos_1, pos_2):
    x1, y1 = pos_1
    x2, y2 = pos_2
    if not torus:
        dx = x1 - x2
        dy = y1 - y2
    else:
        d_x = abs(x1 - x2)
        d_y = abs(y1 - y2)
        dx = min(d_x, size[0] - d_x)
        dy = min(d_y, size[1] - d_y)
    return math.sqrt(dx ** 2 + dy ** 2)


if __name__ == '__main__':
    center = np.array((20, -5))
    size = np.array((100, 50))
    torus = True

    for i in range(-30, 71, 1):
        for j in range(-30, 21, 1):
            for k in range(i, 71, 1):
                for l in range(j, 21, 1):
                    print(i, j, k, l)
                    try:
                        # assert code_from_rth(center, size, torus, np.array((i, j)), np.array((k, l))) == code_from_clmerlos(center, size, torus, np.array((i, j)), np.array((k, l)))
                        assert code_from_clmerlos(center, size, torus, np.array((i, j)), np.array((k, l))) == code_from_mesa_077(center, size, torus, np.array((i, j)), np.array((k, l)))
                    except AssertionError:
                        # print('code from rth got the result: ', code_from_rth(center, size, torus, np.array((i, j)), np.array((k, l))))
                        print('code from clmerlos got the result: ', code_from_clmerlos(center, size, torus, np.array((i, j)), np.array((k, l))))
                        print('code from mesa_077 got the result: ', code_from_mesa_077(center, size, torus, np.array((i, j)), np.array((k, l))))
                        raise AssertionError

```
@simonchx you can't remove the sign from pos_1 and pos_2, since later or pos_1 - pos_2 is a vector operation.

My fix would be to replace `-=` with this:
```
pos_1 = pos_1 - self.center
...
pos_2 = pos_2 - self.center
```
@rht 
> you can't remove the sign from pos_1 and pos_2, since later or pos_1 - pos_2 is a vector operation.

Sorry for that, yes, the sign is needed here.

And the first bug gets fixed after the change you provided.

However, still your code and some fixation I tried failed in some situation.
So, could you please provide a version of your code that could pass the following tests?

```
pos_1 = (-30, -30)
pos_2 = (-30, -4)
assert self.space.get_distance(pos_1, pos_2) == 24

pos_3 = (-30, -29)
assert self.space.get_distance(pos_1, pos_3) == 1
assert self.space.get_distance(pos_2, pos_3) == 25

pos_4 = (-30, -5)
assert self.space.get_distance(pos_3, pos_4) == 24
```
@jackiekazil By comparing results with the basic algorithm below, I'm sure that the get_distance method in mesa 0.77 is the only right one by now.

```
def code_for_basic(center, size, torus, pos_1, pos_2):
    if not torus:
        return np.linalg.norm(pos_1 - pos_2)
    else:
        distance_list = np.zeros(9)
        for i in range(3):
            for j in range(3):
                pos_1_temp = pos_1 + np.array(((i-1) * size[0], (j-1) * size[1]))
                distance_list[i*3+j] = np.linalg.norm(pos_1_temp - pos_2)
        return distance_list.min()
```

@simonchx if my fix doesn't work, I don't see any other way to make it correct again. Also, what would be the result of the accepted solution in SO, https://stackoverflow.com/a/3041398/595474 ?
I think that the right answer is between @simonchx and @rht. 

The 9-grid form will place the blue circle where all the green circles are. Our comparison point is the black circle.

![image](https://user-images.githubusercontent.com/31418520/32850221-8f7e211e-c9ff-11e7-808e-ca1e74febd57.png)

The squares are what happens from the `pos_1 = (pos_1 - self.center) % self.size` commands. 

So in many cases the mod happens to be right, but in the one in the image it differs from the actual shortest distance to the 9-grid version. 

The following code should work:

```python
dx = np.abs(pos_1[0] - pos_2[0])
dy = np.abs(pos_1[1] - pos_2[1])
if torus:
    dx = min(dx, size[0] - dx)
    dy = min(dy, size[1] - dy)
return np.sqrt(dx*dx + dy*dy)
```
In a torus, the two possible x and y distances sum to the width and height, respectively. Picking the smallest of those gets the shortest distance. 

~The above code can then be used to get the torus-adjusted heading (dx, dy), and `get_distance` would only need to call for the heading and do the final line.~ That's wrong, you need directions.

For test cases, try:

```
pos_1 = [0.1, 0.25]
```
and any of the following:
```
pos_2 = [[ 0.73029815,  0.55869144],
       [ 0.73830194,  0.62736621],
       [ 0.68541107,  0.51248163],
       [ 0.71511388,  0.60311977],
       [ 0.6449427 ,  0.51770405],
       [ 0.84275763,  0.53588413],
       [ 0.98466934,  0.60409367],
       [ 0.51726302,  0.89554666],
       [ 0.71573997,  0.72804509],
       [ 0.83771952,  0.5308794 ],
       [ 0.62362186,  0.51116214],
       [ 0.85184157,  0.58020845],
       [ 0.96714036,  0.65536399],
       [ 0.85394435,  0.60324709],
       [ 0.63026544,  0.71716265]]
```
@JamesArruda your solution is a variation of mesa 0.77, but with numpy operation instead of `math`. We really should go all numpy for perf reason.
@rht @JamesArruda Thanks for your suggestions. 

Both of them are the same algorithm as the one in mesa 0.77, which is the only one that passed all the tests I did above. Maybe a more effective version of it is needed?

Resolving patch:
diff --git a/mesa/space.py b/mesa/space.py
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -572,12 +572,15 @@ def get_distance(self, pos_1, pos_2):
             pos_1, pos_2: Coordinate tuples for both points.
 
         """
-        pos_1 = np.array(pos_1)
-        pos_2 = np.array(pos_2)
+        x1, y1 = pos_1
+        x2, y2 = pos_2
+
+        dx = np.abs(x1 - x2)
+        dy = np.abs(y1 - y2)
         if self.torus:
-            pos_1 = (pos_1 - self.center) % self.size
-            pos_2 = (pos_2 - self.center) % self.size
-        return np.linalg.norm(pos_1 - pos_2)
+            dx = min(dx, self.width - dx)
+            dy = min(dy, self.height - dy)
+        return np.sqrt(dx * dx + dy * dy)
 
     def torus_adj(self, pos):
         """ Adjust coordinates to handle torus looping.


Test patch:
diff --git a/tests/test_space.py b/tests/test_space.py
--- a/tests/test_space.py
+++ b/tests/test_space.py
@@ -1,6 +1,7 @@
 import unittest
 
 import networkx as nx
+import numpy as np
 
 from mesa.space import ContinuousSpace
 from mesa.space import SingleGrid
@@ -49,6 +50,14 @@ def test_distance_calculations(self):
         pos_3 = (-30, -20)
         assert self.space.get_distance(pos_1, pos_3) == 10
 
+        pos_4 = (20, -5)
+        pos_5 = (20, -15)
+        assert self.space.get_distance(pos_4, pos_5) == 10
+
+        pos_6 = (-30, -29)
+        pos_7 = (21, -5)
+        assert self.space.get_distance(pos_6, pos_7) == np.sqrt(49 ** 2 + 24 ** 2)
+
     def test_heading(self):
         pos_1 = (-30, -30)
         pos_2 = (70, 20)