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

ENH: first bits of refactored dimension #532

Merged
merged 7 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/envs/310-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dependencies:
- python=3.10
- geopandas
- inequality
- libpysal>=4.6.0
- libpysal>=4.8.0
- mapclassify
- networkx
- packaging
Expand Down
2 changes: 1 addition & 1 deletion ci/envs/310-oldest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dependencies:
- python=3.10
- geopandas=0.12
- inequality
- libpysal=4.6.0
- libpysal=4.8.0
- mapclassify
- networkx=2.7
- numpy=1.22
Expand Down
2 changes: 1 addition & 1 deletion ci/envs/311-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dependencies:
- python=3.11
- geopandas
- inequality
- libpysal>=4.6.0
- libpysal>=4.8.0
- mapclassify
- networkx
- packaging
Expand Down
2 changes: 1 addition & 1 deletion ci/envs/312-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies:
- dask
- geopandas
- inequality
- libpysal>=4.6.0
- libpysal>=4.8.0
- networkx
- osmnx
- packaging
Expand Down
2 changes: 1 addition & 1 deletion ci/envs/312-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dependencies:
- python=3.12
- geopandas
- inequality
- libpysal>=4.6.0
- libpysal>=4.8.0
- mapclassify
- networkx
- osmnx
Expand Down
4 changes: 2 additions & 2 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ Dependencies

Required dependencies:

- `geopandas`_ (>= 0.8.0)
- `libpysal`_ (>= 4.6.0)
- `geopandas`_ (>= 0.12.0)
- `libpysal`_ (>= 4.8.0)
- `networkx`_
- `tqdm`_

Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies:
- geopandas
- inequality
- jupyter
- libpysal>=4.6.0
- libpysal>=4.8.0
- mapclassify
- matplotlib
- momepy
Expand Down
1 change: 1 addition & 0 deletions momepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .distribution import *
from .diversity import *
from .elements import *
from .functional._dimension import *
from .graph import *
from .intensity import *
from .preprocessing import *
Expand Down
Empty file added momepy/functional/__init__.py
Empty file.
144 changes: 144 additions & 0 deletions momepy/functional/_dimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import numpy as np
import pandas as pd
import shapely

__all__ = [
"volume",
"floor_area",
"courtyard_area",
"longest_axis_length",
"perimeter_wall",
]


def volume(area, height):
"""
Calculates volume of each object in given GeoDataFrame based on its height and area.

.. math::
area * height

Parameters
----------
area : array_like
array of areas
height : array_like
array of heights

Returns
-------
array_like
"""
return area * height


