8000 ENH: Added Cellular Automata-based Enclosed Tessellation by yu-ta-sato · Pull Request #686 · pysal/momepy · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

ENH: Added Cellular Automata-based Enclosed Tessellation #686

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

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

Conversation

yu-ta-sato
Copy link

Based on the discussion #684, I added Cellular Automata-based Voronoi diagram for enclosed_tessellation, remaining the API the same as before.

Here's a sample code:

from momepy import enclosures, enclosed_tessellation
import geopandas as gpd
import matplotlib.pyplot as plt

blg_polygons = [
    shapely.geometry.Polygon([(15, 32), (35, 32), (35, 38), (15, 38)]),
    shapely.geometry.Polygon([(15, 22), (35, 22), (35, 28), (15, 28)]),
    shapely.geometry.Polygon([(15, 92), (35, 92), (35, 98), (15, 98)]),
    shapely.geometry.Polygon([(15, 82), (35, 82), (35, 88), (15, 88)]),
    shapely.geometry.Polygon([(45, 62), (65, 62), (65, 68), (45, 68)]),
    shapely.geometry.Polygon([(45, 52), (65, 52), (65, 58), (45, 58)]),
]
buildings_gdf = GeoDataFrame(
    {
        "building_id": list(range(1, len(blg_polygons) + 1)),
        "geometry": blg_polygons,
    },
    crs="EPSG:3857",
)

# Define sample barrier geometries.
barrier_geoms = [
    shapely.geometry.LineString([(0, 0), (80, 0)]),
    shapely.geometry.LineString([(80, 0), (80, 120)]),
    shapely.geometry.LineString([(80, 120), (0, 120)]),
    shapely.geometry.LineString([(0, 120), (0, 0)]),
    shapely.geometry.LineString([(40, 0), (40, 110)]),
    shapely.geometry.LineString([(10, 30), (40, 30)]),
    shapely.geometry.LineString([(10, 90), (40, 90)]),
    shapely.geometry.LineString([(40, 60), (70, 60)]),
]

barrier_gdf = GeoDataFrame(
    {
        "name": [
            "Bottom Edge",
            "Right Edge",
            "Top Edge",
            "Left Edge",
            "Main Vertical",
            "Left Cul-de-Sac (Bottom)",
            "Left Cul-de-Sac (Top)",
            "Right Cul-de-Sac (Middle)",
        ],
        "geometry": barrier_geoms,
    },
    crs="EPSG:3857",
)

There're three patterns of enclosed tessellation with comparison:

# Generate enclosure
enclosure = enclosures(barrier_gdf)

# Original enclosed_tessellation function
enclosed_tess_original = enclosed_tessellation(
    buildings_gdf,
    enclosures=enclosure,
)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

enclosure.plot(ax=axes[0], alpha=0.1)
buildings_gdf.plot(ax=axes[0], color="red")
barrier_gdf.plot(ax=axes[0], color="blue")
enclosed_tess_original.plot(ax=axes[0], alpha=0.3, color="red", edgecolor="black", linewidth=0.5)
axes[0].set_title("Original")

# New enclosed_tessellation function (without inner barriers)
enclosed_tess_no_inner = enclosed_tessellation(
    buildings_gdf,
    enclosures=enclosure,
    use_ca=True,
    cell_size=1.0,
    neighbor_mode="moore",
    fulfill=True)

enclosure.plot(ax=axes[1], alpha=0.1)
buildings_gdf.plot(ax=axes[1], color="red")
barrier_gdf.plot(ax=axes[1], color="blue")
enclosed_tess_no_inner.plot(ax=axes[1], alpha=0.3, color="red", edgecolor="black", linewidth=0.5)
axes[1].set_title("CA, no inner barriers")

# New enclosed_tessellation function (with inner barriers)
enclosed_tess_with_inner = enclosed_tessellation(
    buildings_gdf,
    enclosures=enclosure,
    use_ca=True,
    cell_size=1.0,
    neighbor_mode="moore",
    fulfill=True,
    barriers_for_inner=barrier_gdf)

enclosure.plot(ax=axes[2], alpha=0.1)
buildings_gdf.plot(ax=axes[2], color="red")
barrier_gdf.plot(ax=axes[2], color="blue")
enclosed_tess_with_inner.plot(ax=axes[2], alpha=0.3, color="red", edgecolor="black", linewidth=0.5)
axes[2].set_title("CA, with inner barriers")

plt.tight_layout()
plt.show()

output

In the Liverpool City Region by the Overture Maps, the output of the new algorithm with inner barriers (cell size: 1m)are like this:
Screenshot 2025-02-25 at 03 07 40

If it's good to go, let me update the docs.

Copy link
codecov bot commented Feb 25, 2025

Codecov Report

Attention: Patch coverage is 15.86207% with 122 lines in your changes missing coverage. Please review.

