Repository: projectmesa/mesa
Pull request #1686 (2023-05-09 06:50:54)

Correct get_heading for toroidal space
This is an improved version of #1664 where each dimension is optimized independently.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/projectmesa/mesa/pull/1686?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
Patch coverage: **`71.42`**% and project coverage change: **`-0.04`** :warning:
> Comparison is base [(`c9d0c43`)](https://app.codecov.io/gh/projectmesa/mesa/commit/c9d0c431d27352a0203903c6b7ef655cf7b9e2b5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 81.62% compared to head [(`b408533`)](https://app.codecov.io/gh/projectmesa/mesa/pull/1686?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 81.59%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1686      +/-   ##
==========================================
- Coverage   81.62%   81.59%   -0.04%     
==========================================
  Files          18       18              
  Lines        1399     1402       +3     
  Branches      271      272       +1     
==========================================
+ Hits         1142     1144       +2     
- Misses        211      212       +1     
  Partials       46       46              
```


| [Impacted Files](https://app.codecov.io/gh/projectmesa/mesa/pull/1686?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) | Coverage Δ | |
|---|---|---|
| [mesa/space.py](https://app.codecov.io/gh/projectmesa/mesa/pull/1686?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9zcGFjZS5weQ==) | `90.81% <71.42%> (-0.19%)` | :arrow_down: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/projectmesa/mesa/pull/1686?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).


Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 102ce58f..79610fe2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,6 +42,7 @@ extensions = [
     "sphinx.ext.mathjax",
     "sphinx.ext.ifconfig",
     "sphinx.ext.viewcode",
+    "nbsphinx",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
diff --git a/docs/index.rst b/docs/index.rst
index bbabb4d5..2ea12816 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -96,7 +96,7 @@ ABM features users have shared that you may want to use in your model
 
    Mesa Overview <overview>
    tutorials/intro_tutorial
-   tutorials/adv_tutorial
+   tutorials/adv_tutorial.ipynb
    Best Practices <best-practices>
    Useful Snippets <useful-snippets/snippets>
    API Documentation <apis/api_main>
diff --git a/docs/tutorials/adv_tutorial.rst b/docs/tutorials/adv_tutorial.rst
deleted file mode 100644
index 242445d7..00000000
--- a/docs/tutorials/adv_tutorial.rst
+++ /dev/null
@@ -1,560 +0,0 @@
-Advanced Tutorial
-=================
-
-Adding visualization
-~~~~~~~~~~~~~~~~~~~~
-
-So far, we’ve built a model, run it, and analyzed some output
-afterwards. However, one of the advantages of agent-based models is that
-we can often watch them run step by step, potentially spotting
-unexpected patterns, behaviors or bugs, or developing new intuitions,
-hypotheses, or insights. Other times, watching a model run can explain
-it to an unfamiliar audience better than static explanations. Like many
-ABM frameworks, Mesa allows you to create an interactive visualization
-of the model. In this section we’ll walk through creating a
-visualization using built-in components, and (for advanced users) how to
-create a new visualization element.
-
-**Note for Jupyter users: Due to conflicts with the tornado server Mesa
-uses and Jupyter, the interactive browser of your model will load but
-likely not work. This will require you to run the code from .py
-files. The Mesa development team is working to develop a** `Jupyter
-compatible interface <https://github.com/projectmesa/mesa/issues/1363>`_.
-
-First, a quick explanation of how Mesa’s interactive visualization
-works. Visualization is done in a browser window, using JavaScript to
-draw the different things being visualized at each step of the model. To
-do this, Mesa launches a small web server, which runs the model, turns
-each step into a JSON object (essentially, structured plain text) and
-sends those steps to the browser.
-
-A visualization is built up of a few different modules: for example, a
-module for drawing agents on a grid, and another one for drawing a chart
-of some variable. Each module has a Python part, which runs on the
-server and turns a model state into JSON data; and a JavaScript side,
-which takes that JSON data and draws it in the browser window. Mesa
-comes with a few modules built in, and let you add your own as well.
-
-
-Grid Visualization
-^^^^^^^^^^^^^^^^^^
-
-To start with, let’s have a visualization where we can watch the agents
-moving around the grid. For this, you will need to put your model code
-in a separate Python source file; for example, ``MoneyModel.py``. Next,
-either in the same file or in a new one (e.g. ``MoneyModel_Viz.py``)
-import the server class and the Canvas Grid class (so-called because it
-uses HTML5 canvas to draw a grid). If you’re in a new file, you’ll also
-need to import the actual model object.
-
-.. code:: ipython3
-
-    import mesa
-
-    # If MoneyModel.py is where your code is:
-    # from MoneyModel import MoneyModel
-
-``CanvasGrid`` works by looping over every cell in a grid, and
-generating a portrayal for every agent it finds. A portrayal is a
-dictionary (which can easily be turned into a JSON object) which tells
-the JavaScript side how to draw it. The only thing we need to provide is
-a function which takes an agent, and returns a portrayal object. Here’s
-the simplest one: it’ll draw each agent as a red, filled circle which
-fills half of each cell.
-
-.. code:: ipython3
-
-    def agent_portrayal(agent):
-        portrayal = {
-            "Shape": "circle",
-            "Color": "red",
-            "Filled": "true",
-            "Layer": 0,
-            "r": 0.5,
-        }
-        return portrayal
-
-In addition to the portrayal method, we instantiate a canvas grid with
-its width and height in cells, and in pixels. In this case, let’s create
-a 10x10 grid, drawn in 500 x 500 pixels.
-
-.. code:: ipython3
-
-    grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
-
-.. code:: ipython3
-
-    """
-    The full code should now look like:
-    """
-    # from MoneyModel import *
-    import mesa
-
-
-    def agent_portrayal(agent):
-        portrayal = {
-            "Shape": "circle",
-            "Filled": "true",
-            "Layer": 0,
-            "Color": "red",
-            "r": 0.5,
-        }
-        return portrayal
-
-
-    grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
-    server = mesa.visualization.ModularServer(
-        MoneyModel, [grid], "Money Model", {"N": 100, "width": 10, "height": 10}
-    )
-    server.port = 8521  # The default
-    server.launch()
-
-Now we create and launch the actual server. We do this with the
-following arguments:
-
--  The model class we’re running and visualizing; in this case,
-   ``MoneyModel``.
--  A list of module objects to include in the visualization; here, just
-   ``[grid]``
--  The title of the model: “Money Model”
--  Any inputs or arguments for the model itself. In this case, 100
-   agents, and height and width of 10.
-
-Once we create the server, we set the port for it to listen on (you can
-treat this as just a piece of the URL you’ll open in the browser).
-Finally, when you’re ready to run the visualization, use the server’s
-``launch()`` method.
-
-.. code:: python
-
-   server = ModularServer(MoneyModel,
-                          [grid],
-                          "Money Model",
-                          {"N":100, "width":10, "height":10})
-   server.port = 8521 # The default
-   server.launch()
-
-The full code should now look like:
-
-.. code:: python
-
-   from MoneyModel import *
-   import mesa
-
-
-   def agent_portrayal(agent):
-       portrayal = {"Shape": "circle",
-                    "Filled": "true",
-                    "Layer": 0,
-                    "Color": "red",
-                    "r": 0.5}
-       return portrayal
-
-   grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
-   server = mesa.visualization.ModularServer(
-       MoneyModel, [grid], "Money Model", {"N": 100, "width": 10, "height": 10}
-   )server = ModularServer(MoneyModel,
-                          [grid],
-                          "Money Model",
-                          {"N":100, "width":10, "height":10})
-   server.port = 8521 # The default
-   server.launch()
-
-Now run this file; this should launch the interactive visualization
-server and open your web browser automatically. (If the browser doesn’t
-open automatically, try pointing it at http://127.0.0.1:8521 manually.
-If this doesn’t show you the visualization, something may have gone
-wrong with the server launch.)
-
-You should see something like the figure below: the model title, an
-empty space where the grid will be, and a control panel off to the
-right.
-
-.. figure:: files/viz_empty.png
-   :alt: Empty Visualization
-
-   Empty Visualization
-
-Click the ‘reset’ button on the control panel, and you should see the
-grid fill up with red circles, representing agents.
-
-.. figure:: files/viz_redcircles.png
-   :alt: Redcircles Visualization
-
-   Redcircles Visualization
-
-Click ‘step’ to advance the model by one step, and the agents will move
-around. Click ‘run’ and the agents will keep moving around, at the rate
-set by the ‘fps’ (frames per second) slider at the top. Try moving it
-around and see how the speed of the model changes. Pressing ‘pause’ will
-(as you’d expect) pause the model; presing ‘run’ again will restart it.
-Finally, ‘reset’ will start a new instantiation of the model.
-
-To stop the visualization server, go back to the terminal where you
-launched it, and press Control+c.
-
-Changing the agents
-^^^^^^^^^^^^^^^^^^^
-
-In the visualization above, all we could see is the agents moving around
-– but not how much money they had, or anything else of interest. Let’s
-change it so that agents who are broke (wealth 0) are drawn in grey,
-smaller, and above agents who still have money.
-
-To do this, we go back to our ``agent_portrayal`` code and add some code
-to change the portrayal based on the agent properties.
-
-.. code:: python
-
-   def agent_portrayal(agent):
-       portrayal = {"Shape": "circle",
-                    "Filled": "true",
-                    "r": 0.5}
-
-       if agent.wealth > 0:
-           portrayal["Color"] = "red"
-           portrayal["Layer"] = 0
-       else:
-           portrayal["Color"] = "grey"
-           portrayal["Layer"] = 1
-           portrayal["r"] = 0.2
-       return portrayal
-
-Now launch the server again - this will open a new browser window
-pointed at the updated visualization. Initially it looks the same, but
-advance the model and smaller grey circles start to appear. Note that
-since the zero-wealth agents have a higher layer number, they are drawn
-on top of the red agents.
-
-.. figure:: files/viz_greycircles.png
-   :alt: Greycircles Visualization
-
-   Greycircles Visualization
-
-Adding a chart
-^^^^^^^^^^^^^^
-
-Next, let’s add another element to the visualization: a chart, tracking
-the model’s Gini Coefficient. This is another built-in element that Mesa
-provides.
-
-The basic chart pulls data from the model’s DataCollector, and draws it
-as a line graph using the `Charts.js <http://www.chartjs.org/>`__
-JavaScript libraries. We instantiate a chart element with a list of
-series for the chart to track. Each series is defined in a dictionary,
-and has a ``Label`` (which must match the name of a model-level variable
-collected by the DataCollector) and a ``Color`` name. We can also give
-the chart the name of the DataCollector object in the model.
-
-Finally, we add the chart to the list of elements in the server. The
-elements are added to the visualization in the order they appear, so the
-chart will appear underneath the grid.
-
-.. code:: python
-
-   chart = mesa.visualization.ChartModule([{"Label": "Gini",
-                         "Color": "Black"}],
-                       data_collector_name='datacollector')
-
-   server = mesa.visualization.ModularServer(MoneyModel,
-                          [grid, chart],
-                          "Money Model",
-                          {"N":100, "width":10, "height":10})
-
-Launch the visualization and start a model run, and you’ll see a line
-chart underneath the grid. Every step of the model, the line chart
-updates along with the grid. Reset the model, and the chart resets too.
-
-.. figure:: files/viz_chart.png
-   :alt: Chart Visualization
-
-   Chart Visualization
-
-**Note:** You might notice that the chart line only starts after a
-couple of steps; this is due to a bug in Charts.js which will hopefully
-be fixed soon.
-
-Building your own visualization component
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-**Note:** This section is for users who have a basic familiarity with
-JavaScript. If that’s not you, don’t worry! (If you’re an advanced
-JavaScript coder and find things that we’ve done wrong or inefficiently,
-please `let us know <https://github.com/projectmesa/mesa/issues>`__!)
-
-If the visualization elements provided by Mesa aren’t enough for you,
-you can build your own and plug them into the model server.
-
-First, you need to understand how the visualization works under the
-hood. Remember that each visualization module has two sides: a Python
-object that runs on the server and generates JSON data from the model
-state (the server side), and a JavaScript object that runs in the
-browser and turns the JSON into something it renders on the screen (the
-client side).
-
-Obviously, the two sides of each visualization must be designed in
-tandem. They result in one Python class, and one JavaScript ``.js``
-file. The path to the JavaScript file is a property of the Python class.
-
-For this example, let’s build a simple histogram visualization, which
-can count the number of agents with each value of wealth. We’ll use the
-`Charts.js <http://www.chartjs.org/>`__ JavaScript library, which is
-already included with Mesa. If you go and look at its documentation,
-you’ll see that it had no histogram functionality, which means we have
-to build our own out of a bar chart. We’ll keep the histogram as simple
-as possible, giving it a fixed number of integer bins. If you were
-designing a more general histogram to add to the Mesa repository for
-everyone to use across different models, obviously you’d want something
-more general.
-
-Client-Side Code
-^^^^^^^^^^^^^^^^
-
-In general, the server- and client-side are written in tandem. However,
-if you’re like me and more comfortable with Python than JavaScript, it
-makes sense to figure out how to get the JavaScript working first, and
-then write the Python to be compatible with that.
-
-In the same directory as your model, create a new file called
-``HistogramModule.js``. This will store the JavaScript code for the
-client side of the new module.
-
-JavaScript classes can look alien to people coming from other languages
-– specifically, they can look like functions. (The Mozilla `Introduction
-to Object-Oriented
-JavaScript <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript>`__
-is a good starting point). In ``HistogramModule.js``, start by creating
-the class itself:
-
-.. code:: javascript
-
-    const HistogramModule = function(bins, canvas_width, canvas_height) {
-        // The actual code will go here.
-    };
-
-Note that our object is instantiated with three arguments: the number of
-integer bins, and the width and height (in pixels) the chart will take
-up in the visualization window.
-
-When the visualization object is instantiated, the first thing it needs
-to do is prepare to draw on the current page. To do so, it adds a
-`canvas <https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API>`__
-tag to the page. It also gets the canvas' context, which is required for doing
-anything with it.
-
-.. code:: javascript
-
-    const HistogramModule = function(bins, canvas_width, canvas_height) {
-      // Create the canvas object:
-        const canvas = document.createElement("canvas");
-        Object.assign(canvas, {
-          width: canvas_width,
-          height: canvas_height,
-          style: "border:1px dotted",
-        });
-      // Append it to #elements:
-      const elements = document.getElementById("elements");
-      elements.appendChild(canvas);
-
-      // Create the context and the drawing controller:
-      const context = canvas.getContext("2d");
-    };
-
-
-Look at the Charts.js `bar chart
-documentation <http://www.chartjs.org/docs/#bar-chart-introduction>`__.
-You’ll see some of the boilerplate needed to get a chart set up.
-Especially important is the ``data`` object, which includes the
-datasets, labels, and color options. In this case, we want just one
-dataset (we’ll keep things simple and name it “Data”); it has ``bins``
-for categories, and the value of each category starts out at zero.
-Finally, using these boilerplate objects and the canvas context we
-created, we can create the chart object.
-
-.. code:: javascript
-
-    const HistogramModule = function(bins, canvas_width, canvas_height) {
-      // Create the canvas object:
-        const canvas = document.createElement("canvas");
-        Object.assign(canvas, {
-          width: canvas_width,
-          height: canvas_height,
-          style: "border:1px dotted",
-        });
-      // Append it to #elements:
-      const elements = document.getElementById("elements");
-      elements.appendChild(canvas);
-
-      // Create the context and the drawing controller:
-      const context = canvas.getContext("2d");
-
-      // Prep the chart properties and series:
-      const datasets = [{
-        label: "Data",
-        fillColor: "rgba(151,187,205,0.5)",
-        strokeColor: "rgba(151,187,205,0.8)",
-        highlightFill: "rgba(151,187,205,0.75)",
-        highlightStroke: "rgba(151,187,205,1)",
-        data: []
-      }];
-
-      // Add a zero value for each bin
-      for (var i in bins)
-        datasets[0].data.push(0);
-
-      const data = {
-        labels: bins,
-        datasets: datasets
-      };
-
-      const options = {
-        scaleBeginsAtZero: true
-      };
-
-      // Create the chart object
-      let chart = new Chart(context, {type: 'bar', data: data, options: options});
-
-      // Now what?
-    };
-
-There are two methods every client-side visualization class must
-implement to be able to work: ``render(data)`` to render the incoming
-data, and ``reset()`` which is called to clear the visualization when
-the user hits the reset button and starts a new model run.
-
-In this case, the easiest way to pass data to the histogram is as an
-array, one value for each bin. We can then just loop over the array and
-update the values in the chart’s dataset.
-
-There are a few ways to reset the chart, but the easiest is probably to
-destroy it and create a new chart object in its place.
-
-With that in mind, we can add these two methods to the class:
-
-.. code:: javascript
-
-    const HistogramModule = function(bins, canvas_width, canvas_height) {
-      // ...Everything from above...
-      this.render = function(data) {
-        datasets[0].data = data;
-        chart.update();
-      };
-
-      this.reset = function() {
-        chart.destroy();
-        chart = new Chart(context, {type: 'bar', data: data, options: options});
-      };
-    };
-Note the ``this``. before the method names. This makes them public and
-ensures that they are accessible outside of the object itself. All the
-other variables inside the class are only accessible inside the object
-itself, but not outside of it.
-
-Server-Side Code
-^^^^^^^^^^^^^^^^
-
-Can we get back to Python code? Please?
-
-Every JavaScript visualization element has an equal and opposite
-server-side Python element. The Python class needs to also have a
-``render`` method, to get data out of the model object and into a
-JSON-ready format. It also needs to point towards the code where the
-relevant JavaScript lives, and add the JavaScript object to the model
-page.
-
-In a Python file (either its own, or in the same file as your
-visualization code), import the ``VisualizationElement`` class we’ll
-inherit from, and create the new visualization class.
-
-.. code:: python
-
-       from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE
-
-       class HistogramModule(VisualizationElement):
-           package_includes = [CHART_JS_FILE]
-           local_includes = ["HistogramModule.js"]
-
-           def __init__(self, bins, canvas_height, canvas_width):
-               self.canvas_height = canvas_height
-               self.canvas_width = canvas_width
-               self.bins = bins
-               new_element = "new HistogramModule({}, {}, {})"
-               new_element = new_element.format(bins,
-                                                canvas_width,
-                                                canvas_height)
-               self.js_code = "elements.push(" + new_element + ");"
-
-There are a few things going on here. ``package_includes`` is a list of
-JavaScript files that are part of Mesa itself that the visualization
-element relies on. You can see the included files in
-`mesa/visualization/templates/ <https://github.com/projectmesa/mesa/tree/main/mesa/visualization/templates>`__.
-Similarly, ``local_includes`` is a list of JavaScript files in the same
-directory as the class code itself. Note that both of these are class
-variables, not object variables – they hold for all particular objects.
-
-Next, look at the ``__init__`` method. It takes three arguments: the
-number of bins, and the width and height for the histogram. It then uses
-these values to populate the ``js_code`` property; this is code that the
-server will insert into the visualization page, which will run when the
-page loads. In this case, it creates a new HistogramModule (the class we
-created in JavaScript in the step above) with the desired bins, width
-and height; it then appends (``push``\ es) this object to ``elements``,
-the list of visualization elements that the visualization page itself
-maintains.
-
-Now, the last thing we need is the ``render`` method. If we were making
-a general-purpose visualization module we’d want this to be more
-general, but in this case we can hard-code it to our model.
-
-.. code:: python
-
-   import numpy as np
-
-   class HistogramModule(VisualizationElement):
-       # ... Everything from above...
-
-       def render(self, model):
-           wealth_vals = [agent.wealth for agent in model.schedule.agents]
-           hist = np.histogram(wealth_vals, bins=self.bins)[0]
-           return [int(x) for x in hist]
-
-Every time the render method is called (with a model object as the
-argument) it uses numpy to generate counts of agents with each wealth
-value in the bins, and then returns a list of these values. Note that
-the ``render`` method doesn’t return a JSON string – just an object that
-can be turned into JSON, in this case a Python list (with Python
-integers as the values; the ``json`` library doesn’t like dealing with
-numpy’s integer type).
-
-Now, you can create your new HistogramModule and add it to the server:
-
-.. code:: python
-
-       histogram = mesa.visualization.HistogramModule(list(range(10)), 200, 500)
-       server = mesa.visualization.ModularServer(MoneyModel,
-                              [grid, histogram, chart],
-                              "Money Model",
-                              {"N":100, "width":10, "height":10})
-       server.launch()
-
-Run this code, and you should see your brand-new histogram added to the
-visualization and updating along with the model!
-
-.. figure:: files/viz_histogram.png
-   :alt: Histogram Visualization
-
-   Histogram Visualization
-
-If you’ve felt comfortable with this section, it might be instructive to
-read the code for the
-`ModularServer <https://github.com/projectmesa/mesa/blob/main/mesa/visualization/ModularVisualization.py#L259>`__
-and the
-`modular_template <https://github.com/projectmesa/mesa/blob/main/mesa/visualization/templates/modular_template.html>`__
-to get a better idea of how all the pieces fit together.
-
-Happy Modeling!
-~~~~~~~~~~~~~~~
-
-This document is a work in progress. If you see any errors, exclusions
-or have any problems please contact
-`us <https://github.com/projectmesa/mesa/issues>`__.
diff --git a/mesa/space.py b/mesa/space.py
index 9dc64245..a1b9ff43 100644
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -119,6 +119,11 @@ class _Grid:
         # Neighborhood Cache
         self._neighborhood_cache: dict[Any, list[Coordinate]] = {}
 
+        # Cutoff used inside self.move_to_empty. The parameters are fitted on Python
+        # 3.11 and it was verified that they are roughly the same for 3.10. Refer to
+        # the code in PR#1565 to check for their stability when a new release gets out.
+        self.cutoff_empties = 7.953 * self.num_cells**0.384
+
     @staticmethod
     def default_val() -> None:
         """Default value for new cell elements."""
@@ -268,6 +273,9 @@ class _Grid:
         if neighborhood is not None:
             return neighborhood
 
+        if self.out_of_bounds(pos):
+            raise Exception("The `pos` tuple passed is out of bounds.")
+
         # We use a list instead of a dict for the neighborhood because it would
         # be easier to port the code to Cython or Numba (for performance
         # purpose), with minimal changes. To better understand how the
@@ -309,7 +317,7 @@ class _Grid:
 
                     neighborhood.append((nx, ny))
 
-        if not include_center and neighborhood:
+        if not include_center:
             neighborhood.remove(pos)
 
         self._neighborhood_cache[cache_key] = neighborhood
@@ -461,9 +469,7 @@ class _Grid:
         x, y = pos
         return self._grid[x][y] == self.default_val()
 
-    def move_to_empty(
-        self, agent: Agent, cutoff: float = 0.998, num_agents: int | None = None
-    ) -> None:
+    def move_to_empty(self, agent: Agent, num_agents: int | None = None) -> None:
         """Moves agent to a random empty cell, vacating agent's old cell."""
         if num_agents is not None:
             warn(
@@ -479,10 +485,10 @@ class _Grid:
 
         # This method is based on Agents.jl's random_empty() implementation. See
         # https://github.com/JuliaDynamics/Agents.jl/pull/541. For the discussion, see
-        # https://github.com/projectmesa/mesa/issues/1052. The default cutoff value
-        # provided is the break-even comparison with the time taken in the else
-        # branching point.
-        if 1 - num_empty_cells / self.num_cells < cutoff:
+        # https://github.com/projectmesa/mesa/issues/1052 and
+        # https://github.com/projectmesa/mesa/pull/1565. The cutoff value provided
+        # is the break-even comparison with the time taken in the else branching point.
+        if num_empty_cells > self.cutoff_empties:
             while True:
                 new_pos = (
                     agent.random.randrange(self.width),
@@ -1006,11 +1012,21 @@ class ContinuousSpace:
         """
         one = np.array(pos_1)
         two = np.array(pos_2)
-        if self.torus:
-            one = (one - self.center) % self.size
-            two = (two - self.center) % self.size
         heading = two - one
-        if isinstance(pos_1, tuple):
+        if self.torus:
+            inverse_heading = heading - np.sign(heading) * self.size
+
+            def get_min_abs(x, y):
+                return x if abs(x) < abs(y) else y
+
+            # Choose the smaller heading based on their absolute value for
+            # each dimension independently.
+            heading = tuple(
+                get_min_abs(heading[i], inverse_heading[i]) for i in range(2)
+            )
+        if isinstance(pos_1, np.ndarray):
+            heading = np.ndarray(heading)
+        else:
             heading = tuple(heading)
         return heading
 
diff --git a/setup.py b/setup.py
index 6760e56e..05f4deab 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,9 @@ extras_require = {
         "pytest-cov",
         "sphinx",
     ],
-    "docs": ["sphinx", "ipython"],
+    # Constrain sphinx version until https://github.com/readthedocs/readthedocs.org/issues/10279
+    # is fixed.
+    "docs": ["sphinx<7", "ipython", "nbsphinx"],
 }
 
 version = ""


Test patch:
diff --git a/tests/test_grid.py b/tests/test_grid.py
index 7b4c590d..a24db60a 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -119,11 +119,8 @@ class TestSingleGrid(unittest.TestCase):
         neighborhood = self.grid.get_neighborhood((0, 0), moore=False)
         assert len(neighborhood) == 2
 
-        neighbors = self.grid.get_neighbors((4, 1), moore=False)
-        assert len(neighbors) == 0
-
-        neighbors = self.grid.get_neighbors((4, 1), moore=True)
-        assert len(neighbors) == 0
+        with self.assertRaises(Exception):
+            neighbors = self.grid.get_neighbors((4, 1), moore=False)
 
         neighbors = self.grid.get_neighbors((1, 1), moore=False, include_center=True)
         assert len(neighbors) == 3