def floor_area(area, height, floor_height=3):
"""Calculates floor area of each object based on height and area.

The number of
floors is simplified into the formula: ``height // floor_height``. B
y default one floor is approximated to 3 metres.

.. math::
area * \\frac{height}{floor_height}


Parameters
----------
area : array_like
array of areas
height : array_like
array of heights
floor_height : float | array_like, optional
float denoting the uniform floor height or an array_like reflecting the building
height by geometry, by default 3

Returns
-------
array_like
"""
return area * (height // floor_height)


def courtyard_area(gdf):
"""Calculates area of holes within geometry - area of courtyards.

Parameters
----------
gdf : GeoDataFrame
A GeoDataFrame containing objects to analyse.

Returns
-------
pandas.Series
"""
return pd.Series(
shapely.area(shapely.polygons(shapely.get_exterior_ring(gdf.geometry.array)))
- gdf.area,
index=gdf.index,
name="courtyard_area",
)


def longest_axis_length(gdf):
"""Calculates the length of the longest axis of object.

Axis is defined as a
diameter of minimal bounding circle around the geometry. It does
not have to be fully inside an object.

.. math::
\\max \\left\\{d_{1}, d_{2}, \\ldots, d_{n}\\right\\}

Parameters
----------
gdf : GeoDataFrame
A GeoDataFrame containing objects to analyse.

Returns
-------
pandas.Series
"""
return shapely.minimum_bounding_radius(gdf.geometry) * 2


def perimeter_wall(gdf, graph=None):
"""
Calculate the perimeter wall length the joined structure.
jGaboardi marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
gdf : GeoDataFrame
GeoDataFrame containing objects to analyse
graph : libpysal.graph.Graph, optional
Graph encoding Queen contiguity of ``gdf``

Returns
-------
pandas.Series
"""

if graph is None:
from libpysal.graph import Graph

graph = Graph.build_contiguity(gdf)

isolates = graph.isolates

# measure perimeter walls of connected components while ignoring isolates
blocks = gdf.drop(isolates)
component_perimeter = (
blocks[[blocks.geometry.name]]
.set_geometry(blocks.buffer(0.01))
.dissolve(by=graph.component_labels.drop(isolates))
.exterior.length
)

# combine components with isolates
results = pd.Series(np.nan, index=gdf.index, name="perimeter_wall")
results.loc[isolates] = gdf.geometry[isolates].exterior.length
results.loc[results.index.drop(isolates)] = component_perimeter.loc[
graph.component_labels.loc[results.index.drop(isolates)]
].values

return results
Empty file.
86 changes: 86 additions & 0 deletions momepy/functional/tests/test_dimension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import geopandas as gpd
import numpy as np
import pandas as pd
import pytest
from libpysal.graph import Graph
from packaging.version import Version
from shapely import Polygon

import momepy as mm

GPD_013 = Version(gpd.__version__) >= Version("0.13")


class TestDimensions:
def setup_method(self):
test_file_path = mm.datasets.get_path("bubenec")
self.df_buildings = gpd.read_file(test_file_path, layer="buildings")
self.df_streets = gpd.read_file(test_file_path, layer="streets")
self.df_tessellation = gpd.read_file(test_file_path, layer="tessellation")
self.df_buildings["height"] = np.linspace(10.0, 30.0, 144)

def test_volume(self):
# pandas
expected = self.df_buildings.area * self.df_buildings["height"]
pd.testing.assert_series_equal(
mm.volume(self.df_buildings.area, self.df_buildings["height"]), expected
)

# numpy
expected = self.df_buildings.area.values * self.df_buildings["height"].values
np.testing.assert_array_equal(
mm.volume(
self.df_buildings.area.values, self.df_buildings["height"].values
),
expected,
)

def test_floor_area(self):
expected = self.df_buildings.area * (self.df_buildings["height"] // 3)
pd.testing.assert_series_equal(
mm.floor_area(self.df_buildings.area, self.df_buildings["height"]), expected
)

expected = self.df_buildings.area * (self.df_buildings["height"] // 5)
pd.testing.assert_series_equal(
mm.floor_area(
self.df_buildings.area, self.df_buildings["height"], floor_height=5
),
expected,
)

floor_height = np.repeat(np.array([3, 4]), 72)
expected = self.df_buildings.area * (
self.df_buildings["height"] // floor_height
)
pd.testing.assert_series_equal(
mm.floor_area(
self.df_buildings.area,
self.df_buildings["height"],
floor_height=floor_height,
),
expected,
)

def test_courtyard_area(self):
expected = self.df_buildings.geometry.apply(
lambda geom: Polygon(geom.exterior).area - geom.area
)
pd.testing.assert_series_equal(
mm.courtyard_area(self.df_buildings), expected, check_names=False
)

@pytest.mark.skipif(not GPD_013, reason="minimum_bounding_radius() not available")
def test_longest_axis_length(self):
expected = self.df_buildings.minimum_bounding_radius() * 2
pd.testing.assert_series_equal(
mm.longest_axis_length(self.df_buildings), expected, check_names=False
)

def test_perimeter_wall(self):
result = mm.perimeter_wall(self.df_buildings)
adj = Graph.build_contiguity(self.df_buildings)
result_given_graph = mm.perimeter_wall(self.df_buildings, adj)

pd.testing.assert_series_equal(result, result_given_graph)
assert result[0] == pytest.approx(137.210, rel=1e-3)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ classifiers = [
requires-python = ">=3.10"
dependencies = [
"geopandas>=0.12.0",
"libpysal>=4.6.0",
"libpysal>=4.8.0",
"networkx>=2.7",
"packaging",
"pandas!=1.5.0",
Expand Down