Project coverage is 96.5%. Comparing base (4037c70) to head (d754ce4).
Report is 103 commits behind head on main.

Files with missing lines Patch % Lines
momepy/functional/_elements.py 15.9% 122 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##            main    #686     +/-   ##
=======================================
- Coverage   97.4%   96.5%   -0.8%     
=======================================
  Files         26      40     +14     
  Lines       4328    7350   +3022     
=======================================
+ Hits        4214    7094   +2880     
- Misses       114     256    +142     
Files with missing lines Coverage Δ
momepy/functional/_elements.py 53.4% <15.9%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jGaboardi
Copy link
Member

@yu-ta-sato This is really cool! Will you be able to add appropriate testing for this new functionality?

@yu-ta-sato
Copy link
Author

@jGaboardi Yes of course! I wanted to be on the same page with you guys in advance, especially the direction of its implementation. If needed, I can separate the function from enclosed_tessellation. Will let @martinfleis have a look at the scripts as well.

@martinfleis
Copy link
Member

Hold on with testing, I'll have some requests but need more time to properly look at it.

@jGaboardi jGaboardi added the enhancement New feature or request label Feb 25, 2025
@martinfleis
Copy link
Member

Hey, enclosed tessellation is supposed to be continuous coverage by definition. As illustrated below, that is not the case at the moment. Also, I would very much prefer to ensure that the lines are as straight as possible, without this artifact of the grid we see here. We can either do that in conversion of the grid to polygons or afterwards using coverage simplification. We will need to touch the API as well but let's focus on ensuring this behaves the way we want first.

Screenshot 2025-03-04 at 13 59 35

@yu-ta-sato
Copy link
Author

We can either do that in conversion of the grid to polygons or afterwards using coverage simplification.

The cellular-automata grids are already polygonised in the returning outputs (by _get_cell_polygon in _voronoi_by_ca). For the latter, Ramer–Douglas–Peucker algorithm might be applied for the smoothing?

@martinfleis
Copy link
Member

For the latter, Ramer–Douglas–Peucker algorithm might be applied for the smoothing?

No. We need to ensure that whatever comes from the grid is continuous coverage and then use upcoming coverage_simplify which ensures that the simplification does not break topological relations between neighbouring cells.

@yu-ta-sato
Copy link
Author

yu-ta-sato commented Mar 4, 2025

Did you enable fulfill=True? I prepared for the parameter so that the cells of boundaries are assigned to the closest tessellations to ensure the coverage (_assign_adjacent_seed_cells). Hope this is what you mean by the continuous coverage

image

@martinfleis
Copy link
Member

Ah, no. I did not. However, this should not be an option, it should be set by default.

@yu-ta-sato
Copy link
Author

Need to chase the on-going discussion on shapely a bit more, but can we implement coverage_simplify on the polygons once it gets stable in its v2.1? Meanwhile I can work on the testing and API design.

@martinfleis
Copy link
Member

We can already do that. It will just be tested in the dev CI environment only.

blg = blg[
shapely.area(
shapely.intersection(
blg.geometry.array, shapely.geometry.Polygon(poly.boundary)
Copy link
Member

Choose a reason for hiding this comment

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

What is the rationale behind this? Why are you ignoring holes when a MultiPolygon is provided? Feels inconsistent.

Copy link
Author

Choose a reason for hiding this comment

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

The initial intention was to let the poly be accommodated with inner barriers, but now they're separated. Somehow this part remained, so will remove it.

if barrier_geoms.geom_type == "Polygon":
# Take buffer of polygon and extract its exterior boundary
barrier_geoms_buffered = GeoSeries(
[barrier_geoms.buffer(10).exterior], crs=seed_geoms.crs
Copy link
Member

Choose a reason for hiding this comment

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

Where does 10 come from here? Can't use hardcoded values here.

Copy link
Author

Choose a reason for hiding this comment

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

This amount of buffer was needed to ensure that all the extent are assigned to either of the cell state, which could be arbitrary number (This kind of vacant cells occurred between three tessellations). Will set the number based on the cell size automatically.

Comment on lines 589 to 590
raise ImportError(
"Shapely 2.1.0 or higher is required for coverage_simplify."
Copy link
Member

Choose a reason for hiding this comment

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

We should raise this before we start doing anything else. Fail fast.

Copy link
Member

Choose a reason for hiding this comment

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

Also, the error should say something like Shapely 2.1.0 or higher is required for tessellation with inner barriers provided.

# Only keep the geometry which is within the enclosure
inner_barriers = inner_barriers[inner_barriers.within(enclosure)]

return GeoSeries(inner_barriers.geometry, crs=barriers.crs)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't inner_barriers already a GeoSeries?

Copy link
Author

Choose a reason for hiding this comment

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

I checked it, you're right.

yu-ta-sato and others added 2 commits March 26, 2025 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0