Skip to content

Commit 982a9ee

Browse files
Merge pull request #26 from alliander-opensource/feature/pre-release-improvements
Feature/pre release improvements
2 parents 6591873 + 7cf8b4f commit 982a9ee

File tree

14 files changed

+529
-136
lines changed

14 files changed

+529
-136
lines changed

CONTRIBUTING.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ We'd love to accept your patches and contributions to this project. There are ju
1313
Contribution does not necessarily mean committing code to the repository.
1414
We recognize different levels of contributions as shown below in increasing order of dedication.
1515

16-
1. Use and test the project. Give feedback on the user experience or suggest new features.
17-
2. Report bugs or security vulnerabilities.
18-
3. Fix bugs.
19-
4. Improve the project by developing new features.
16+
1. Use and test the project. Give feedback on the user experience.
17+
2. Suggest or prioritize new features to be developed, based on substantiated input from a DSO.
18+
3. Report bugs or security vulnerabilities.
19+
4. Fix bugs.
20+
5. Improve the project by developing new features.
2021

2122
## Filing bugs, security vulnerabilities or feature requests
2223

@@ -79,12 +80,12 @@ Contributions should be submitted as GitHub pull requests. See [Creating a pull
7980
Follow this process for a code change and pull request:
8081

8182
1. Fork the repository.
82-
1. Make your change in a feature/description_of_your_change branch.
83-
1. Run the tests.
84-
1. Run the [pre-commit hooks](https://pre-commit.com/) to check for code style and other issues.
85-
1. Submit a pull request.
86-
1. Pull requests will be reviewed by one of the maintainers who may discuss, offer constructive feedback, request changes, or approve the work.
87-
1. Upon receiving the sign-off from one of the maintainers will merge it for you.
83+
2. Make your change in a feature/description_of_your_change branch.
84+
3. Run the tests.
85+
4. Run the [pre-commit hooks](https://pre-commit.com/) to check for code style and other issues.
86+
5. Submit a pull request.
87+
6. Pull requests will be reviewed by one of the maintainers who may discuss, offer constructive feedback, request changes, or approve the work.
88+
7. Upon receiving the sign-off from one of the maintainers will merge it for you.
8889

8990
## Attribution
9091

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0
88

99
This repository shares research on software for automatic placement of electricity cables using a combination of geo-information and graph theory.
1010

11-
The utility network needs to be expanded due to the energy transition. Finding a location for new infrastructure is no easy feat considering the amount of involved design criteria.
11+
The utility network needs to be expanded due to the energy transition. Finding a location for new infrastructure is no easy feat considering the amount of involved design criteria.
1212
This research includes the creation of a software package for automatic placement of utility network using a combination of geo-information and graph theory.
1313

1414
This research is being carried out at Alliander, a Dutch DSO, as part of [Jelmar Versleijen](https://research.wur.nl/en/persons/jelmar-versleijen)'s PhD with [Wagening University](https://www.wur.nl/en.htm). [Read more about research at Alliander](https://www.alliander.com/nl/alliander-en-open-research/).
@@ -57,12 +57,14 @@ Run tests using pytest:
5757
```bash
5858
poetry run python -m pytest tests/
5959
```
60+
For quick experimentation, weights of existing criteria can be changed. This can be done by editing the `mcda_presets.py` file.
6061

61-
Expanding criteria included in the `mcda_presets.py` can be done by:
62+
Adding new criteria can be done by:
6263

63-
1. Adding a new class to the `criteria` folder.
64-
2. Implementing the `get_suitability` method.
65-
3. Adding the new class to the `mcda_presets.py` file. Set the group and weight of the new class.
64+
1. Add vector data to the case study geopackage in the `data/examples` folder.
65+
2. Adding a new entry to `mcda_presets.py` according to the pydantic model `RasterPresetCriteria`.
66+
3. Adding a new class to the `utility_route_planner/mcda/vector_preprocessing` folder.
67+
4. Implementing the `specific_preprocess` method. This commonly consists out reclassifying a dominant attribute.
6668

6769
# Support
6870

@@ -96,4 +98,4 @@ The software is largely dependent on data. Data is incorporated in the example f
9698

9799
Citing
98100
-------
99-
t.b.d.
101+
t.b.d.

main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ def run_mcda_lcpa(
5050

5151
logger.info(f"Route CPU time: {(time.process_time_ns() - start_cpu_time) / 1e9:.2f} seconds.")
5252
route_evaluation_metrics = RouteEvaluationMetrics(
53-
lcpa_engine.lcpa_result, path_suitability_raster, human_designed_route, project_area_geometry
53+
lcpa_engine.lcpa_result,
54+
path_suitability_raster,
55+
human_designed_route,
56+
project_area_geometry,
57+
mcda_engine.processed_vector_metrics,
5458
)
5559
route_evaluation_metrics.get_route_evaluation_metrics()
5660

tests/integration/mcda/mcda_vector_base_test.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from unittest.mock import Mock
5+
from unittest.mock import Mock, MagicMock
66

77
import geopandas as gpd
88
import numpy as np
9+
import pandas as pd
910

1011
import pytest
12+
import shapely
1113

1214
from settings import Config
1315
from utility_route_planner.models.mcda.exceptions import InvalidSuitabilityValue, UnassignedValueFoundDuringReclassify
@@ -111,3 +113,100 @@ def test_missing_values(input_which_should_raise):
111113
wrong_input = input_which_should_raise
112114
with pytest.raises(UnassignedValueFoundDuringReclassify):
113115
validate_values_to_reclassify(values_to_reclassify, wrong_input)
116+
117+
118+
class DummyPreprocessor(VectorPreprocessorBase):
119+
criterion = "dummy_criterion"
120+
121+
def specific_preprocess(self, prepared_data, criterion):
122+
pass
123+
124+
125+
class TestGetMetrics:
126+
@pytest.fixture
127+
def get_dummy_criterion(self):
128+
return MagicMock(spec=RasterPresetCriteria)
129+
130+
def test_get_statistics_invalid_column(self, get_dummy_criterion):
131+
dummy_criterion = get_dummy_criterion
132+
dummy_criterion.columns_to_reclassify = ["missing_col"]
133+
dummy_criterion.layer_names = ["layer1"]
134+
135+
gdf = gpd.GeoDataFrame({"geometry": [None], "col1": ["A"], "suitability_value": [1]})
136+
preprocessor = DummyPreprocessor()
137+
with pytest.raises(ValueError):
138+
preprocessor.get_statistics(get_dummy_criterion, gdf)
139+
140+
def test_get_statistics_no_reclassify(self, get_dummy_criterion):
141+
mock_criterion = get_dummy_criterion
142+
mock_criterion.columns_to_reclassify = []
143+
mock_criterion.layer_names = ["layer1"]
144+
gdf = gpd.GeoDataFrame({"geometry": [], "suitability_value": []})
145+
146+
preprocessor = DummyPreprocessor()
147+
result = preprocessor.get_statistics(mock_criterion, gdf)
148+
assert "criterion" in result.columns
149+
assert "weight_key" in result.columns
150+
assert "area_m2" in result.columns
151+
assert "suitability_value" in result.columns
152+
153+
def test_get_statistics_one_reclassify_column(self, get_dummy_criterion):
154+
dummy_criterion = get_dummy_criterion
155+
dummy_criterion.columns_to_reclassify = ["col1"]
156+
dummy_criterion.layer_names = ["layer1"]
157+
gdf = gpd.GeoDataFrame(
158+
{
159+
"geometry": [shapely.Point(0, 0).buffer(10), shapely.Point(0, 0).buffer(10)],
160+
"col1": ["A", "B"],
161+
"suitability_value": [1, 2],
162+
}
163+
)
164+
preprocessor = DummyPreprocessor()
165+
result = preprocessor.get_statistics(dummy_criterion, gdf)
166+
expected_df = pd.DataFrame(
167+
{
168+
"criterion": ["dummy_criterion", "dummy_criterion"],
169+
"weight_key": ["A", "B"],
170+
"suitability_value": [1, 2],
171+
"area_m2": [313.6548490545941, 313.6548490545941],
172+
}
173+
)
174+
pd.testing.assert_frame_equal(result.reset_index(drop=True), expected_df, check_dtype=False)
175+
176+
def test_get_statistics_two_reclassify_columns(self, get_dummy_criterion):
177+
dummy_criterion = get_dummy_criterion
178+
get_dummy_criterion.columns_to_reclassify = ["col1", "col2"]
179+
get_dummy_criterion.layer_names = ["layer1"]
180+
example_polygon1 = shapely.Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
181+
example_polygon2 = shapely.Polygon([(5, 5), (5, 15), (15, 15), (15, 5), (5, 5)])
182+
gdf = gpd.GeoDataFrame(
183+
{
184+
"geometry": [example_polygon1, example_polygon1, example_polygon2],
185+
"col1": ["A", "B", "B"],
186+
"col2": ["X", "Y", "Y"],
187+
"suitability_value": [1, 2, 2],
188+
}
189+
)
190+
preprocessor = DummyPreprocessor()
191+
result = preprocessor.get_statistics(dummy_criterion, gdf)
192+
193+
expected_df = pd.DataFrame(
194+
{
195+
"criterion": ["dummy_criterion", "dummy_criterion"],
196+
"weight_key": ["A: X", "B: Y"],
197+
"suitability_value": [1, 2],
198+
"area_m2": [100, 175.0],
199+
}
200+
)
201+
pd.testing.assert_frame_equal(result.reset_index(drop=True), expected_df, check_dtype=False)
202+
203+
def test_get_statistics_three_reclassify_columns_raises(self, get_dummy_criterion):
204+
dummy_criterion = get_dummy_criterion
205+
dummy_criterion.columns_to_reclassify = ["col1", "col2", "col3"]
206+
dummy_criterion.layer_names = ["layer1"]
207+
gdf = gpd.GeoDataFrame(
208+
{"geometry": [None], "col1": ["A"], "col2": ["B"], "col3": ["C"], "suitability_value": [1]}
209+
)
210+
preprocessor = DummyPreprocessor()
211+
with pytest.raises(ValueError):
212+
preprocessor.get_statistics(dummy_criterion, gdf)

tests/integration/mcda/mcda_vector_test.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,118 @@ def test_small_above_ground_obstacles(self):
935935
assert "niet-bgt" not in reclassified_gdf["bgt-type"]
936936
assert "waardeOnbekend" not in reclassified_gdf["plus-type"]
937937

938+
reclassified_gdfscheiding_only = SmallAboveGroundObstacles._set_suitability_values(
939+
[bgt_bak_bord_kast_paal_put_straatmeubilair], weight_values
940+
)
941+
pd.testing.assert_series_equal(
942+
reclassified_gdfscheiding_only["suitability_value"],
943+
pd.Series(
944+
[
945+
8,
946+
9,
947+
10,
948+
11,
949+
12,
950+
13,
951+
14,
952+
15,
953+
16,
954+
17,
955+
18,
956+
19,
957+
20,
958+
21,
959+
22,
960+
23,
961+
24,
962+
25,
963+
26,
964+
27,
965+
28,
966+
29,
967+
30,
968+
31,
969+
32,
970+
33,
971+
34,
972+
35,
973+
36,
974+
37,
975+
38,
976+
39,
977+
40,
978+
41,
979+
42,
980+
43,
981+
44,
982+
44,
983+
45,
984+
46,
985+
47,
986+
48,
987+
49,
988+
50,
989+
51,
990+
52,
991+
53,
992+
54,
993+
55,
994+
56,
995+
57,
996+
58,
997+
59,
998+
59,
999+
60,
1000+
61,
1001+
62,
1002+
63,
1003+
64,
1004+
65,
1005+
66,
1006+
67,
1007+
68,
1008+
69,
1009+
70,
1010+
71,
1011+
72,
1012+
72,
1013+
73,
1014+
74,
1015+
75,
1016+
76,
1017+
77,
1018+
78,
1019+
79,
1020+
80,
1021+
81,
1022+
82,
1023+
83,
1024+
84,
1025+
85,
1026+
]
1027+
),
1028+
check_names=False,
1029+
check_exact=True,
1030+
check_dtype=False,
1031+
check_index=False,
1032+
)
1033+
1034+
assert "waardeOnbekend" not in reclassified_gdfscheiding_only["plus-type"]
1035+
1036+
reclassified_gdf_others_only = SmallAboveGroundObstacles._set_suitability_values(
1037+
[bgt_scheiding_1, bgt_scheiding_2], weight_values
1038+
)
1039+
pd.testing.assert_series_equal(
1040+
reclassified_gdf_others_only["suitability_value"],
1041+
pd.Series([1, 2, 3, 4, 5, 7]),
1042+
check_names=False,
1043+
check_exact=True,
1044+
check_dtype=False,
1045+
check_index=False,
1046+
)
1047+
1048+
assert "niet-bgt" not in reclassified_gdf_others_only["bgt-type"]
1049+
9381050
def test_vegetation_object(self):
9391051
weight_values = {
9401052
# plus_type
@@ -1190,3 +1302,17 @@ def test_existing_substations(self):
11901302
check_index=False,
11911303
)
11921304
assert reclassified_gdf.geom_type.unique().tolist() == ["Polygon"]
1305+
1306+
1307+
def test_get_vector_metrics_benchmark():
1308+
mcda_engine = McdaCostSurfaceEngine(
1309+
Config.RASTER_PRESET_NAME_BENCHMARK, Config.PYTEST_PATH_GEOPACKAGE_MCDA, shapely.Point(1, 1).buffer(100)
1310+
)
1311+
df1 = pd.DataFrame({"area_m2": [1.2, 2.5, 3.7]})
1312+
df2 = pd.DataFrame({"area_m2": [4.1, 5.9]})
1313+
present_weight_dfs = [df1, df2]
1314+
n_total_weights = mcda_engine.get_vector_metrics(present_weight_dfs)
1315+
1316+
assert n_total_weights == 195 # As per the benchmark preset configuration
1317+
df_expected = pd.DataFrame({"area_m2": [1, 2, 4, 4, 6]})
1318+
pd.testing.assert_frame_equal(df_expected.reset_index(drop=True), pd.DataFrame({"area_m2": [1, 2, 4, 4, 6]}))

tests/integration/mcda_lcpa_main_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def test_mcda_lcpa_chain_pytest_files(self, utility_route_sketch):
4848
write_results_to_geopackage(Config.PATH_GEOPACKAGE_LCPA_OUTPUT, lcpa_engine.lcpa_result, "utility_route_result")
4949

5050

51+
@pytest.mark.skip(reason="Benchmark cases are skipped in normal test runs to save time.")
5152
@pytest.mark.parametrize(
5253
"path_geopackage, layer_name_project_area, layer_name_utility_route_human_designed",
5354
[

utility_route_planner/models/mcda/load_mcda_preset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ class RasterPresetCriteria(pydantic.BaseModel):
3030
layer_names: list = pydantic.Field(description="Layer names in the geopackage which will be handled.")
3131
preprocessing_function: VectorPreprocessorBase
3232
group: str = pydantic.Field(description="Determines how the criteria is handled.")
33+
columns_to_reclassify: typing.Optional[list] = pydantic.Field(
34+
default=[], description="List of dominant attributes to reclassify."
35+
)
3336
weight_values: dict = pydantic.Field(..., description="Contains values for defining how important the layer is.")
3437
geometry_values: typing.Optional[dict] = pydantic.Field(
35-
default=None,
38+
default={},
3639
description="Contains values for optional computational geometry steps, e.g., buffer.",
3740
)
3841

0 commit comments

Comments
 (0)