flood-adapt 1.0.4__py3-none-any.whl → 1.1.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.
Files changed (45) hide show
  1. flood_adapt/__init__.py +2 -3
  2. flood_adapt/adapter/fiat_adapter.py +8 -3
  3. flood_adapt/adapter/sfincs_adapter.py +2 -2
  4. flood_adapt/adapter/sfincs_offshore.py +1 -1
  5. flood_adapt/config/fiat.py +1 -1
  6. flood_adapt/config/gui.py +185 -8
  7. flood_adapt/database_builder/database_builder.py +155 -129
  8. flood_adapt/database_builder/metrics_utils.py +1834 -0
  9. flood_adapt/dbs_classes/database.py +23 -28
  10. flood_adapt/dbs_classes/dbs_benefit.py +0 -26
  11. flood_adapt/dbs_classes/dbs_event.py +2 -2
  12. flood_adapt/dbs_classes/dbs_measure.py +2 -2
  13. flood_adapt/dbs_classes/dbs_scenario.py +0 -24
  14. flood_adapt/dbs_classes/dbs_static.py +4 -4
  15. flood_adapt/dbs_classes/dbs_strategy.py +2 -4
  16. flood_adapt/dbs_classes/dbs_template.py +65 -25
  17. flood_adapt/flood_adapt.py +63 -12
  18. flood_adapt/misc/exceptions.py +43 -6
  19. {flood_adapt-1.0.4.dist-info → flood_adapt-1.1.0.dist-info}/METADATA +3 -3
  20. {flood_adapt-1.0.4.dist-info → flood_adapt-1.1.0.dist-info}/RECORD +24 -44
  21. flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +0 -90
  22. flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +0 -57
  23. flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +0 -121
  24. flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +0 -65
  25. flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +0 -126
  26. flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +0 -60
  27. flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +0 -121
  28. flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +0 -65
  29. flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +0 -45
  30. flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +0 -4
  31. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +0 -143
  32. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +0 -153
  33. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +0 -127
  34. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +0 -57
  35. flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +0 -4
  36. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +0 -191
  37. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +0 -153
  38. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +0 -178
  39. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +0 -57
  40. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +0 -9
  41. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +0 -65
  42. /flood_adapt/database_builder/templates/infographics/{OSM/styles.css → styles.css} +0 -0
  43. {flood_adapt-1.0.4.dist-info → flood_adapt-1.1.0.dist-info}/LICENSE +0 -0
  44. {flood_adapt-1.0.4.dist-info → flood_adapt-1.1.0.dist-info}/WHEEL +0 -0
  45. {flood_adapt-1.0.4.dist-info → flood_adapt-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  import gc
2
2
  import os
3
3
  import shutil
4
- import time
5
4
  from pathlib import Path
6
5
  from typing import Any, Literal, Optional, Union
7
6
 
@@ -11,6 +10,8 @@ import pandas as pd
11
10
  import xarray as xr
12
11
  from geopandas import GeoDataFrame
13
12
 
13
+ from flood_adapt.adapter.fiat_adapter import FiatAdapter
14
+ from flood_adapt.adapter.sfincs_adapter import SfincsAdapter
14
15
  from flood_adapt.config.hazard import SlrScenariosModel
15
16
  from flood_adapt.config.impacts import FloodmapType
16
17
  from flood_adapt.config.site import Site
@@ -22,7 +23,7 @@ from flood_adapt.dbs_classes.dbs_scenario import DbsScenario
22
23
  from flood_adapt.dbs_classes.dbs_static import DbsStatic
23
24
  from flood_adapt.dbs_classes.dbs_strategy import DbsStrategy
24
25
  from flood_adapt.dbs_classes.interface.database import IDatabase
25
- from flood_adapt.misc.exceptions import DatabaseError
26
+ from flood_adapt.misc.exceptions import ConfigError, DatabaseError
26
27
  from flood_adapt.misc.log import FloodAdaptLogging
27
28
  from flood_adapt.misc.path_builder import (
28
29
  TopLevelDir,
@@ -140,8 +141,6 @@ class Database(IDatabase):
140
141
 
141
142
  def shutdown(self):
142
143
  """Explicitly shut down the singleton and clear all references."""
143
- import gc
144
-
145
144
  self._instance = None
146
145
  self._init_done = False
147
146
 
@@ -191,7 +190,7 @@ class Database(IDatabase):
191
190
  SLR scenarios configuration model with the file path set to the static path.
192
191
  """
193
192
  if self.site.sfincs.slr_scenarios is None:
194
- raise DatabaseError("No SLR scenarios defined in the site configuration.")
193
+ raise ConfigError("No SLR scenarios defined in the site configuration.")
195
194
  slr = self.site.sfincs.slr_scenarios
196
195
  slr.file = str(self.static_path / slr.file)
197
196
  return slr
@@ -577,20 +576,13 @@ class Database(IDatabase):
577
576
  for dir in os.listdir(self.scenarios.output_path)
578
577
  ]
579
578
 
580
- def _call_garbage_collector(func, path, exc_info, retries=5, delay=0.1):
581
- """Retry deletion up to 5 times if the file is locked."""
582
- for attempt in range(retries):
583
- gc.collect()
584
- time.sleep(delay)
585
- try:
586
- func(path) # Retry deletion
587
- return # Exit if successful
588
- except Exception as e:
589
- print(
590
- f"Attempt {attempt + 1}/{retries} failed to delete {path}: {e}"
591
- )
592
-
593
- print(f"Giving up on deleting {path} after {retries} attempts.")
579
+ # Init model instances outside the loop
580
+ overland = self.static.get_overland_sfincs_model()
581
+ fiat = self.static.get_fiat_model()
582
+ if self.site.sfincs.config.offshore_model is not None:
583
+ offshore = self.static.get_offshore_sfincs_model()
584
+ else:
585
+ offshore = None
594
586
 
595
587
  for _dir in output_scenarios:
596
588
  # Delete if: input was deleted or corrupted output due to unfinished run
@@ -598,12 +590,18 @@ class Database(IDatabase):
598
590
  path.name for path in input_scenarios
599
591
  ] or not finished_file_exists(_dir):
600
592
  logger.info(f"Cleaning up corrupted outputs of scenario: {_dir.name}.")
601
- shutil.rmtree(_dir, onerror=_call_garbage_collector)
593
+ shutil.rmtree(_dir, ignore_errors=True)
602
594
  # If the scenario is finished, delete the simulation folders depending on `save_simulation`
603
595
  elif finished_file_exists(_dir):
604
- self._delete_simulations(_dir.name)
596
+ self._delete_simulations(_dir.name, overland, fiat, offshore)
605
597
 
606
- def _delete_simulations(self, scenario_name: str) -> None:
598
+ def _delete_simulations(
599
+ self,
600
+ scenario_name: str,
601
+ overland: SfincsAdapter,
602
+ fiat: FiatAdapter,
603
+ offshore: Optional[SfincsAdapter],
604
+ ) -> None:
607
605
  """Delete all simulation folders for a given scenario.
608
606
 
609
607
  Parameters
@@ -617,7 +615,6 @@ class Database(IDatabase):
617
615
 
618
616
  if not self.site.sfincs.config.save_simulation:
619
617
  # Delete SFINCS overland
620
- overland = self.static.get_overland_sfincs_model()
621
618
  if sub_events:
622
619
  for sub_event in sub_events:
623
620
  overland._delete_simulation_folder(scn, sub_event=sub_event)
@@ -625,8 +622,7 @@ class Database(IDatabase):
625
622
  overland._delete_simulation_folder(scn)
626
623
 
627
624
  # Delete SFINCS offshore
628
- if self.site.sfincs.config.offshore_model:
629
- offshore = self.static.get_offshore_sfincs_model()
625
+ if self.site.sfincs.config.offshore_model and offshore is not None:
630
626
  if sub_events:
631
627
  for sub_event in sub_events:
632
628
  sim_path = offshore._get_simulation_path_offshore(
@@ -639,7 +635,7 @@ class Database(IDatabase):
639
635
  sim_path.parent.iterdir()
640
636
  ):
641
637
  # Remove the parent directory `simulations` if it is empty
642
- sim_path.parent.rmdir()
638
+ shutil.rmtree(sim_path.parent, ignore_errors=True)
643
639
  else:
644
640
  sim_path = offshore._get_simulation_path_offshore(scn)
645
641
  if sim_path.exists():
@@ -648,9 +644,8 @@ class Database(IDatabase):
648
644
 
649
645
  if sim_path.parent.exists() and not any(sim_path.parent.iterdir()):
650
646
  # Remove the parent directory `simulations` if it is empty
651
- sim_path.parent.rmdir()
647
+ shutil.rmtree(sim_path.parent, ignore_errors=True)
652
648
 
653
649
  if not self.site.fiat.config.save_simulation:
654
650
  # Delete FIAT
655
- fiat = self.static.get_fiat_model()
656
651
  fiat._delete_simulation_folder(scn)
@@ -1,5 +1,3 @@
1
- import shutil
2
-
3
1
  from flood_adapt.dbs_classes.dbs_template import DbsTemplate
4
2
  from flood_adapt.misc.exceptions import DatabaseError
5
3
  from flood_adapt.workflows.benefit_runner import Benefit, BenefitRunner
@@ -37,30 +35,6 @@ class DbsBenefit(DbsTemplate[Benefit]):
37
35
  # Save the benefit
38
36
  super().save(object_model, overwrite=overwrite)
39
37
 
40
- def delete(self, name: str, toml_only: bool = False):
41
- """Delete an already existing benefit in the database.
42
-
43
- Parameters
44
- ----------
45
- name : str
46
- name of the benefit
47
- toml_only : bool, optional
48
- whether to only delete the toml file or the entire folder. If the folder is empty after deleting the toml,
49
- it will always be deleted. By default False
50
-
51
- Raises
52
- ------
53
- DatabaseError
54
- Raise error if benefit has already model output
55
- """
56
- # First delete the benefit
57
- super().delete(name, toml_only=toml_only)
58
-
59
- # Delete output if edited
60
- output_path = self.output_path / name
61
- if output_path.exists():
62
- shutil.rmtree(output_path, ignore_errors=True)
63
-
64
38
  def get_runner(self, name: str) -> BenefitRunner:
65
39
  return BenefitRunner(self._database, self.get(name))
66
40
 
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from flood_adapt.dbs_classes.dbs_template import DbsTemplate
4
- from flood_adapt.misc.exceptions import DatabaseError
4
+ from flood_adapt.misc.exceptions import DoesNotExistError
5
5
  from flood_adapt.objects.events.event_factory import EventFactory
6
6
  from flood_adapt.objects.events.event_set import EventSet
7
7
  from flood_adapt.objects.events.events import Event
@@ -31,7 +31,7 @@ class DbsEvent(DbsTemplate[Event]):
31
31
 
32
32
  # Check if the object exists
33
33
  if not Path(event_path).is_file():
34
- raise DatabaseError(f"{self.display_name} '{name}' does not exist.")
34
+ raise DoesNotExistError(name, self.display_name)
35
35
 
36
36
  # Load event
37
37
  return EventFactory.load_file(event_path, load_all=load_all)
@@ -4,7 +4,7 @@ from typing import Any
4
4
  import geopandas as gpd
5
5
 
6
6
  from flood_adapt.dbs_classes.dbs_template import DbsTemplate
7
- from flood_adapt.misc.exceptions import DatabaseError
7
+ from flood_adapt.misc.exceptions import DatabaseError, DoesNotExistError
8
8
  from flood_adapt.misc.utils import resolve_filepath
9
9
  from flood_adapt.objects.measures.measure_factory import MeasureFactory
10
10
  from flood_adapt.objects.measures.measures import Measure
@@ -34,7 +34,7 @@ class DbsMeasure(DbsTemplate[Measure]):
34
34
 
35
35
  # Check if the object exists
36
36
  if not Path(full_path).is_file():
37
- raise DatabaseError(f"{self.display_name}: '{name}' does not exist.")
37
+ raise DoesNotExistError(name, self.display_name)
38
38
 
39
39
  # Load and return the object
40
40
  return MeasureFactory.get_measure_object(full_path)
@@ -1,4 +1,3 @@
1
- import shutil
2
1
  from typing import Any
3
2
 
4
3
  from flood_adapt.dbs_classes.dbs_template import DbsTemplate
@@ -36,29 +35,6 @@ class DbsScenario(DbsTemplate[Scenario]):
36
35
 
37
36
  return scenarios
38
37
 
39
- def delete(self, name: str, toml_only: bool = False):
40
- """Delete an already existing scenario in the database.
41
-
42
- Parameters
43
- ----------
44
- name : str
45
- name of the scenario to be deleted
46
- toml_only : bool, optional
47
- whether to only delete the toml file or the entire folder. If the folder is empty after deleting the toml,
48
- it will always be deleted. By default False
49
-
50
- Raises
51
- ------
52
- DatabaseError
53
- Raise error if scenario to be deleted is already in use.
54
- """
55
- # First delete the scenario
56
- super().delete(name, toml_only)
57
-
58
- # Then delete the results
59
- if (self.output_path / name).exists():
60
- shutil.rmtree(self.output_path / name, ignore_errors=False)
61
-
62
38
  def check_higher_level_usage(self, name: str) -> list[str]:
63
39
  """Check if a scenario is used in a benefit.
64
40
 
@@ -12,7 +12,7 @@ from flood_adapt.adapter.sfincs_adapter import SfincsAdapter
12
12
  from flood_adapt.config.config import Settings
13
13
  from flood_adapt.dbs_classes.interface.database import IDatabase
14
14
  from flood_adapt.dbs_classes.interface.static import IDbsStatic
15
- from flood_adapt.misc.exceptions import DatabaseError
15
+ from flood_adapt.misc.exceptions import ConfigError, DatabaseError
16
16
 
17
17
 
18
18
  def cache_method_wrapper(func: Callable) -> Callable:
@@ -260,7 +260,7 @@ class DbsStatic(IDbsStatic):
260
260
  def get_offshore_sfincs_model(self) -> SfincsAdapter:
261
261
  """Get the template overland Sfincs model."""
262
262
  if self._database.site.sfincs.config.offshore_model is None:
263
- raise DatabaseError("No offshore model defined in the site configuration.")
263
+ raise ConfigError("No offshore model defined in the site configuration.")
264
264
 
265
265
  offshore_path = (
266
266
  self._database.static_path
@@ -273,7 +273,7 @@ class DbsStatic(IDbsStatic):
273
273
  def get_fiat_model(self) -> FiatAdapter:
274
274
  """Get the path to the FIAT model."""
275
275
  if self._database.site.fiat is None:
276
- raise DatabaseError("No FIAT model defined in the site configuration.")
276
+ raise ConfigError("No FIAT model defined in the site configuration.")
277
277
  template_path = self._database.static_path / "templates" / "fiat"
278
278
  with FiatAdapter(
279
279
  model_root=template_path,
@@ -287,7 +287,7 @@ class DbsStatic(IDbsStatic):
287
287
  @cache_method_wrapper
288
288
  def get_cyclone_track_database(self) -> CycloneTrackDatabase:
289
289
  if self._database.site.sfincs.cyclone_track_database is None:
290
- raise DatabaseError(
290
+ raise ConfigError(
291
291
  "No cyclone track database defined in the site configuration."
292
292
  )
293
293
  database_file = str(
@@ -1,7 +1,7 @@
1
1
  from itertools import combinations
2
2
 
3
3
  from flood_adapt.dbs_classes.dbs_template import DbsTemplate
4
- from flood_adapt.misc.exceptions import DatabaseError
4
+ from flood_adapt.misc.exceptions import AlreadyExistsError, DatabaseError
5
5
  from flood_adapt.objects.measures.measures import MeasureType
6
6
  from flood_adapt.objects.strategies.strategies import Strategy
7
7
 
@@ -49,9 +49,7 @@ class DbsStrategy(DbsTemplate[Strategy]):
49
49
  if overwrite and object_exists:
50
50
  self.delete(object_model.name, toml_only=True)
51
51
  elif not overwrite and object_exists:
52
- raise DatabaseError(
53
- f"'{object_model.name}' name is already used by another {self.display_name}. Choose a different name"
54
- )
52
+ raise AlreadyExistsError(object_model.name, self.display_name)
55
53
 
56
54
  # Check if any measures overlap
57
55
  self._check_overlapping_measures(object_model.measures)
@@ -8,7 +8,13 @@ import tomli_w
8
8
 
9
9
  from flood_adapt.dbs_classes.interface.database import IDatabase
10
10
  from flood_adapt.dbs_classes.interface.element import AbstractDatabaseElement
11
- from flood_adapt.misc.exceptions import DatabaseError
11
+ from flood_adapt.misc.exceptions import (
12
+ AlreadyExistsError,
13
+ DatabaseError,
14
+ DoesNotExistError,
15
+ IsStandardObjectError,
16
+ IsUsedInError,
17
+ )
12
18
  from flood_adapt.objects.object_model import Object
13
19
 
14
20
  T_OBJECTMODEL = TypeVar("T_OBJECTMODEL", bound=Object)
@@ -41,13 +47,18 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
41
47
  -------
42
48
  Object
43
49
  object of the type of the specified object model
50
+
51
+ Raises
52
+ ------
53
+ DoesNotExistError
54
+ Raise error if the object does not exist.
44
55
  """
45
56
  # Make the full path to the object
46
57
  full_path = self.input_path / name / f"{name}.toml"
47
58
 
48
59
  # Check if the object exists
49
60
  if not Path(full_path).is_file():
50
- raise DatabaseError(f"{self.display_name}: '{name}' does not exist.")
61
+ raise DoesNotExistError(name, self.display_name)
51
62
 
52
63
  # Load and return the object
53
64
  return self._object_class.load_file(full_path)
@@ -74,6 +85,15 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
74
85
  name of the new measure
75
86
  new_description : str
76
87
  description of the new measure
88
+
89
+ Raises
90
+ ------
91
+ AlreadyExistsError
92
+ Raise error if an object with the new name already exists.
93
+ IsStandardObjectError
94
+ Raise error if an object with the new name is a standard object.
95
+ DatabaseError
96
+ Raise error if the saving of the object fails.
77
97
  """
78
98
  copy_object = self.get(old_name)
79
99
  copy_object.name = new_name
@@ -123,8 +143,14 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
123
143
 
124
144
  Raises
125
145
  ------
146
+ AlreadyExistsError
147
+ Raise error if object to be saved already exists.
148
+ IsStandardObjectError
149
+ Raise error if object to be overwritten is a standard object.
150
+ IsUsedInError
151
+ Raise error if object to be overwritten is already in use.
126
152
  DatabaseError
127
- Raise error if name is already in use.
153
+ Raise error if the overwriting of the object fails.
128
154
  """
129
155
  self._validate_to_save(object_model, overwrite=overwrite)
130
156
 
@@ -138,7 +164,7 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
138
164
  )
139
165
 
140
166
  def delete(self, name: str, toml_only: bool = False):
141
- """Delete an already existing object in the database.
167
+ """Delete an already existing object as well as its outputs from the database.
142
168
 
143
169
  Parameters
144
170
  ----------
@@ -150,34 +176,50 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
150
176
 
151
177
  Raises
152
178
  ------
153
- DatabaseError
179
+ IsStandardObjectError
180
+ Raise error if object to be deleted is a standard object.
181
+ IsUsedInError
154
182
  Raise error if object to be deleted is already in use.
183
+ DoesNotExistError
184
+ Raise error if object to be deleted does not exist.
185
+ DatabaseError
186
+ Raise error if the deletion of the object fails.
155
187
  """
156
188
  # Check if the object is a standard object. If it is, raise an error
157
189
  if self._check_standard_objects(name):
158
- raise DatabaseError(
159
- f"'{name}' cannot be deleted/modified since it is a standard {self.display_name}."
160
- )
190
+ raise IsStandardObjectError(name, self.display_name)
161
191
 
162
192
  # Check if object is used in a higher level object. If it is, raise an error
163
193
  if used_in := self.check_higher_level_usage(name):
164
- raise DatabaseError(
165
- f"{self.display_name}: '{name}' cannot be deleted/modified since it is already used in the {self._higher_lvl_object.capitalize()}(s): {', '.join(used_in)}"
194
+ raise IsUsedInError(
195
+ name, self.display_name, self._higher_lvl_object, used_in
166
196
  )
167
197
 
168
- # Once all checks are passed, delete the object
198
+ # Check if the object exists
169
199
  toml_path = self.input_path / name / f"{name}.toml"
170
- if toml_only:
171
- # Only delete the toml file
172
- toml_path.unlink(missing_ok=True)
173
- # If the folder is empty, delete the folder
174
- if not list(toml_path.parent.iterdir()):
175
- toml_path.parent.rmdir()
176
- else:
177
- # Delete the entire folder
178
- shutil.rmtree(toml_path.parent, ignore_errors=True)
179
- if (self.output_path / name).exists():
180
- shutil.rmtree(self.output_path / name, ignore_errors=True)
200
+ if not toml_path.exists():
201
+ raise DoesNotExistError(name, self.display_name)
202
+
203
+ # Once all checks are passed, delete the object
204
+ toml_path.unlink(missing_ok=True)
205
+
206
+ # Delete the entire folder
207
+ if not list(toml_path.parent.iterdir()):
208
+ shutil.rmtree(toml_path.parent)
209
+ elif not toml_only:
210
+ try:
211
+ shutil.rmtree(toml_path.parent)
212
+ except OSError as e:
213
+ raise DatabaseError(f"Failed to delete `{name}` due to: {e}") from e
214
+
215
+ # Delete output
216
+ if (self.output_path / name).exists():
217
+ try:
218
+ shutil.rmtree(self.output_path / name)
219
+ except OSError as e:
220
+ raise DatabaseError(
221
+ f"Failed to delete output of `{name}` due to: {e}"
222
+ ) from e
181
223
 
182
224
  def _check_standard_objects(self, name: str) -> bool:
183
225
  """Check if an object is a standard object.
@@ -275,6 +317,4 @@ class DbsTemplate(AbstractDatabaseElement[T_OBJECTMODEL]):
275
317
  if overwrite and object_exists:
276
318
  self.delete(object_model.name, toml_only=True)
277
319
  elif not overwrite and object_exists:
278
- raise DatabaseError(
279
- f"'{object_model.name}' name is already used by another {self.display_name.lower()}. Choose a different name"
280
- )
320
+ raise AlreadyExistsError(object_model.name, self.display_name)
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, List, Optional, Union
2
+ from typing import Any, List, Literal, Optional, Union
3
3
 
4
4
  import geopandas as gpd
5
5
  import numpy as np
@@ -935,43 +935,94 @@ class FloodAdapt:
935
935
 
936
936
  return infographic_path
937
937
 
938
- def get_infometrics(self, name: str) -> pd.DataFrame:
939
- """Return the metrics for the given scenario.
938
+ def get_infometrics(
939
+ self, name: str, aggr_name: Optional[str] = None
940
+ ) -> pd.DataFrame:
941
+ """Return the infometrics DataFrame for the given scenario and optional aggregation.
940
942
 
941
943
  Parameters
942
944
  ----------
943
945
  name : str
944
946
  The name of the scenario.
947
+ aggr_name : Optional[str], default None
948
+ The name of the aggregation, if any.
945
949
 
946
950
  Returns
947
951
  -------
948
- metrics: pd.DataFrame
949
- The metrics for the scenario.
952
+ df : pd.DataFrame
953
+ The infometrics DataFrame for the scenario (and aggregation if specified).
950
954
 
951
955
  Raises
952
956
  ------
953
957
  FileNotFoundError
954
- If the metrics file does not exist.
958
+ If the metrics file does not exist for the given scenario (and aggregation).
955
959
  """
960
+ if aggr_name is not None:
961
+ fn = f"Infometrics_{name}_{aggr_name}.csv"
962
+ else:
963
+ fn = f"Infometrics_{name}.csv"
956
964
  # Create the infographic path
957
- metrics_path = self.database.scenarios.output_path.joinpath(
958
- name,
959
- f"Infometrics_{name}.csv",
960
- )
965
+ metrics_path = self.database.scenarios.output_path.joinpath(name, fn)
961
966
 
962
967
  # Check if the file exists
963
968
  if not metrics_path.exists():
964
969
  raise FileNotFoundError(
965
970
  f"The metrics file for scenario {name}({str(metrics_path)}) does not exist."
966
971
  )
967
-
968
972
  # Read the metrics file
969
- return MetricsFileReader(str(metrics_path)).read_metrics_from_file(
973
+ df = MetricsFileReader(str(metrics_path)).read_metrics_from_file(
970
974
  include_long_names=True,
971
975
  include_description=True,
972
976
  include_metrics_table_selection=True,
977
+ include_metrics_map_selection=True,
978
+ )
979
+ if aggr_name is not None:
980
+ df = df.T
981
+ return df
982
+
983
+ def get_aggr_metric_layers(
984
+ self,
985
+ name: str,
986
+ aggr_type: str,
987
+ type: Literal["single_event", "risk"] = "single_event",
988
+ rp: Optional[int] = None,
989
+ equity: bool = False,
990
+ ) -> list[dict]:
991
+ # Read infometrics from csv file
992
+ metrics_df = self.get_infometrics(name, aggr_name=aggr_type)
993
+
994
+ # Filter based on "Show in Metrics Map" column
995
+ if "Show In Metrics Map" in metrics_df.index:
996
+ mask = metrics_df.loc["Show In Metrics Map"].to_numpy().astype(bool)
997
+ metrics_df = metrics_df.loc[:, mask]
998
+
999
+ # Keep only relevant attributes of the infometrics
1000
+ keep_rows = [
1001
+ "Description",
1002
+ "Long Name",
1003
+ "Show In Metrics Table",
1004
+ "Show In Metrics Map",
1005
+ ]
1006
+ metrics_df = metrics_df.loc[
1007
+ [row for row in keep_rows if row in metrics_df.index]
1008
+ ]
1009
+
1010
+ # Transform to list of dicts
1011
+ metrics = []
1012
+ for col in metrics_df.columns:
1013
+ metric_dict = {"name": col}
1014
+ # Add the first 4 rows as key-value pairs
1015
+ for i, idx in enumerate(metrics_df.index):
1016
+ metric_dict[idx] = metrics_df.loc[idx, col]
1017
+ metrics.append(metric_dict)
1018
+
1019
+ # Get the filtered metrics layers from the GUI configuration
1020
+ filtered_metrics = self.database.site.gui.output_layers.get_aggr_metrics_layers(
1021
+ metrics, type, rp, equity
973
1022
  )
974
1023
 
1024
+ return filtered_metrics
1025
+
975
1026
  # Static
976
1027
  def load_static_data(self):
977
1028
  """Read the static data into the cache.
@@ -10,13 +10,50 @@ class DatabaseError(FloodAdaptError):
10
10
  pass
11
11
 
12
12
 
13
- class ComponentError(FloodAdaptError):
14
- """Base class for exceptions raised in any component/object related files."""
13
+ class IsStandardObjectError(DatabaseError):
14
+ """Raised when an operation is attempted on a standard object."""
15
15
 
16
- pass
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ object_type: str,
20
+ ):
21
+ msg = f"The {object_type} '{name}' is a standard object and cannot be modified or deleted."
22
+ super().__init__(msg)
17
23
 
