Skip to content

Commit

Permalink
Merge pull request #111 from Deltares/feature/108-apply-a-measure-sel…
Browse files Browse the repository at this point in the history
…ection-strategy-to-all-locations

Feature/108 apply a measure selection strategy to all locations
  • Loading branch information
Carsopre authored Oct 25, 2023
2 parents 8d78642 + 65d1215 commit c3b46f2
Show file tree
Hide file tree
Showing 21 changed files with 335 additions and 46 deletions.
72 changes: 42 additions & 30 deletions koswat/cost_report/io/csv/summary_matrix_csv_fom_builder.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import logging
import math
from collections import defaultdict
from typing import Any
from typing import Any, Type

from koswat.core.io.csv.koswat_csv_fom import KoswatCsvFom
from koswat.core.protocols.builder_protocol import BuilderProtocol
from koswat.cost_report.profile.volume_cost_parameters import VolumeCostParameter
from koswat.cost_report.summary.koswat_summary import KoswatSummary
from koswat.dike.surroundings.point.point_surroundings import PointSurroundings
from koswat.dike_reinforcements.reinforcement_profile.outside_slope.cofferdam_reinforcement_profile import (
CofferdamReinforcementProfile,
)
from koswat.dike_reinforcements.reinforcement_profile.reinforcement_profile_protocol import (
ReinforcementProfileProtocol,
)
from koswat.dike_reinforcements.reinforcement_profile.standard.piping_wall_reinforcement_profile import (
PipingWallReinforcementProfile,
)
from koswat.dike_reinforcements.reinforcement_profile.standard.soil_reinforcement_profile import (
SoilReinforcementProfile,
)
from koswat.dike_reinforcements.reinforcement_profile.standard.stability_wall_reinforcement_profile import (
StabilityWallReinforcementProfile,
)
from koswat.strategies.strategy_location_matrix import StrategyLocationReinforcements


class SummaryMatrixCsvFomBuilder(BuilderProtocol):
Expand All @@ -19,22 +35,30 @@ class SummaryMatrixCsvFomBuilder(BuilderProtocol):
def __init__(self) -> None:
self.koswat_summary = None

def get_summary_reinforcement_type_column_order(
self,
) -> list[Type[ReinforcementProfileProtocol]]:
return [
type(_report.profile_cost_report.reinforced_profile)
for _report in self.koswat_summary.locations_profile_report_list
]

def build(self) -> KoswatCsvFom:
_csv_fom = KoswatCsvFom()

_profile_type_key = "Profile type"
_cost_per_km_key = "Cost per km (€)"
_locations_key = "locations"

_dict_of_entries = defaultdict(list)
for _loc_prof_report in self.koswat_summary.locations_profile_report_list:
_dict_of_entries[_profile_type_key].append(_loc_prof_report.profile_type)
_dict_of_entries[_profile_type_key].append(
_loc_prof_report.profile_type_name
)
_dict_of_entries[_cost_per_km_key].append(_loc_prof_report.cost_per_km)
self._get_volume_cost_parameters(
_loc_prof_report.profile_cost_report.volume_cost_parameters.__dict__,
_dict_of_entries,
)
_dict_of_entries[_locations_key].append(_loc_prof_report.locations)

if not _dict_of_entries:
logging.error("No entries generated for the CSV Matrix.")
Expand All @@ -48,13 +72,9 @@ def dict_to_csv_row(key, placeholders: int) -> list[str]:
return row

