Skip to content
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

Voronoi Tesselation based Discrete Space #2084

Open
wants to merge 49 commits into
base: main
Choose a base branch
from

Conversation

vitorfrois
Copy link

@vitorfrois vitorfrois commented Mar 21, 2024

First version of Voronoi Tesselation based Discrete Space,

described on issue #1895 . This feature allows the user to build a discrete space based on a random sample of points, where neighbors are defined by Delaunay Triangulation.

More specifically, Delaunay Triangulation is a dual-graph representation of the Voronoi Tesselation. Using this algorithm, we can easily find nearest neighbors without delimiting cells edges.
image

The library chosen for the triangulation algorithm implementation was PyHull, a wrapper of qhull C library, which deals with spatial calculations. The choice was made considering performance, size and usability (SciPy has a similar module but much heavier).

Based on my discussion with @EwoutH on the issue thread, i thought it would be useful to inherit DiscreteSpace class.

class VoronoiGrid(DiscreteSpace):

Example

from mesa import Model
from mesa.experimental.cell_space import VoronoiGrid, CellAgent

class MyAgent(CellAgent):
    def __init__(self, unique_id, model):
        super().__init__(unique_id, model)

Users can input cells centroids

points = [
    [0, 0], [0, 1], [0, 2], 
    [1, 0], [1, 1], [1, 2],
    [2, 0], [2, 1], [2, 2]]
grid = VoronoiGrid(centroids_coordinates=points, capacity=1, random=model.random)

and cells neighborhoods are defined by Delaunay triangulation

agent = MyAgent(1, model)
cell = grid.select_random_empty_cell()
agent.move_to(cell)
print(cell)
print(cell.neighborhood())
Cell([1, 0], [<__main__.MyAgent object at 0x7f2177268450>])
CellCollection({Cell([0, 1], []): [], Cell([1, 1], []): [], Cell([0, 0], []): [], Cell([2, 1], []): [], Cell([2, 0], []): []})

Next Steps

Definitely, next steps involve computing the boundaries of each cell using Voronoi Tesselation. This allow us to define capacity of each cell based on its area/volume.

@EwoutH EwoutH added feature Release notes label experimental Release notes label labels Mar 23, 2024
@EwoutH EwoutH requested a review from wang-boyu March 23, 2024 11:19
@EwoutH
Copy link
Contributor

EwoutH commented Mar 23, 2024

Thanks, it's a great start! Using DelaunayTri is a smart approach.

I guess move_to currently move an agent to the cells centroid? While that is logical behavior, I would also like to see an method in which agents are moved to A) the closest point in the cell from their current position and B) a random spot in the cell. This could be a separate PR.

Another thing that it's needed in the long term is a method to check in which cell a point (and thus an agent) is.

@wang-boyu I would also appreciate a review from your mesa-geo expertise.

For the currently functionality, I would like some additional docstring and unittests. Then you can decide if you want to add further functionality to this PR, or merge this first and follow up with new PRs.

Edit: One thought, also for the other maintainers: Would we need separate Discrete and Continuous Voronoi spaces?