18
24
 
19
- class WorkflowError(FloodAdaptError):
20
- """Base class for exceptions raised in any workflow related files."""
25
+ class AlreadyExistsError(DatabaseError):
26
+ """Raised when an attempt is made to create an object that already exists."""
21
27
 
22
- pass
28
+ def __init__(
29
+ self,
30
+ name: str,
31
+ object_type: str,
32
+ ):
33
+ msg = f"The {object_type} '{name}' already exists."
34
+ super().__init__(msg)
35
+
36
+
37
+ class DoesNotExistError(DatabaseError):
38
+ """Raised when an attempt is made to access an object that does not exist."""
39
+
40
+ def __init__(self, name: str, object_type: str):
41
+ msg = f"The {object_type} '{name}' does not exist."
42
+ super().__init__(msg)
43
+
44
+
45
+ class IsUsedInError(DatabaseError):
46
+ """Raised when an attempt is made to delete or modify an object that is in use / referenced by another object."""
47
+
48
+ def __init__(
49
+ self, name: str, object_type: str, used_in_type: str, used_in: list[str]
50
+ ):
51
+ msg = f"The {object_type} '{name}' cannot be deleted/modified since it is already used in the {used_in_type}(s): {', '.join(used_in)}"
52
+ super().__init__(msg)
53
+
54
+
55
+ class ConfigError(DatabaseError):
56
+ """Raised when optional configuration, usually in the site, is missing or invalid."""
57
+
58
+ def __init__(self, message: str):
59
+ super().__init__(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: flood-adapt
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: A software package support system which can be used to assess the benefits and costs of flood resilience measures
5
5
  Author-email: Gundula Winter <Gundula.Winter@deltares.nl>, Panos Athanasiou <Panos.Athanasiou@deltares.nl>, Frederique de Groen <Frederique.deGroen@deltares.nl>, Tim de Wilde <Tim.deWilde@deltares.nl>, Julian Hofer <Julian.Hofer@deltares.nl>, Daley Adrichem <Daley.Adrichem@deltares.nl>, Luuk Blom <Luuk.Blom@deltares.nl>
6
6
  License: ====================================================
@@ -714,7 +714,7 @@ Requires-Dist: cht-observations==0.2.1
714
714
  Requires-Dist: cht-tide==0.1.1
715
715
  Requires-Dist: dask==2024.11.2
716
716
  Requires-Dist: numba_celltree==0.2.2
717
- Requires-Dist: fiat-toolbox==0.1.20
717
+ Requires-Dist: fiat-toolbox<0.2.0,>=0.1.22
718
718
  Requires-Dist: fiona<2.0,>=1.0
719
719
  Requires-Dist: geojson<4.0,>=3.0
720
720
  Requires-Dist: geopandas<2.0,>=1.0
@@ -723,7 +723,7 @@ Requires-Dist: hydromt-sfincs==1.2.0
723
723
  Requires-Dist: numpy<2.0,>=1.0
724
724
  Requires-Dist: numpy-financial<2.0,>=1.0
725
725
  Requires-Dist: pandas<3.0,>=2.0
726
- Requires-Dist: plotly<7.0,>=6.0
726
+ Requires-Dist: plotly<6.3,>=6.0
727
727
  Requires-Dist: pydantic<3.0,>=2.0
728
728
  Requires-Dist: pydantic-settings<3.0,>=2.0
729
729
  Requires-Dist: pyogrio<1.0