_location_rows = self._get_locations_matrix(
_dict_of_entries[_locations_key], self.koswat_summary.available_locations
)
_required_placeholders = (
len(_location_rows[0])
- len(self.koswat_summary.locations_profile_report_list)
- 1
self.koswat_summary.reinforcement_per_locations
)
_required_placeholders = 2 # Fix value.
_headers = dict_to_csv_row(_profile_type_key, _required_placeholders)
_cost_rows = [
dict_to_csv_row(_parameter_key, _required_placeholders)
Expand All @@ -69,18 +89,8 @@ def dict_to_csv_row(key, placeholders: int) -> list[str]:

def _get_locations_matrix(
self,
suitable_locations: list[list[PointSurroundings]],
available_locations: list[PointSurroundings],
reinforcement_per_locations: list[StrategyLocationReinforcements],
) -> list[list[Any]]:
def _find_location(
location: PointSurroundings, locations: list[PointSurroundings]
) -> PointSurroundings:
return next(
_loc
for _loc in locations
if _loc.traject_order == location.traject_order
)

def _location_as_row(
matrix_item: tuple[PointSurroundings, list[int]]
) -> list[Any]:
Expand All @@ -89,20 +99,22 @@ def _location_as_row(
_location_as_row.extend(_m_values)
return _location_as_row

if not any(available_locations):
if not any(reinforcement_per_locations):
logging.warning("No locations specified for the report.")
return [[]]

# Initiate locations matrix.
_matrix = defaultdict(list)
for _available_loc in available_locations:
_matrix[_available_loc] = [0] * len(suitable_locations)

# Set the suitable locations.
for idx, _loc_list in enumerate(suitable_locations):
for _loc in _loc_list:
_available_loc = _find_location(_loc, available_locations)
_matrix[_available_loc][idx] = 1

for _reinforcement_per_location in reinforcement_per_locations:
_suitable_locations = [
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
]

return list(
map(
_location_as_row,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def total_volume(self) -> float:
return self.profile_cost_report.total_volume * len(self.locations)

@property
def profile_type(self) -> str:
def profile_type_name(self) -> str:
if (
not self.profile_cost_report
or not self.profile_cost_report.reinforced_profile
Expand Down
15 changes: 6 additions & 9 deletions koswat/cost_report/summary/koswat_summary.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from __future__ import annotations

from typing import List
from dataclasses import field, dataclass

from koswat.cost_report.multi_location_profile import MultiLocationProfileCostReport
from koswat.dike.surroundings.point.point_surroundings import PointSurroundings
from koswat.strategies.strategy_location_matrix import StrategyLocationReinforcements


@dataclass
class KoswatSummary:
locations_profile_report_list: List[MultiLocationProfileCostReport]
available_locations: List[PointSurroundings]

def __init__(self) -> None:
self.locations_profile_report_list = []
self.available_locations = []
locations_profile_report_list: list[MultiLocationProfileCostReport] = field(default_factory=lambda: [])
available_locations: list[PointSurroundings] = field(default_factory=lambda: [])
reinforcement_per_locations: list[StrategyLocationReinforcements] = field(default_factory=lambda: [])
40 changes: 40 additions & 0 deletions koswat/cost_report/summary/koswat_summary_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@
from koswat.cost_report.multi_location_profile import (
MultiLocationProfileCostReportBuilder,
)
from koswat.cost_report.multi_location_profile.multi_location_profile_cost_report import (
MultiLocationProfileCostReport,
)
from koswat.cost_report.summary.koswat_summary import KoswatSummary
from koswat.cost_report.summary.koswat_summary_location_matrix_builder import (
KoswatSummaryLocationMatrixBuilder,
)
from koswat.dike.profile.koswat_input_profile_base import KoswatInputProfileBase
from koswat.dike.surroundings.point.point_surroundings import PointSurroundings
from koswat.dike_reinforcements import ReinforcementProfileBuilderFactory
from koswat.dike_reinforcements.reinforcement_profile.reinforcement_profile import (
ReinforcementProfile,
)
from koswat.dike_reinforcements.reinforcement_profile.reinforcement_profile_protocol import (
ReinforcementProfileProtocol,
)
from koswat.strategies.order_strategy.order_stategy import OrderStrategy
from koswat.strategies.strategy_input import StrategyInput


class KoswatSummaryBuilder(BuilderProtocol):
Expand Down Expand Up @@ -78,6 +90,28 @@ def _get_multi_location_profile_cost_builder(
_builder.koswat_costs = self.run_scenario_settings.costs
return _builder

def _get_final_reinforcement_per_location(
self,
locations_profile_report_list: list[MultiLocationProfileCostReport],
available_locations: list[PointSurroundings],
) -> dict[PointSurroundings, ReinforcementProfile]:
_matrix = KoswatSummaryLocationMatrixBuilder(
locations_profile_report_list, available_locations
).build()

# TODO: `structure_buffer` and `min_space_between_structures` should come
# from the ini files.

_strategy_input = StrategyInput(
locations_matrix=_matrix,
structure_buffer=10,
min_space_between_structures=50,
)

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

def build(self) -> KoswatSummary:
_summary = KoswatSummary()
_summary.available_locations = self.run_scenario_settings.surroundings.locations
Expand All @@ -92,4 +126,10 @@ def build(self) -> KoswatSummary:
for _calc_profile in self._get_calculated_profile_list():
_mlpc_builder.reinforced_profile = _calc_profile
_summary.locations_profile_report_list.append(_mlpc_builder.build())

_summary.reinforcement_per_locations = (
self._get_final_reinforcement_per_location(
_summary.locations_profile_report_list, _summary.available_locations
)
)
return _summary
24 changes: 24 additions & 0 deletions koswat/cost_report/summary/koswat_summary_location_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Type
from koswat.dike.surroundings.point.point_surroundings import PointSurroundings
from koswat.dike_reinforcements.reinforcement_profile.reinforcement_profile import (
ReinforcementProfile,
)


class KoswatSummaryLocationMatrix:
locations_matrix: dict[PointSurroundings, list[Type[ReinforcementProfile]]]

def __init__(self) -> None:
self.locations_matrix = []

def sort_by_traject_order(self):
self.locations_matrix = dict(
sorted(self.locations_matrix.items(), key=lambda x: x[0].traject_order)
)

@classmethod
def from_point_surroundings_list(cls, point_surroundings: list[PointSurroundings]):
_this_cls = cls()
_this_cls.locations_matrix = dict((_ps, []) for _ps in point_surroundings)
return _this_cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import logging
from koswat.core.protocols.builder_protocol import BuilderProtocol
from koswat.cost_report.multi_location_profile.multi_location_profile_cost_report import (
MultiLocationProfileCostReport,
)
from koswat.cost_report.summary.koswat_summary_location_matrix import (
KoswatSummaryLocationMatrix,
)
from koswat.dike.surroundings.point.point_surroundings import PointSurroundings
from koswat.dike_reinforcements.reinforcement_profile.reinforcement_profile import (
ReinforcementProfile,
)


class KoswatSummaryLocationMatrixBuilder(BuilderProtocol):
"""
NOTE: Although this 'problem' could be easily solved with `pandas`
or `numpy`, I prefer not to include external (heavy) dependencies unless
strictily necessary.
If / when performance would become a problem, then this builder could (perhaps)
benefit from using the aforementioned libraries.
"""

locations_profile_report_list: list[MultiLocationProfileCostReport]
available_locations: list[PointSurroundings]

def __init__(
self,
locations_profile: list[MultiLocationProfileCostReport],
available_locations: list[PointSurroundings],
) -> None:
self.locations_profile_report_list = locations_profile
self.available_locations = available_locations

def _get_multi_location_profile_to_dict_matrix(
self, locations_profile: MultiLocationProfileCostReport
) -> dict[PointSurroundings, ReinforcementProfile]:
return dict(
(_location, type(locations_profile.profile_cost_report.reinforced_profile))
for _location in locations_profile.locations
)

def _get_list_summary_matrix_for_locations_with_reinforcements(
self,
) -> list[dict[PointSurroundings, ReinforcementProfile]]:
return list(
map(
self._get_multi_location_profile_to_dict_matrix,
self.locations_profile_report_list,
)
)

def build(self) -> KoswatSummaryLocationMatrix:
# 1. First we get all the possible reinforcements per point.

logging.info("Initalizing locations-reinforcements matrix.")
_reinforce_matrix_dict_list = (
self._get_list_summary_matrix_for_locations_with_reinforcements()
)

# 2. Then we initialize the matrix with all available locations,
# but no reinforcements.

_summary_matrix = KoswatSummaryLocationMatrix.from_point_surroundings_list(
self.available_locations
)

# 3. Last, we merge the reinforcements dictionary into the matrix.
for _location in _summary_matrix.locations_matrix.keys():
for _reinforce_matrix_dict in _reinforce_matrix_dict_list:
if _location in _reinforce_matrix_dict:
_summary_matrix.locations_matrix[_location].append(
_reinforce_matrix_dict[_location]
)

_summary_matrix.sort_by_traject_order()

logging.info("Finalized locations-reinforcements matrix.")

return _summary_matrix
26 changes: 26 additions & 0 deletions koswat/dike/surroundings/point/point_surroundings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ def __init__(self) -> None:
self.traject_order = -1
self.distance_to_surroundings = []

def __hash__(self) -> int:
"""
Overriding of the "magic" hash operator required
so that `PointSurroundings` can be used as a key in a python dict.
"""
return hash(
(
self.section,
self.traject_order,
self.location,
)
)

def __eq__(self, __value: object) -> bool:
"""
Overriding of the "magic" equality operator required
so that `PointSurroundings` can be used as a key in a python dict.
"""
if not isinstance(__value, type(self)):
return False
return (self.location, self.section, self.traject_order) == (
__value.location,
__value.section,
__value.traject_order,
)

@property
def closest_surrounding(self) -> float:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@


class CofferdamReinforcementProfile(OutsideSlopeReinforcementProfile):
output_name: str = "Kistdam"
input_data: CofferDamInputProfile
layers_wrapper: ReinforcementLayersWrapper
old_profile: KoswatProfileProtocol
new_ground_level_surface: float

def __str__(self) -> str:
return "Kistdam"
return self.output_name
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@


class ReinforcementProfile(ReinforcementProfileProtocol, KoswatProfileBase):
output_name: str
input_data: ReinforcementInputProfileProtocol
layers_wrapper: ReinforcementLayersWrapper
old_profile: KoswatProfileProtocol
Expand Down
Loading

0 comments on commit c3b46f2

Please sign in to comment.