class VoronoiGrid(DiscreteSpace):
def __init__(
self,
centroids_coordinates: Sequence[Sequence[float]],
Copy link
Contributor

@EwoutH EwoutH Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work with Shapely Points? That might be very useful for Mesa geo applications (cc @wang-boyu).

Just add with adding a simple test case for this, maybe it will just work.



class VoronoiGrid(DiscreteSpace):
def __init__(
Copy link
Contributor

@EwoutH EwoutH Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to to initialize the super class here (with super().__init__())

from random import Random
from typing import Optional

from pyhull.delaunay import DelaunayTri
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried about adding this package as a dependency. It's last update was in 2015.

Maybe we can use SciPy?

Otherwise maybe shapely.ops.triangulate or keeping the code in our own repo.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I choose PyHull because Mesa is not using Scipy already and it is a very heavy dependency. I'll take a look at Shapely, it looks a good alternative, and as you said, can be integrated with Mesa Geo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. Shapely is also quite heavy unfortunately. It’s just a little code, can we house it in an own class?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The computation can be quite heavy acoording to the number of points, but it is duable. In my opinion, since the result of Delaunay Triangulation is a graph, we can use NetworkGrid to represent it, and in the continuous case, implement in the Mesa Geo using Shapely.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable!

@wang-boyu
Copy link
Member

Thanks for the PR. I have to say that I'm still a bit confused about why this is based on discrete space, not continuous space (as I mentioned in #1895 (comment) and #1895 (comment)).

As a concrete example, consider this simple space:

Cell 1 Cell 2 Cell 3
O ? X

If it is a Voronoi space, should Cell 2 be split into halves, where left half belongs to agent of type O and right half belongs to agent of type X? How does this work in discrete space?

@tpike3
Copy link
Contributor

tpike3 commented Jul 12, 2024

Apologies, a little late to this discussion, this is really cool @vitorfrois.

As dependencies and keeping Mesa lightweight is a constant struggle, but form a very superficial look is there a reason we wont use networkx (https://networkx.org/documentation/stable/reference/algorithms/voronoi.html) and this has a corresponding example that may work for mesa_geo (https://networkx.org/documentation/stable/auto_examples/geospatial/plot_delaunay.html)?

Let me know what you think/ what I am missing?

@vitorfrois
Copy link
Author

@wang-boyu

I have to say that I'm still a bit confused about why this is based on discrete space, not continuous space. How does this work in discrete space?

I'm considering the discrete space for Voronoi Tesselation as a static/non changing graph, where neighbors are obtained by Delaunay Triangulation. As @EwoutH said, it is only a special case for HexGrid where cell are not equally spaced.

I think its more about a choice of implementation where I considered mainly the Cholera example. As I suggested yesterday,

since the result of Delaunay Triangulation is a graph, we can use NetworkGrid to represent it, and in the continuous case, implement in the Mesa Geo using Shapely.

Does that make sense?

@vitorfrois
Copy link
Author

@tpike3 thanks for the comment. That's more or less what I'm thinking. In Mesa Geo, we have opportunity to do it better with Shapely

@wang-boyu
Copy link
Member

I'm considering the discrete space for Voronoi Tesselation as a static/non changing graph, where neighbors are obtained by Delaunay Triangulation.

Thanks for clarifying. Appreciate it.

Question: Is the Delaunay Triangulation done in continuous space?

@vitorfrois
Copy link
Author

Question: Is the Delaunay Triangulation done in continuous space?

Yes, it is

puer-robustus and others added 6 commits July 15, 2024 13:07
While it might be desired in a specific model to have the same agent
be placed in multiple spots simultaneously, the typical use case is
that one agent has one position at every given moment. This commit
decorates the place_agent() method in a way that it emits a warning
when called with an agent which already has a location.

Fixes: projectmesa#1522
uv is a fast drop-in pip, pip-compile, virtualenv etc replacement created by the creator of Ruff.
* Update Ruff to 0.3.4; apply ruff format .

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…ojectmesa#2087)

This fixes
- some minor grammatical errors,
- an incorrect header indentation level,
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](astral-sh/ruff-pre-commit@v0.3.4...v0.3.5)
- [github.com/asottile/pyupgrade: v3.15.0 → v3.15.2](asottile/pyupgrade@v3.15.0...v3.15.2)
pre-commit-ci bot and others added 19 commits July 15, 2024 13:07
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.5.0](astral-sh/ruff-pre-commit@v0.4.3...v0.5.0)
- [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](asottile/pyupgrade@v3.15.2...v3.16.0)
- [github.com/codespell-project/codespell: v2.2.6 → v2.3.0](codespell-project/codespell@v2.2.6...v2.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This doesn't seem to be used as often as the current simple namespace.
Fixes two things pre-commit detected:
- Some codespell errors
- A case of E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
Removing the old, legacy visualisation. It will still be available in 2.3.x.

Checkout the Visualization Tutorial (https://mesa.readthedocs.io/en/latest/tutorials/visualization_tutorial.html) to migrate to the new visualisation.
Currently measures crashed when it's None (which is the default). The only way to circumvent this is adding [], but that's non-ideal and not obvious.

This PR fixes that, so that measures can actually be the default value of None.

This also proves we need to test Jupyter viz in notebooks, not only in console, since both have different code paths.
- Add docstring to Jupyter viz module and functions
- Render the docstring in Read the Docs: https://mesa.readthedocs.io/en/latest/apis/visualization.html
* Jupyter Viz: Don't avoid interactive backend

We were avoiding the interactive backend, but that isn't recommended anymore and Solara should take care of that itself.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Sets the version to 3.0.0a0 for the first Mesa 3.0 pre-release, and updates the release notes with 2.3.1 and 3.0.0a0 changelog
Don't track virtual environment files in Git
@vitorfrois
Copy link
Author

vitorfrois commented Jul 15, 2024

Updates

  • Implemented Voronoi from scratch (basically copied a simple already implemented algorithm)
  • Now using the regions resulting from Delaunay/Voronoi Triangulation to draw Voronoi cells in JupyterViz. As the cell is an important in voronoi diagram, I added an attribute cell_coloring_property to VoronoiGrid. When drawing the grid, the polygon is drawn with plt.fill(polygon, alpha=cell.properties[space.cell_coloring_property]), so one can tweak the cell coloring property to adjust the visualization. In the example, the cell coloring property is cases ratio, which give darker shades on cells with more cases.
  • Altough, could not cover visualization/components/matplotlib.py with tests :(
  • It was simpler and more visual to directly implement Voronoi cells since they were computed anyway, instead of the visualization using NetworkX, mentioned earlier. It is not that performant though
    image
    This gives us the example above

Copy link
Contributor

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really outstanding, Vítor. That space visualization especially, it really offers a lot of value.

Small minor comments. I would also like to see more test coverage, if you have the chance.

I’m going to approve conceptually, especially considering the cell space is still experimental. @rht are you able to do a technical code review and merge if it looks good?

CC @quaquel, you might also find this interesting (and have some comments).

@@ -43,6 +43,7 @@ dependencies = [
"pandas",
"solara",
"tqdm",
"pyhull",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can now be removed right?

from mesa.experimental.cell_space.discrete_space import DiscreteSpace


class Delaunay:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put this in a separate file? @rht?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In voronoi.py should be fine. We can split it once it is used in more than one context. The docstring should clearly indicate that this is a helper class, and not a space implementation it itself. I'd rename it to _Delaunay for now so that we can merge this PR sooner. We can discuss about other use cases later.

class Delaunay:
"""
Class to compute a Delaunay triangulation in 2D
ref: https://github.com/jmespadero/pyDelaunay2D
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much percentage of the code is based on this GitHub repo? Their license is GPLv3, and we actually can't copy their code because there will be a licensing issue.

@vitorfrois
Copy link
Author

vitorfrois commented Jul 21, 2024 via email

@rht
Copy link
Contributor

rht commented Jul 21, 2024

There are 2 ways to proceed:

  1. Use ChatGPT to do code laundry, i.e. ask it to do Delaunay triangulation code. It probably is going to be based on that GPLv3 repo, but you can blame ClosedAI for that instead.
  2. Use SciPy, but have SciPy to be an optional dependency, only imported when the user needs VoronoiGrid

I'm more in favor of option 2, because option 1 is rather shady.

@EwoutH
Copy link
Contributor

EwoutH commented Jul 21, 2024

Yeah the license issue is a big one. Another (far fetched) option is to contact the maintainer to see if he’s willing to publish on another, more permissive license.

@EwoutH
Copy link
Contributor

EwoutH commented Jul 25, 2024

@vitorfrois I would like to try to contact the maintainers. Do you want to write them a message or would you like me to do it?

@EwoutH
Copy link
Contributor

EwoutH commented Jul 29, 2024

I can do it tomorrow, then hopefully we can move this forward next week!

I would like to include it in the next 3.0 alpha release.

@EwoutH
Copy link
Contributor

EwoutH commented Aug 1, 2024

I reached out Tuesday, hopefully we will get an answer soon: jmespadero/pyDelaunay2D#7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental Release notes label feature Release notes label
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants