-
Notifications
You must be signed in to change notification settings - Fork 874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Formal neighborhood definition for grids #1900
Comments
Now really full solution of this, but some thoughts:
neighborhood_def = {include_center: False, radius: 1, moore: True}
class NeighborhoodDefinition:
def __init__(self, include_center=False, radius=1, moore=True):
self.include_center = include_center
self.radius = radius
self.moore = moore
|
An advantage of using a simple dictionary would be to directly use the definition via unpacking. So Imho we could do
Whenever we need the values in a traditional function. |
But this issue has let me to think about how we define theesa space API and if we should switch to a more "chainable" API? So for example instead of doing We do something like this grid[pos] # get the current position (a Position)
.neighborhood(moore, radius, center) # get the neighborhood (a PositionList)
.agents # get the agents (an AgentList) So we would have all those intermediate classes with their own attributes and methods. In this case it might seem a bit verbose, but I think especially combined with properties we can have a much cleaner and more powerful way of extracting the things we want. I mean to say that we can have simpler functions as building blocks for complex logic and not having highly specialized functions like What do you think? |
Thanks for the insights! I like both ideas of the dictionary unpacking and the chainable API (feels a bit like Pandas). I don't know yet which I prefer. It might be useful to discuss this at the dev meeting. |
What about specifying the default values of neighborhood_config = {"include_center": False, "radius": 1, "moore": True}
mesa.space.MultiGrid(self.width, self.height, torus=True, neighborhood_config=neighborhood_config)
...
# if you want to deviate from the initial spec
self.model.grid.get_neighborhood(self.pos, moore=False) Tangent: while writing #1953, I thought about the new API for space along the line of the AgentSet. Had similar conclusions to @Corvince's specification in #1900 (comment), which I assume that @quaquel had thought of as well. Thinking about having a unified consistent API as much as possible, instead of |
I have been thinking a bit about a PositionSet and even discussed it with @EwoutH. I think it is a good idea and should ideally mimic the API of AgentSet. As I indicated in #1953, if having a dedicated class for a GridCell is at least as performant as the current implementation of Grid, It would be quite straightforward, I believe, to create a PositionSet, or GridCellSet, or whatever it is named, class. |
I was playing around with a dedicated GridCell a long time ago and from what I remember the performance was quite bad. But I don't think it is needed for a PositionSet, it should be enough to just store the coordinates. This way it is even independent of the used space, since coordinates are a defining feature of any place in space. The rest can be expanded on-demand. But regarding GridCell, as I said, I did this years ago. If you have a solid implementation idea in mind it might be worthwhile to experiment. |
I agree on the coordinates/tuples point. What keeps tripping me up, however, is the difference between the coordinates and the content at those coordinates. Having a dedicated object for this might help. In fact, multigrid does this already because each cell contains a list. |
I have rigged up a very rough first draft and only tested random neighbor access with a neighborhood size of 1 using Moore neighborhood and non-torroidal spaces, but my grid cell implementation appears 2 orders of magnitude faster. This is evidently far from conclusive. Random access works against the caching that is currently in place. My implementation favors retrieval of direct neighbors, so I have no idea yet how it scales to larger neighborhood sizes. My brain is fried by now because I have been at a workshop. I hope to get back to this over the weekend, but. |
That sounds promising! However, yes, the real problems slowdown start with torus handling and larger von Neumann neighborhoods. But hey even if it's just as fast as the current implementation it would be worth considering. Although when the current implementation with positions and content can be confusing I am not sure if this will improve when we have position, cells and content. But I also think part of the confusion stems from our current API and its inconsistencies and ambiguous naming. So definitely room for improvement. |
Honestly don’t know if this is the right direction, but very happy with the discussion and very curious to explore it further. Looking forward to a draft implementation!
Now I’m curious what kind of workshop is able to fry your brain. |
|
A slightly different direction would be to make at least |
Re 1) i think that's still not too bad, given that radius 1 is the most common neighborhood. Was this on a toroidal or non toroidal space? Re 2) how would that work? If I want to query the grid I can provide coordinates, but how would I provide a cell that I want to receive? Maybe I am missing something. |
|
Okay I finally get it. You precalculate all the (radius-1) neighborhoods on grid generation. That wasn't clear to me. But its pretty clever if the (one-time) cost isn't too high. But for larger neighborhoods I think its a bit more complicated than recursively reducing the radius, because you don't want to have the same neighbors more than once. And for von Neumann Neighborhoods you need to reduce the radius differently for straight and edge neighbors. Its probably doable but I think involves more logic. Still I think it might be worthwhile. How does your class look like? Do you have a public branch? Maybe you did this already but since neighborhoods are fixed you I think it can be returned as a tuple instead of a list (probably negligible performance difference) and use slots (should be noticeable). Regarding API advantages, I don't find your particular example very convincing. I would say its about the same length as |
|
Would be great to have a branch, I'll have some time tomorrow and would be interested to take a deep dive |
I'll do my best to make a rough draft available by tomorrow. It likely will be more proof of principle rather than a full implementation within mesa. |
Yeah that's fine and what I am looking for! |
this is the basic code. For a notebook, see here. The code is rough, and performance is fine for direct neighbors/neighborhoods but not for a larger radius. One advantage of having a grid cell like this is that this approach works for hex grids, triangular grids, Voronoi meshes, and any graph-based network. It would allow us to tie the API together across all these types of spaces (to the extent that this is currently not the case, I did not fully check). from typing import List, Any
import random
from functools import cache
from mesa import Model, Agent
class GridCell:
@property
def neighbors(self):
return [entry.content for entry in self.neighborhood if entry.content != None]
def __init__(self, i:int, j:int) -> None:
self.i = i
self.j = j
self.coords = (i, j)
self._neighborhood = None
self.content = None
self.left: GridCell = None
self.right: GridCell = None
self.top: GridCell = None
self.bottom: GridCell = None
self.topleft: GridCell = None
self.topright: GridCell = None
self.bottomright: GridCell = None
self.bottomleft: GridCell = None
def _calculate_neighborhood(self):
neighborhood = [self.topleft, self.top, self.topright,
self.right, self.bottomright, self.bottom,
self.bottomleft, self.left]
neighborhood = [entry for entry in neighborhood if entry != None]
self.neighborhood = neighborhood
class Grid:
def __init__(self, width:int, height:int, torus:bool = False) -> None:
self.width = width
self.height = height
self.torus = torus
self.cells: List[List[GridCell]] = [[GridCell(j, i) for i in range(width)] for j in range(height)]
# connect within a row
for row in self.cells:
for cell1, cell2 in zip(row, row[1::]):
cell1.right = cell2
cell2.left = cell1
if torus:
for row in self.cells:
row[0].left = row[-1]
row[-1].right = row[0]
# connect across rows
for entry in zip(self.cells, self.cells[1::]):
for cell1, cell2 in zip(*entry):
cell1.bottom = cell2
cell1.bottomleft = cell2.left
cell1.bottomright = cell2.right
cell2.top = cell1
cell2.topright = cell1.left
cell2.topleft = cell1.right
if torus:
for cell1, cell2 in zip(self.cells[0], self.cells[-1]):
cell1.top = cell2
cell1.topleft = cell2.left
cell1.topright = cell2.right
cell2.bottom = cell1
cell2.bottomleft = cell1.left
cell2.bottomright = cell1.right
for row in self.cells:
for cell in row:
cell._calculate_neighborhood()
@cache
def get_neighborhood(self, coords, radius):
i, j = coords
neighborhood = {}
if radius==0:
return {self.cells[i][j]:None}
else:
radius = radius - 1
for cell in self.cells[i][j].neighborhood:
neighborhood = neighborhood | self.get_neighborhood(cell.coords, radius)
return neighborhood
def get_neighbors(self, coords, radius):
neighborhood = self.get_neighborhood(coords, radius)
return [content for entry in neighborhood if (content := entry.conten != None)]
def place_agent(self, agent, pos):
pass |
So, I tested your implementation extensively on the Schelling Model and I have to say I am very impressed and excited! This has several advantages:
I restructured the code a bit. Get_neighborhood and get_neighbors are now primarily functions of the GridCells, not the Grid. The Grid only holds references to the GridCells and is responsible for initially connecting them. This way for other "Grids" (like HexGrid) we only need to modify the get_neighborhood method and the _connect_single_cells method (see below). 3D Grids should also be straightforward to implement. Since we are no longer dependent on the added performance of a list-of-lists implementation we can just use a dictionary for all cells. This way we can always use the pos tuple and don't need to destructure into x, y in several places. Thus we can also swap definition of pos however we want without code changes for placing agents, etc. Here is my adapted code class GridCell:
__slots__ = ["coords", "connections", "content"]
def __init__(self, i: int, j: int) -> None:
self.coords = (i, j)
self.connections = None
self.content = None
def neighbors(self, moore=True, include_center=False, radius=1):
return (
entry.content
for entry in self.get_neighborhood(moore, include_center, radius)
if entry.content != None
)
@cache
def get_neighborhood(self, moore=True, include_center=False, radius=1):
if radius == 0:
return {self: None}
neighborhood = {}
radius = radius - 1
for cell in self.connections:
if moore:
neighborhood = neighborhood | cell.get_neighborhood(
moore, include_center, radius
)
else:
if cell.i == self.i or cell.j == self.j:
neighborhood = neighborhood | cell.get_neighborhood(
moore, include_center, radius
)
if not include_center:
neighborhood.pop(self, None)
return neighborhood
class Grid:
def __init__(self, width: int, height: int, torus: bool = False) -> None:
self.width = width
self.height = height
self.torus = torus
self.cells = {
(i, j): GridCell(i, j) for j in range(width) for i in range(height)
}
self._connect_all_cells()
def _connect_all_cells(self):
for coords, cell in self.cells.items():
cell.connections = self._connect_single_cell(coords)
def _connect_single_cell(self, cell):
i, j = cell
directions = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
]
connections = []
for di, dj in directions:
ni, nj = (i + di, j + dj)
if self.torus:
ni, nj = ni % self.height, nj % self.width
if 0 <= ni < self.height and 0 <= nj < self.width:
connections.append(self.cells[ni, nj])
return connections
@cache
def get_neighborhood(self, moore=True, include_center=False, radius=1):
return self.cells[coords].get_neighborhood(moore, include_center, radius) |
By how much? This information is critical. A list of list, or an N-dimensional NDarray is a more compact representation for at least a square grid. As such, the memory consumption of the system after 100 steps (to account for the caching), needs to be compared. |
Thanks, @Corvince, for looking at this. Could you share the Schelling example somehow? I might have time to look at this again over the weekend. Could it be an idea to start developing this further in the experimental folder? @rht memory use should largely be a non-issue. Effectively, this grid implementation is a linked list. The memory use of a single grid cell is marginal: just a few references and the content (no different from the current MultiGrid). The caching is also a non-issue. First, we use |
That SGTM; my remaining concern would be that if it is easy to translate the implementation to Cython. With the cells just being a collection, I think it should be fine. Though I'm not sure how the cache decorator would work out of the box in Cython. I suppose the caching process would happen in the Python layer instead of C layer. |
The caching seems to be written in C, so if we decide to move to cython that should not be a major concern. |
Played around some more and had some interesting ideas, circling this discussion back onto its starting point. Instead of a dedicated GridCell I now have just a Cell that can be used across all spaces. class Cell:
__slots__ = ["coords", "connections", "content"]
def __init__(coords: Coordinates) -> None:
self.coords = coords
self.connections = []
self.content = []
def connect(self, other) -> None:
"""Connects this cell to another cell."""
self.connections.append(other)
def disconnect(self, other) -> None:
"""Disconnects this cell from another cell."""
self.connections.remove(other)
def __repr__(self):
return f"Cell({self.coords})" The generic Space class is responsible for creating the cells and connecting them (to be subclassed for concrete space classes like Grid) class Space:
def __init__(self):
self.cells = {} # <= dict of cells, obviously needs some initialization dependend on the concrete Space
for cell in self.cells.values():
self._connect_single_cell(cell)
def _connect_single_cell(self, cell): # <= different for every concrete Space
...
def get_neighborhood(self, coords: Coordinates, Neighborhood: Neighborhood):
return Neighborhood.of(self.cells[coords]) Here things get interesting. The get_neighborhood function of the Space uses a Neighborhood class, which is a formal definition of a neighborhood of cells. As an example here is a definition for a GridNeighborhood class GridNeighborhood:
def __init__(self, moore=True, include_center=False, radius=1):
self.moore = moore
self.include_center = include_center
self.radius = radius
@cache
def of(self, cell, radius=None):
"""Heavily optimized. You don't need to understand it, just skip it"""
radius = self.radius if radius is None else radius
if radius == 0:
neighborhood = {cell: cell.content}
else:
neighborhood = {}
for neighbor in cell.connections:
if (
self.moore
or neighbor.coords[0] == cell.coords[0]
or neighbor.coords[1] == cell.coords[1]
):
neighborhood.update(self.of(neighbor, radius - 1))
if radius == self.radius and not self.include_center:
neighborhood.pop(cell, None)
return neighborhood As you can see we move all the logic on how to receive a neighborhood into a dedicated class that has a single responsibility - return the neighborhood of a cell. It returns a dictionary like the following { Cell(0, 0): [Agent] 1, Cell(0, 1): [Agent2, Agent3]} This means we get both the neighborhood as cells and the neighbors in once function call. No more confusion between neighborhood and neighbors. You want the cells? Use dict.keys(). You want the agents? Use dict.values(). With this basic setup we can achieve a very high amount of reusability. Since we always use the same cells with the same properties we can implement most space functions just once. Agent movements are just going from one cell to another. Empty cells are just cells which have an empty list of content. Speaking of which its important that the content is a list or any other mutable object. This way we can simply cache the neighborhood including agents. The neighborhood dictionary is just a mapping of For the API usage this will look like this # somewhere in your code
Neighborhood = GridNeighborhood(True, False, 1)
# For a Schelling agent:
for neighbors in Neighborhood.of(self.cell).values():
if neighbors and neighbors[0].type == self.type:
self.similar += 1
# Normal Access through grid
model.grid.get_neighborhood((17, 23), Neighborhood)
# Alternatively
Neighborhood.of(model.grid[(17, 23)]) Looking forward to your comments on this! Hope to publish my experiments in a branch tomorrow, but needs some cleanup |
I am impressed by your improvements upon my first rough implementation. Some testing showed that the problem I ran into with bad scaling to higher radii is now gone. This is probably because of the heavy optimization in For some some quick thoughts (and I am happy to contribute wherever you want input).
|
Thank you for your quick feedback! Even though it looks very different now you initial draft was invaluable for me! At first I tried together with ChatGPT to create a slightly different implementation for the neighborhood search, but it ended up much worse, so I took your solution and just incrementally improved it. On the comprehensiveness: I only got the cache to work on the last iteration. With the cache in place it could probably reverted to a more readable solution, but lets not hang on implementation details.
Probably yes and I won't think too much about ContinuousSpaces just yet - but internally ContinuousSpace already uses some form of discrete grid, so maybe it can actually can be made to interop nicely.
The implemenation is correct, but the naming is a bit off. Neighborhood here refers to the defintion of the neighborhood, like in GridNeighborhood class. But currently it only implements Neighborhood.of so maybe it should just be a factory function that returns a get_neighborhood function? Something like get_neighborhood_of = create_neighbord_getter(moore=True, include_center=False, radius=1)
I mainly renamed neighboorhood to connections. I think this way its clearer, because in the example of a regular grid von Neumann and Moore neighboorhoods are different things, but the connections are the same. I mean it depends on how you connect your cells and this is somewhat arbitrary on purpose. But for this reason I found connections to be more general and hence the explicit connect and disconnect functions. So you could connect Cells dynamically. The type is just a list of connected Cells. Maybe this should be a private variable to be honest.
I have been thinking about this too during breakfast. I think this is indeed the way to go. I think this will also be more intuitive to use than the currently returned dictionary. Because although its nice if you know how it works, its not imminent that cells are keys and content are values. So it would look more like neighborhood = get_neighborhood_of(self.cell)
for agent in neighborhood.agents:
pass and for a random cell agents = neighborhood.select_random().contents
# Maybe even return a random agent directly, across the CellCollection?
agent = neighborhood.select_random_agent() In summary I think there are still some open naming questions but I really like the direction this is going |
Just really quick since I am about to leave and wanted to drop you something for the weekend. Here is a branch with some implementations of the discussed things. Pretty much work in progress, but I think I wont be able to continue work until next week |
Thanks for that. I'll see what I can do over the weekend. First, I have an ABM MESA exam to make for my students. |
I started playing with the code drawing on #1978. Below are my notes of what I run into. I hope to have my version of the code on GitHub by the end of the weekend. It seems that Another possible bottleneck is try:
self.content.remove(agent)
except ValueError:
pass which gives a similar speed up. So the current bottleneck is the Another point is I will continue to play with this over the weekend if time allows for it. |
So an update. Below is the performance comparison as generated by @EwoutH benchmark. It contains both Schelling and WolfSheep, updated to use the new Grid class.
My code can be found at https://github.com/quaquel/mesa/tree/cell-space. It is still very much WIP. But some key points
|
Very interesting. I've been a fly on the wall for the last part of this discussion, I will try to properly read up on it tomorrow or Tuesday. |
Thanks for the valuable feedback @quaquel ! I also modified some code and pushed an update to my branch. Some things we updated in parallel, I picked up some ideas from you and also made some changes based on ideas that have been floating around here and there (thanks to everyone for those). As it is now, I find not only the potential performance gains interesting, but even more so I find the new syntax to be more expressive. I think thats an even better advantage. Plus this would give us a chance to remove a lot of code crust. A few changes I made:
Performance difference is about 30% for the schelling example compared to main branch.
The reason why I kept this outside is to make Cell as independent as possible of the concrete space. The current implementation is coupled to a Grid. I think it shouldn't be so much slower. Maybe you did the same "mistake" I did in the beginning and recreated the neighborhood_getter on each step? Because while creating the getter isn't costly in terms of performance, but you lose the cache. So you need to define the getter as a class or instance variable. Clearly this isn't ideal. Maybe we could define a default_neighborhood for the Cell constructor that is used for get_neighborhood? The reason I used a create_neighborhood_getter btw is specifically to allow fine-grained optimizations like splitting moore vs von Neumann. Currently it just creates a single function to handle all cases, but it would be simple to rewrite to return either a moore or a von Neumann function. I think the name "neighborhood_getter" isn't optimal, because the basic idea is that you have a function that takes a cell as its first argument and does something to it. So getting the neighborhhood is just one of many examples (albeit probably the most common). Thats it for now. I may post more later, but lets see how my work goes today. |
Just some quick thoughts before I am in to meetings all day.
|
Agreed this needs some investigating, before going forward. Again, I don' like the current semantics any way, so lets see where we go with this..
You are somewhat correct that this is rather unorthodox. It depends a bit on 1), but I just don't want to be unnecessarily restrictive. But performance wise its a single if statement, so it shouldn't really have an impact
Basically following the SOLID principles here and to have a clearer structure here. The space classes hold references to the Cell classes and may act upon them. But the cell class can exist independent of the space class. The usual benefits are that this makes the code easier to test and reason about.
Agreed. I think in my next work effort I'll put the code into the mesa experimental folder and open a draft PR. I will do this on my mesa fork and give you write access, so we can work on this together. |
On 2, if you encode Moore/von Neuman in the connections, neighborhood getting becomes universal across all discrete grids (so excel like, hex, networks, triangular, voronoi, etc). Or you get the slightly strange conceptual issue that non "excel" discrete spaces use a Moore neighborhood. It also makes the need for a formal neighborhood definition less important because it reduces to the neighorhood of a cell for a given radius. I am not so worried about performance for exactly the reasons you have given. On 5, clear and makes sense although I still wonder whether cells can exist outside of some DiscreteSpace. But from a coupling and testing point of view, I agree. I quickly looked at your code as well. I noticed the use of a |
Finally
I like this. Would, and if so how, that work for continious? Or will GridCells be by definition discrete?
Can we make this optional?
I like this above a list of lists. Would there still be ways to quickly access a row or column if needed, for example for torus (row[0], column[-1], etc.)?
This would mainly be useful for keeping track of neighbours (as agents) in your neighbourhoods right? Can we expose this to users, that they can cache any type of neighbourhood they like?
Would be amazing!
Might this make it difficult to create optimized implementations that connects a whole grid together at once (on init for example)? Or could we use another method there?
Really nice that we got back to the original issue!
This is fucking brilliant.
Was also thinking about that. I would really like support for Cells as Voronoi space. Just define a bunch of points in a continuous space, and whatever point is closest to you, in that space you are now. Neighbors of cells are all that share an edge/point. It would also amazing to just support loading in a custom polygons as cells. But we might get into mesa-geo space, @wang-boyu what do you think?
Agreed, we can take minor performance hits (~10% on model level) for an better API. Man-hours are generally more expensive than compute-hours.
Really looking forward to this, the Space module is getting too big (I'm a part of it, sorry).
At some point we have to think about how they interface. Currently the PropertyLayer is an specialized tool mainly targeted at the square Grids, so maybe it can become a specialized implementation for that.
Maybe general select functions with random/closest as a keyword? Like how I just want to say this right here is the pinnacle of open source. On of the most insightful and productive discussions I ever read. Most question I had when starting at the top of the discussion where addressed somewhere in it. Truly iterating on both's best ideas. You guys rock! |
Thank you @EwoutH for your words, this is really really encouraging!
@quaquel I think that's a brilliant approach! It simplifies the neighborhood and makes it generally applicable. Will try this out tonight. |
On reading your comment again after I understood it, I realize I just repeated what you wrote 😅 |
Some quick replies
|
## Summary This PR introduces an alteranative implementation for discrete spaces. This implementation centers on the explicit inclusion of a Cell class. Agents can occupy cells. Cells have connections, specifying their neighbors. The resulting classes offer a cell centric API where agents interact with a cell, and query the cell for its neighbors. To capture a collection of cells, and their content (_i.e._, Agents), this PR adds a new CellCollection class. This is an immutable collection of cells with convenient attribute accessors to the cells, or their agents. This PR also includes a CellAgent class which extends the default Agent class by adding a `move_to` method that works in conjunction with the new discrete spaces. From a performance point of view, the current code is a bit slower in building the grid and cell data structure, but in most use cases this increase in time for model initialization will be more than offset by the faster retrieval of neighboring cells and the agents that occupy them. ## Motive The PR emerged out of various experiments aimed at improving the performance of the current discrete space code. Moreover, it turned out that a cell centric API resolved various open issues (_e.g._, #1900, #1903, #1953). ## Implementation The key idea is to have Cells with connections, and using this to generate neighborhoods for a given radius. So all discrete space classes are in essence a [linked data structure](https://en.wikipedia.org/wiki/Linked_data_structure). The cell centric API idea is used to implement 4 key discrete space classes: OrthogonalMooreGrid, OrthogonalVonNeumannGrid (alternative for SingleGrid and MultiGrid, and moore and von Neumann neighborhood) , HexGrid (alternative for SingleHexGrid and MultiHexGrid), and Network (alternative for NetworkGrid). Cells have a capacity, so there is no longer a need for seperating Single and Multi grids. Moore and von Neumann reflect different neighborhood connections and so are now implemented as seperate classes. --------- Co-authored-by: Jan Kwakkel <j.h.kwakkel@tudelft.nl>
## Summary This PR introduces an alteranative implementation for discrete spaces. This implementation centers on the explicit inclusion of a Cell class. Agents can occupy cells. Cells have connections, specifying their neighbors. The resulting classes offer a cell centric API where agents interact with a cell, and query the cell for its neighbors. To capture a collection of cells, and their content (_i.e._, Agents), this PR adds a new CellCollection class. This is an immutable collection of cells with convenient attribute accessors to the cells, or their agents. This PR also includes a CellAgent class which extends the default Agent class by adding a `move_to` method that works in conjunction with the new discrete spaces. From a performance point of view, the current code is a bit slower in building the grid and cell data structure, but in most use cases this increase in time for model initialization will be more than offset by the faster retrieval of neighboring cells and the agents that occupy them. ## Motive The PR emerged out of various experiments aimed at improving the performance of the current discrete space code. Moreover, it turned out that a cell centric API resolved various open issues (_e.g._, projectmesa#1900, projectmesa#1903, projectmesa#1953). ## Implementation The key idea is to have Cells with connections, and using this to generate neighborhoods for a given radius. So all discrete space classes are in essence a [linked data structure](https://en.wikipedia.org/wiki/Linked_data_structure). The cell centric API idea is used to implement 4 key discrete space classes: OrthogonalMooreGrid, OrthogonalVonNeumannGrid (alternative for SingleGrid and MultiGrid, and moore and von Neumann neighborhood) , HexGrid (alternative for SingleHexGrid and MultiHexGrid), and Network (alternative for NetworkGrid). Cells have a capacity, so there is no longer a need for seperating Single and Multi grids. Moore and von Neumann reflect different neighborhood connections and so are now implemented as seperate classes. --------- Co-authored-by: Jan Kwakkel <j.h.kwakkel@tudelft.nl>
Sparked by #2309, maybe each Cell should have a neighborhood definition, which by default is inherited from the space it’s in, but can be overwritten. A space also has a default neighborhood definition for cells, which also can be overwritten. So basically:
|
One idea Jan and I had in a call is letting the CellCollection (or a mask) represent a neighbourhood, and then writing a utility function that can take things like: def_to_collection(include_center=False, radius: 1, moore: True):
... or def_to_mask, etc. |
When implementing #1898, I was inherently playing with spaces. A lot of times closeness (as a spatial relationship) is important, and a lot of time, there is a hard cut-off with a binary result: You are either a neighbor or not.
We have a few nice mechanisms built-in the current neighbor logic:
moore
: If True, return Moore neighborhood (including diagonals)If False, return Von Neumann neighborhood (exclude diagonals)
include_center
: If True, return the (x, y) cell as well. Otherwise, return surrounding cells only.radius
: radius, in cells, of neighborhood to get.However, it's a bit tedious to pass all those specifications each time you want to use a neighborhood of something. So therefor I would like to have some formal neighborhood specification, so that you can pass one object or variable throughout Mesa and knowing consistently what you need to send and what you get.
The text was updated successfully, but these errors were encountered: