ObjectNat 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ObjectNat might be problematic. Click here for more details.

Files changed (33) hide show
  1. objectnat/_api.py +14 -14
  2. objectnat/_config.py +43 -47
  3. objectnat/_version.py +1 -1
  4. objectnat/methods/coverage_zones/__init__.py +3 -3
  5. objectnat/methods/coverage_zones/graph_coverage.py +11 -4
  6. objectnat/methods/coverage_zones/radius_voronoi_coverage.py +4 -2
  7. objectnat/methods/coverage_zones/stepped_coverage.py +20 -10
  8. objectnat/methods/isochrones/__init__.py +1 -1
  9. objectnat/methods/isochrones/isochrone_utils.py +167 -167
  10. objectnat/methods/isochrones/isochrones.py +31 -11
  11. objectnat/methods/noise/__init__.py +3 -3
  12. objectnat/methods/noise/noise_init_data.py +10 -10
  13. objectnat/methods/noise/noise_reduce.py +155 -155
  14. objectnat/methods/noise/noise_simulation.py +14 -13
  15. objectnat/methods/noise/noise_simulation_simplified.py +10 -9
  16. objectnat/methods/point_clustering/__init__.py +1 -1
  17. objectnat/methods/point_clustering/cluster_points_in_polygons.py +3 -3
  18. objectnat/methods/provision/__init__.py +1 -1
  19. objectnat/methods/provision/provision.py +112 -20
  20. objectnat/methods/provision/provision_exceptions.py +59 -59
  21. objectnat/methods/provision/provision_model.py +323 -348
  22. objectnat/methods/utils/__init__.py +1 -1
  23. objectnat/methods/utils/geom_utils.py +173 -173
  24. objectnat/methods/utils/graph_utils.py +5 -5
  25. objectnat/methods/utils/math_utils.py +32 -32
  26. objectnat/methods/visibility/__init__.py +6 -6
  27. objectnat/methods/visibility/visibility_analysis.py +9 -17
  28. objectnat-1.3.0.dist-info/METADATA +201 -0
  29. objectnat-1.3.0.dist-info/RECORD +33 -0
  30. {objectnat-1.2.2.dist-info → objectnat-1.3.0.dist-info}/WHEEL +1 -1
  31. objectnat-1.2.2.dist-info/METADATA +0 -116
  32. objectnat-1.2.2.dist-info/RECORD +0 -33
  33. {objectnat-1.2.2.dist-info/licenses → objectnat-1.3.0.dist-info}/LICENSE.txt +0 -0
@@ -1,5 +1,3 @@
1
- from typing import Tuple
2
-
3
1
  import geopandas as gpd
4
2
  import numpy as np
5
3
  import pandas as pd
