Skip to content

Commit

Permalink
feat: generate strategies intermediate results (#208)
Browse files Browse the repository at this point in the history
* chore: Modified definition of `StrategyLocationReinforcement` so it contains the different assigned reinforcements

* chore: Small correction to directly display the previous selection if suitable

* chore: Corrected summary builder so the strategy to run can be modified dinamically

* chore: Small correction to output history of selection measure during strategies

* test: Fixed failing tests after merge from `master`

* chore: Small rework to track only three items in the selection history

* chore: Renaming of outdated references

* chore: Corrected code `get_strategy_reinforcemetns` so that the available measures are already ordered based on the current strategy

* chore: Added some missing docstrings

* chore: Corrections in the selection and output of history selected measures

* chore: Correction to output selected measures

* chore: Fixed output of steps

* test: fixed failing tests

* chore: Cleaning selected measures to avoid in-between steps

* chore: Added to-do

* test: Sync test data

* feat: Added new dataclass and modified `StrategyLocationReinforcement` so we can better keep track -per-step- of the history and output it appropriately

* chore: Added missing subproject

* docs: Extended docstrings

* test: added missing tests

* test: Updated failing tests

* chore: Extended shp export logic to include a new layer to represent the `step` from initial to new

* test: fixed failing tests

* chore: Small corrections to validate generated geodataframes

* test: Updated reference data

* chore: fixed failing test

* chore: Extracted class for better maintainability

* test: Added missing coverage test

* docs: Documentation cleanup

* chore: Removed todo notes

* chore: Created new property to represent both available and filtered measures'

* chore: Processed review remarks; Moved dataclass into typedict for performance reasons

* test: Fixed failing test
  • Loading branch information
Carsopre authored Nov 1, 2024
1 parent bf93f81 commit 1bb25e6
Show file tree
Hide file tree
Showing 79 changed files with 724 additions and 183 deletions.
32 changes: 29 additions & 3 deletions docs/reference/koswat_cost_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ After running a Koswat analysis, several files and directories will be generated
- Dike section: The selected dike section being analyzed.
- Generated files: A combination of images and a 'csv' matrix result.
- Images: Visual description of each of the possible reinforcements being applied.
- summary_costs.csv: A csv file containing all the costs information of the summary.
- `summary_costs.csv`: A csv file containing all the costs information of the summary.
- Represents the Summary, Profile and Layer report.
- summary_locations.csv: A csv file containing per-location a breakdown of available reinforcements and selected reinforcement ( see [strategies](koswat_strategies.md)).
- `summary_locations.csv` (phased out): A csv file containing per-location a breakdown of available reinforcements and selected reinforcement ( see [strategies](koswat_strategies.md)).
- Represents the Location report.
- summary_infrastructure_costs.csv: A csv file contaning all the infrastructure costs at each location for each of the supported reinforcement profile types.
- `/summary_locations` directory: contains the following `.shp` files (and their related binaries):
-`summary_locations_measures`: The same data as present in `summary_locations.csv` is used to shape the geometry of a dike's traject. We keep as well the type of chosen reinforcement.
- `summary_locations_new`: an overlay of `summary_locations_measures` with the geometry buffered in relation to the new selected reinforcement.
- `summary_locations_old`: an overlay of `summary_locations_measures` with the geometry buffered in relation to the original selected reinforcement.
- `summary_locations_step`: an overlay of `summary_locations_measures` with the geometry buffered in relation to the [Ordered Strategy](koswat_strategies.md#order-based-default) step.
- `summary_infrastructure_costs.csv`: A csv file contaning all the infrastructure costs at each location for each of the supported reinforcement profile types.
- Represents the [Infrastructure Report](#infrastructure-report).

Example using a summarized view of the output tree directory when running the acceptance test `test_main.test_given_valid_input_succeeds`:
Expand All @@ -108,6 +113,27 @@ acceptance
| | | | Stabiliteitswand.png
| | | | Verticale_piping_oplossing.png
| | | |
| | | +-- summary_locations
| | | | summary_locations_measures.cpg
| | | | summary_locations_measures.dbf
| | | | summary_locations_measures.prj
| | | | summary_locations_measures.shp
| | | | summary_locations_measures.shx
| | | | summary_locations_new.cpg
| | | | summary_locations_new.dbf
| | | | summary_locations_new.prj
| | | | summary_locations_new.shp
| | | | summary_locations_new.shx
| | | | summary_locations_old.cpg
| | | | summary_locations_old.dbf
| | | | summary_locations_old.prj
| | | | summary_locations_old.shp
| | | | summary_locations_step.shx
| | | | summary_locations_step.cpg
| | | | summary_locations_step.dbf
| | | | summary_locations_step.prj
| | | | summary_locations_step.shp
| | | | summary_locations_step.shx
| | | +-- Grondmaatregel_profiel
| | | | added_Grondmaatregel_profiel_CLAY.png
| | | | added_Grondmaatregel_profiel_GRASS.png
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ def _get_total_meters_per_selected_measure(
# ALWAYS to be of 1 meter.
_sorted_reinforcements = sorted(
self.koswat_summary.reinforcement_per_locations,
key=lambda x: x.selected_measure.output_name,
key=lambda x: x.current_selected_measure.output_name,
)
return dict(
(k, len(list(g)))
for k, g in groupby(
_sorted_reinforcements,
lambda x: x.selected_measure,
lambda x: x.current_selected_measure,
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from dataclasses import dataclass, field
from itertools import groupby
from typing import Type
from typing import Callable, Type

from geopandas import GeoDataFrame

from koswat.cost_report.io.summary.summary_locations.cluster_geodataframe_output_fom import (
ClusterGeoDataFrameOutputFom,
)
from koswat.cost_report.io.summary.summary_locations.cluster_shp_fom import (
ClusterShpFom,
)
Expand All @@ -24,12 +27,22 @@ class ClusterCollectionShpFom:
crs_projection: str = "EPSG:28992"

@classmethod
def from_summary(cls, koswat_summary: KoswatSummary) -> ClusterCollectionShpFom:
def from_summary(
cls,
koswat_summary: KoswatSummary,
cluster_criteria: Callable[
[StrategyLocationReinforcement], type[ReinforcementProfileProtocol]
] = lambda x: x.current_selected_measure,
) -> ClusterCollectionShpFom:
"""
Maps the `KoswatSummary` into a file object model that can be exported into `*.shp` files.
Args:
koswat_summary (KoswatSummary): The summary containing the information to export.
cluster_criteria (Callable[
[StrategyLocationReinforcement],
type[ReinforcementProfileProtocol]
]): (Lambda) Function criteria to group the locations by reinforcement type.
Returns:
ClusterCollectionShpFom: Dataclass instance that can be directly exported into `.shp`.
Expand All @@ -53,21 +66,20 @@ def to_cluster_shp_fom(
to_cluster_shp_fom,
groupby(
koswat_summary.reinforcement_per_locations,
key=lambda x: x.selected_measure,
key=cluster_criteria,
),
)
)
)

def generate_geodataframes(self) -> tuple[GeoDataFrame, GeoDataFrame, GeoDataFrame]:
def generate_geodataframes(self) -> ClusterGeoDataFrameOutputFom:
"""
Generates all geodataframes of the given clusters. The generated geodataframes
correspond to the, base geometry (without buffering), the old and new geometries
with their profile's width being buffered to the base geometry.
Returns:
tuple[GeoDataFrame, GeoDataFrame, GeoDataFrame]:
Tuple of geodataframes maping this `ClusterCollectionShpFom`.
ClusterGeoDataFrameOutputFom: Resulting geodataframes wrapper maping this `ClusterCollectionShpFom`.
"""

def to_gdf_entry(
Expand All @@ -94,9 +106,11 @@ def buffered_entry(buffered_value: float) -> dict:
def dict_list_to_gdf(dict_entries: list[dict]) -> GeoDataFrame:
return GeoDataFrame(data=dict_entries, crs=self.crs_projection)

return tuple(
map(
dict_list_to_gdf,
zip(*(to_gdf_entry(_cl) for _cl in self.clusters)),
return ClusterGeoDataFrameOutputFom(
tuple(
map(
dict_list_to_gdf,
zip(*(to_gdf_entry(_cl) for _cl in self.clusters)),
)
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from geopandas import GeoDataFrame

from koswat.core.io.file_object_model_protocol import FileObjectModelProtocol


class ClusterGeoDataFrameOutputFom(FileObjectModelProtocol):
"""
Data structure to wrap the output generated by `ClusterCollectionShpFom.generate_geodataframes`.
"""

base_layer: GeoDataFrame | None
initial_state: GeoDataFrame | None
new_state: GeoDataFrame | None

def __init__(self, gdf_tuples: tuple[GeoDataFrame]):
self.base_layer = None
self.initial_state = None
self.new_state = None

if not gdf_tuples:
return
self.base_layer = gdf_tuples[0]
self.initial_state = gdf_tuples[1]
self.new_state = gdf_tuples[2]

def is_valid(self) -> bool:
"""
Checks that all `GeoDataFrame` properties are set.
Returns:
bool: Validation result.
"""

def validate_gdf(gdf: GeoDataFrame | None) -> bool:
return gdf is not None

return all(
map(validate_gdf, [self.base_layer, self.initial_state, self.new_state])
)
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ def build(self) -> KoswatCsvFom:
"Y coord",
]
+ _ordered_profile_types
+ ["Selected reinforcement"]
+ [
"Initial selection",
"Ordered selection",
"Optimized selection",
]
),
entries=self._get_locations_matrix(
self.koswat_summary.reinforcement_per_locations
Expand Down Expand Up @@ -75,9 +79,12 @@ def _location_as_row(
int(_type in _reinforcement_per_location.available_measures)
for _type in self.get_summary_reinforcement_type_column_order()
]
_matrix[_reinforcement_per_location.location] = _suitable_locations + [
_reinforcement_per_location.selected_measure.output_name
]
_matrix[_reinforcement_per_location.location] = _suitable_locations + list(
map(
lambda x: x.output_name,
_reinforcement_per_location.get_selected_measure_steps(),
)
)

return list(
map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@ def export(self, koswat_summary: KoswatSummary, export_path: Path) -> None:
_old = export_path.joinpath("summary_locations_old.shp")
_new = export_path.joinpath("summary_locations_new.shp")

# Get clusters
# Get clusters comparing old to new.
_clusters = ClusterCollectionShpFom.from_summary(
koswat_summary
koswat_summary, lambda x: x.current_selected_measure
).generate_geodataframes()

if not _clusters:
if not _clusters.is_valid():
return

_clusters[0].to_file(_measures)
_clusters[1].to_file(_old)
_clusters[2].to_file(_new)
# Export clusters to file
_clusters.base_layer.to_file(_measures)
_clusters.initial_state.to_file(_old)
_clusters.new_state.to_file(_new)

# Get clusters for steps
_step_clusters = ClusterCollectionShpFom.from_summary(
koswat_summary, lambda x: x.get_selected_measure_steps()[1]
).generate_geodataframes()

if not _step_clusters.is_valid():
return

# Export clusters to file
_step_clusters.new_state.to_file(
export_path.joinpath("summary_locations_step.shp")
)
4 changes: 2 additions & 2 deletions koswat/cost_report/summary/koswat_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def get_infrastructure_costs(
# Get the infra cost tuples (without and with surtax) for each location.
_infra_costs_dict = defaultdict(list)
for _loc in self.reinforcement_per_locations:
_infra_costs_dict[_loc.selected_measure].append(
_loc.get_infrastructure_costs(_loc.selected_measure)
_infra_costs_dict[_loc.current_selected_measure].append(
_loc.get_infrastructure_costs(_loc.current_selected_measure)
)

# Sum the infra costs for each reinforcement type.
Expand Down
6 changes: 4 additions & 2 deletions koswat/cost_report/summary/koswat_summary_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import math
from dataclasses import dataclass
from dataclasses import dataclass, field

from koswat.configuration.settings.koswat_run_scenario_settings import (
KoswatRunScenarioSettings,
Expand Down Expand Up @@ -28,11 +28,13 @@
)
from koswat.strategies.order_strategy.order_strategy import OrderStrategy
from koswat.strategies.strategy_input import StrategyInput
from koswat.strategies.strategy_protocol import StrategyProtocol


@dataclass
class KoswatSummaryBuilder(BuilderProtocol):
run_scenario_settings: KoswatRunScenarioSettings = None
strategy_type: type[StrategyProtocol] = field(default_factory=lambda: OrderStrategy)

@staticmethod
def _get_corrected_koswat_scenario(
Expand Down Expand Up @@ -108,7 +110,7 @@ def _get_final_reinforcement_per_location(

# In theory this will become a factory (somewhere) where
# the adequate strategy will be chosen.
return OrderStrategy().apply_strategy(_strategy_input)
return self.strategy_type().apply_strategy(_strategy_input)

def build(self) -> KoswatSummary:
_summary = KoswatSummary()
Expand Down
3 changes: 2 additions & 1 deletion koswat/strategies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ This modules contains the logic to choose which measure will be applied for a gi

- `StrategyLocationReinforcement`, represents a mapped location to a selected measure.
- location (`PointSurroundings`), a point (meter) in the dike traject.
- selected_measure (`Type[ReinforcementProfileProtocol]`), which is the reinforcement that should be applied to the location.
- current_selected_measure (`Type[ReinforcementProfileProtocol]`), which is the reinforcement that should be applied to the location.
- available_measures (`list[Type[ReinforcementProfileProtocol]]`), which are the possible reinforcements that could be applied to the location.
- strategy_location_input (`StrategyLocationInput`), the related input with available reinforcements and their costs related to this location-measure mapping.

- `StrategyStepAssignment`, helps keep track of the different `StrategyLocationReinforcement.current_selected_measure` values done at each strategy.

## Available strategies

Expand Down
3 changes: 2 additions & 1 deletion koswat/strategies/infra_priority_strategy/infra_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from koswat.strategies.strategy_location_reinforcement import (
StrategyLocationReinforcement,
)
from koswat.strategies.strategy_step.strategy_step_enum import StrategyStepEnum


@dataclass
Expand Down Expand Up @@ -75,4 +76,4 @@ def set_cheapest_common_available_measure(
if _selection != self.reinforcement_type:
self.reinforcement_type = _selection
for _cd in self.cluster:
_cd.selected_measure = _selection
_cd.set_selected_measure(_selection, StrategyStepEnum.INFRASTRUCTURE)
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def get_cluster_option(
location_collection: list[StrategyLocationReinforcement],
) -> list[list[InfraCluster]]:
_icc = []
for _w_element in filter(len, location_collection):
for _w_element in filter(any, location_collection):
_ic = InfraCluster(
min_required_length=from_cluster.min_required_length,
reinforcement_type=from_cluster.reinforcement_type,
Expand Down Expand Up @@ -155,7 +155,7 @@ def _get_initial_state(
) -> Iterator[InfraCluster]:
for _grouped_by, _grouping in groupby(
order_strategy.apply_strategy(strategy_input),
key=lambda x: x.selected_measure,
key=lambda x: x.current_selected_measure,
):
_grouping_data = list(_grouping)
if not _grouping_data:
Expand Down
7 changes: 4 additions & 3 deletions koswat/strategies/order_strategy/order_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from koswat.strategies.strategy_location_reinforcement import (
StrategyLocationReinforcement,
)
from koswat.strategies.strategy_step.strategy_step_enum import StrategyStepEnum


@dataclass
Expand Down Expand Up @@ -62,7 +63,7 @@ def extend_cluster(self, other: OrderCluster):
"""
Extends the current cluster with the reinforcements
(`list[StrategyLocationReinforcement]`) from another cluster.
Modifies the `selected_measure` property of those measures being merged but it
Modifies the `current_selected_measure` property of those measures being merged but it
does not remove them from their source cluster.
Args:
Expand All @@ -75,9 +76,9 @@ def extend_cluster(self, other: OrderCluster):
logging.warning("Trying to extend cluster from an unrelated one.")

if any(self.location_reinforcements):
_new_profile_type = self.location_reinforcements[0].selected_measure
_new_profile_type = self.location_reinforcements[0].current_selected_measure
for _lr in other.location_reinforcements:
_lr.selected_measure = _new_profile_type
_lr.set_selected_measure(_new_profile_type, StrategyStepEnum.ORDERED)

if self.left_neighbor == other:
self.location_reinforcements = (
Expand Down
Loading

0 comments on commit 1bb25e6

Please sign in to comment.