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

Feature/108 apply a measure selection strategy to all locations #111

Merged
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
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
]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better!

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__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this match with the design principle of parameterless init?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not. I'll make it parameterless. in the follow-up issue #110 (as #109 has already an opened PR).

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