@@ -18,29 +16,53 @@ def get_service_provision(
18
16
  threshold: int,
19
17
  buildings_demand_column: str = "demand",
20
18
  services_capacity_column: str = "capacity",
21
- pandarallel_init_kwargs: dict = None,
22
- ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
23
- """Calculate load from buildings with demands on the given services using the distances matrix between them.
19
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
20
+ """
21
+ Compute service provision between demand locations (buildings) and service facilities.
24
22
 
25
- Parameters:
26
- services (gpd.GeoDataFrame):
27
- GeoDataFrame of services
28
- adjacency_matrix (pd.DataFrame):
29
- DataFrame representing the adjacency matrix
23
+ The function implements a **gravity-based allocation model**: service capacity is
24
+ distributed across nearby demand points with weights that **decay with the square
25
+ of distance (or generalized cost)**. Closer buildings receive proportionally
26
+ higher shares of the available capacity.
27
+
28
+ Args:
30
29
  buildings (gpd.GeoDataFrame):
31
- GeoDataFrame of demanded buildings
30
+ GeoDataFrame of **demand locations** (e.g., residential buildings).
31
+ Must include a numeric column with demand values
32
+ (see ``buildings_demand_column``).
33
+ adjacency_matrix (pd.DataFrame):
34
+ A rectangular DataFrame representing **OD (origin–destination) costs**
35
+ between ``buildings`` (rows) and ``services`` (columns).
36
+ Units must match ``threshold`` (e.g., minutes or meters).
37
+ Missing or infinite values (``NaN`` or ``inf``) are treated as **unreachable**.
38
+ The row index must match ``buildings.index`` and column index must
39
+ match ``services.index``.
40
+ services (gpd.GeoDataFrame):
41
+ GeoDataFrame of **service facilities** (e.g., schools, clinics).
42
+ Must include a numeric column with service capacity
43
+ (see ``services_capacity_column``).
32
44
  threshold (int):
33
- Threshold value
45
+ Maximum allowed cost value for assignment.
46
+ Any OD entry **greater than this threshold** is considered unreachable.
47
+ Units are the same as in ``adjacency_matrix``.
34
48
  buildings_demand_column (str):
35
- column name of buildings demands
49
+ Column name of building demand values. Default is ``"demand"``.
36
50
  services_capacity_column (str):
37
- column name of services capacity
38
- pandarallel_init_kwargs (dict):
39
- Dictionary of keyword arguments to pass to pandarallel
51
+ Column name of service capacity values. Default is ``"capacity"``.
40
52
 
41
53
  Returns:
42
- (Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]): Tuple of GeoDataFrames representing provision
43
- buildings, provision services, and provision links
54
+ Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
55
+ A tuple of three GeoDataFrames:
56
+
57
+ - **buildings**: input buildings with updated provision metrics.
58
+ - **services**: input services with updated load and capacity metrics.
59
+ - **links**: building–service links within the threshold, containing
60
+ allocated demand shares and distances/costs based on the gravity model.
61
+
62
+ Notes:
63
+ - The model is **gravity-based**, with cost weights decaying by the **square of distance**.
64
+ - Unreachable OD pairs (``NaN`` or ``inf``) are ignored.
65
+ - The function does not perform routing; it expects a precomputed OD matrix.
44
66
  """
45
67
  buildings = buildings.copy()
46
68
  services = services.copy()
@@ -53,15 +75,42 @@ def get_service_provision(
53
75
  demanded_buildings=buildings,
54
76
  adjacency_matrix=adjacency_matrix,
55
77
  threshold=threshold,
56
- pandarallel_init_kwargs=pandarallel_init_kwargs,
57
78
  ).run()
58
79
  return provision_buildings, provision_services, provision_links
59
80
 
60
81
 
61
82
  def clip_provision(
62
83
  buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, selection_zone: gpd.GeoDataFrame
63
- ) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
84
+ ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
85
+ """
86
+ Clip service provision results to a specific geographic boundary.
87
+
88
+ Keeps only:
89
+ * buildings that intersect the ``selection_zone``;
90
+ * links that connect to the kept buildings;
91
+ * services referenced by those links.
92
+
93
+ Args:
94
+ buildings:
95
+ GeoDataFrame of buildings **after** :func:`get_service_provision`.
96
+ services:
97
+ GeoDataFrame of services **after** :func:`get_service_provision`.
98
+ links:
99
+ GeoDataFrame of building–service links from
100
+ :func:`get_service_provision`. Must include indices or columns
101
+ to match buildings and services.
102
+ selection_zone:
103
+ GeoDataFrame (polygon or multipolygon) defining the clipping area.
104
+
105
+ Returns:
106
+ Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
107
+ The filtered subsets of buildings, services, and links
108
+ that fall inside the specified zone.
64
109
 
110
+ Notes:
111
+ - The function performs **spatial filtering only**.
112
+ It does **not** recompute or redistribute demand/supply.
113
+ """
65
114
  assert selection_zone.crs == buildings.crs == services.crs == links.crs, (
66
115
  f"CRS mismatch: buildings_crs:{buildings.crs}, "
67
116
  f"links_crs:{links.crs} , "
@@ -83,6 +132,49 @@ def clip_provision(
83
132
  def recalculate_links(
84
133
  buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, new_max_dist: float
85
134
  ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
135
+ """
136
+ Recalculate provision aggregates after tightening the accessibility threshold.
137
+
138
+ Removes all links whose cost (distance or time) exceeds ``new_max_dist``, then
139
+ updates demand and capacity aggregates accordingly. This is done **without
140
+ redistributing** removed demand to alternative services.
141
+
142
+ Args:
143
+ buildings:
144
+ GeoDataFrame of buildings after :func:`get_service_provision`.
145
+ Expected to include provision-related fields such as demand, demand_left,
146
+ supplied demand, and average distance/cost.
147
+
148
+ services:
149
+ GeoDataFrame of services after :func:`get_service_provision`, with
150
+ fields describing remaining capacity and service load.
151
+
152
+ links:
153
+ GeoDataFrame of building–service links containing at least:
154
+
155
+ - ``building_index``
156
+ - ``service_index``
157
+ - ``distance`` (or time cost, in the same units as ``new_max_dist``)
158
+ - ``demand`` (assigned portion)
159
+
160
+ new_max_dist:
161
+ New maximum allowed cost value (same units as OD/threshold).
162
+ Links with cost **greater than** this value are removed.
163
+
164
+ Returns:
165
+ tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
166
+ - **buildings**: updated aggregate demand metrics and recalculated
167
+ average cost.
168
+ - **services**: updated load and capacity fields after freeing excess capacity.
169
+ - **links**: subset of links that remain within the new threshold.
170
+
171
+ Notes:
172
+ - If no links exceed ``new_max_dist``, the function logs a warning
173
+ and returns the original inputs unchanged.
174
+ - Average cost values are recomputed based on remaining links.
175
+ If a building has no remaining assigned demand, ``avg_dist`` becomes ``NaN``.
176
+ - Removed demand is **not reallocated** to other services.
177
+ """
86
178
  buildings = buildings.copy()
87
179
  services = services.copy()
88
180
  links = links.copy()
@@ -1,59 +1,59 @@
1
- class CapacityKeyError(KeyError):
2
- def __init__(self, *args):
3
- if args:
4
- self.message = args[0]
5
- else:
6
- self.message = None
7
-
8
- def __str__(self):
9
- if self.message:
10
- return f"CapacityKeyError, {self.message} "
11
-
12
- return (
13
- "Column 'capacity' was not found in provided 'services' GeoDataFrame. This attribute "
14
- "corresponds to the total capacity for each service."
15
- )
16
-
17
-
18
- class CapacityValueError(ValueError):
19
- def __init__(self, *args):
20
- if args:
21
- self.message = args[0]
22
- else:
23
- self.message = None
24
-
25
- def __str__(self):
26
- if self.message:
27
- return f"CapacityValueError, {self.message} "
28
-
29
- return "Column 'capacity' in 'services' GeoDataFrame has no valid value."
30
-
31
-
32
- class DemandKeyError(KeyError):
33
- def __init__(self, *args):
34
- if args:
35
- self.message = args[0]
36
- else:
37
- self.message = None
38
-
39
- def __str__(self):
40
- if self.message:
41
- return f"DemandKeyError, {self.message} "
42
-
43
- return (
44
- "The column 'demand' was not found in the provided 'demanded_buildings' GeoDataFrame. "
45
- "This attribute corresponds to the number of demands for the selected service in each building."
46
- )
47
-
48
-
49
- class DemandValueError(ValueError):
50
- def __init__(self, *args):
51
- if args:
52
- self.message = args[0]
53
- else:
54
- self.message = None
55
-
56
- def __str__(self):
57
- if self.message:
58
- return f"DemandValueError, {self.message} "
59
- return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."
1
+ class CapacityKeyError(KeyError):
2
+ def __init__(self, *args):
3
+ if args:
4
+ self.message = args[0]
5
+ else:
6
+ self.message = None
7
+
8
+ def __str__(self):
9
+ if self.message:
10
+ return f"CapacityKeyError, {self.message} "
11
+
12
+ return (
13
+ "Column 'capacity' was not found in provided 'services' GeoDataFrame. This attribute "
14
+ "corresponds to the total capacity for each service."
15
+ )
16
+
17
+
18
+ class CapacityValueError(ValueError):
19
+ def __init__(self, *args):
20
+ if args:
21
+ self.message = args[0]
22
+ else:
23
+ self.message = None
24
+
25
+ def __str__(self):
26
+ if self.message:
27
+ return f"CapacityValueError, {self.message} "
28
+
29
+ return "Column 'capacity' in 'services' GeoDataFrame has no valid value."
30
+
31
+
32
+ class DemandKeyError(KeyError):
33
+ def __init__(self, *args):
34
+ if args:
35
+ self.message = args[0]
36
+ else:
37
+ self.message = None
38
+
39
+ def __str__(self):
40
+ if self.message:
41
+ return f"DemandKeyError, {self.message} "
42
+
43
+ return (
44
+ "The column 'demand' was not found in the provided 'demanded_buildings' GeoDataFrame. "
45
+ "This attribute corresponds to the number of demands for the selected service in each building."
46
+ )
47
+
48
+
49
+ class DemandValueError(ValueError):
50
+ def __init__(self, *args):
51
+ if args:
52
+ self.message = args[0]
53
+ else:
54
+ self.message = None
55
+
56
+ def __str__(self):
57
+ if self.message:
58
+ return f"DemandValueError, {self.message} "
59
+ return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."