flood-adapt 0.3.10__py3-none-any.whl → 0.3.12__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.
flood_adapt/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
1
  # has to be here at the start to avoid circular imports
2
- __version__ = "0.3.10"
2
+ __version__ = "0.3.12"
3
3
 
4
4
  from flood_adapt import adapter, dbs_classes, objects
5
5
  from flood_adapt.config.config import Settings
6
6
  from flood_adapt.config.site import Site
7
+ from flood_adapt.database_builder.database_builder import DatabaseBuilder
7
8
  from flood_adapt.flood_adapt import FloodAdapt
8
9
  from flood_adapt.misc.exceptions import ComponentError, DatabaseError, FloodAdaptError
9
10
  from flood_adapt.misc.log import FloodAdaptLogging
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "FloodAdaptError",
22
23
  "DatabaseError",
23
24
  "ComponentError",
25
+ "DatabaseBuilder",
24
26
  ]
25
27
 
26
28
  FloodAdaptLogging() # Initialize logging once for the entire package
@@ -153,11 +153,16 @@ class FiatAdapter(IImpactAdapter):
153
153
 
154
154
  def close_files(self):
155
155
  """Close all open files and clean up file handles."""
156
- if hasattr(self.logger, "handlers"):
157
- for handler in self.logger.handlers:
158
- if isinstance(handler, logging.FileHandler):
159
- handler.close()
160
- self.logger.removeHandler(handler)
156
+ loggers = [self.logger]
157
+ if self._model is not None:
158
+ loggers.append(self._model.logger)
159
+
160
+ for logger in loggers:
161
+ if hasattr(logger, "handlers"):
162
+ for handler in logger.handlers:
163
+ if isinstance(handler, logging.FileHandler):
164
+ handler.close()
165
+ logger.removeHandler(handler)
161
166
 
162
167
  def __enter__(self) -> "FiatAdapter":
163
168
  return self
@@ -198,11 +203,10 @@ class FiatAdapter(IImpactAdapter):
198
203
  ------
199
204
  OSError: If the directory cannot be deleted.
200
205
  """
201
- self.logger.info("Deleting Delft-FIAT simulation folder")
202
- try:
206
+ self.close_files()
207
+ if self.model_root.exists():
208
+ self.logger.info(f"Deleting {self.model_root}")
203
209
  shutil.rmtree(self.model_root)
204
- except OSError as e_info:
205
- self.logger.warning(f"{e_info}\nCould not delete {self.model_root}.")
206
210
 
207
211
  def fiat_completed(self) -> bool:
208
212
  """Check if fiat has run as expected.
@@ -1539,3 +1543,25 @@ class FiatAdapter(IImpactAdapter):
1539
1543
  if line.startswith("#"):
1540
1544
  line = "#" + " " * hash_spacing + line.lstrip("#")
1541
1545
  file.write(line)
1546
+
1547
+ def _delete_simulation_folder(self, scn: Scenario):
1548
+ """
1549
+ Delete the Delft-FIAT simulation folder for a given scenario.
1550
+
1551
+ Parameters
1552
+ ----------
1553
+ scn : Scenario
1554
+ The scenario for which the simulation folder should be deleted.
1555
+
1556
+ Raises
1557
+ ------
1558
+ OSError
1559
+ If the directory cannot be deleted.
1560
+ """
1561
+ simulation_path = (
1562
+ self.database.scenarios.output_path / scn.name / "Impacts" / "fiat_model"
1563
+ )
1564
+ if simulation_path.exists():
1565
+ self.close_files()
1566
+ shutil.rmtree(simulation_path)
1567
+ self.logger.info(f"Deleted Delft-FIAT simulation folder: {simulation_path}")
@@ -35,8 +35,8 @@ from flood_adapt.misc.path_builder import (
35
35
  from flood_adapt.misc.utils import cd, resolve_filepath
36
36
  from flood_adapt.objects.events.event_set import EventSet
37
37
  from flood_adapt.objects.events.events import Event, Mode, Template
38
- from flood_adapt.objects.events.historical import HistoricalEvent
39
38
  from flood_adapt.objects.events.hurricane import TranslationModel
39
+ from flood_adapt.objects.events.synthetic import SyntheticEvent
40
40
  from flood_adapt.objects.forcing import unit_system as us
41
41
  from flood_adapt.objects.forcing.discharge import (
42
42
  DischargeConstant,
@@ -257,9 +257,8 @@ class SfincsAdapter(IHazardAdapter):
257
257
  template_path = (
258
258
  self.database.static.get_overland_sfincs_model().get_model_root()
259
259
  )
260
- shutil.copytree(template_path, sim_path, dirs_exist_ok=True)
261
260
 
262
- with SfincsAdapter(model_root=sim_path) as model:
261
+ with SfincsAdapter(model_root=template_path) as model:
263
262
  model._load_scenario_objects(scenario, event)
264
263
  is_risk = "Probabilistic " if model._event_set is not None else ""
265
264
  self.logger.info(
@@ -765,89 +764,32 @@ class SfincsAdapter(IHazardAdapter):
765
764
 
766
765
  with SfincsAdapter(model_root=sim_paths[0]) as dummymodel:
767
766
  # read mask and bed level
768
- mask = dummymodel.get_mask().stack(z=("x", "y"))
769
- zb = dummymodel.get_bedlevel().stack(z=("x", "y")).to_numpy()
767
+ mask = dummymodel.get_mask()
768
+ zb = dummymodel.get_bedlevel()
770
769
 
771
770
  zs_maps = []
772
771
  for simulation_path in sim_paths:
773
772
  # read zsmax data from overland sfincs model
774
773
  with SfincsAdapter(model_root=simulation_path) as sim:
775
774
  zsmax = sim._get_zsmax().load()
776
- zs_stacked = zsmax.stack(z=("x", "y"))
777
- zs_maps.append(zs_stacked)
775
+ zs_maps.append(zsmax)
778
776
 
779
777
  # Create RP flood maps
780
-
781
- # 1a: make a table of all water levels and associated frequencies
782
- zs = xr.concat(zs_maps, pd.Index(frequencies, name="frequency"))
783
- # Get the indices of columns with all NaN values
784
- nan_cells = np.where(np.all(np.isnan(zs), axis=0))[0]
785
- # fill nan values with minimum bed levels in each grid cell, np.interp cannot ignore nan values
786
- zs = xr.where(np.isnan(zs), np.tile(zb, (zs.shape[0], 1)), zs)
787
- # Get table of frequencies
788
- freq = np.tile(frequencies, (zs.shape[1], 1)).transpose()
789
-
790
- # 1b: sort water levels in descending order and include the frequencies in the sorting process
791
- # (i.e. each h-value should be linked to the same p-values as in step 1a)
792
- sort_index = zs.argsort(axis=0)
793
- sorted_prob = np.flipud(np.take_along_axis(freq, sort_index, axis=0))
794
- sorted_zs = np.flipud(np.take_along_axis(zs.values, sort_index, axis=0))
795
-
796
- # 1c: Compute exceedance probabilities of water depths
797
- # Method: accumulate probabilities from top to bottom
798
- prob_exceed = np.cumsum(sorted_prob, axis=0)
799
-
800
- # 1d: Compute return periods of water depths
801
- # Method: simply take the inverse of the exceedance probability (1/Pex)
802
- rp_zs = 1.0 / prob_exceed
803
-
804
- # For each return period (T) of interest do the following:
805
- # For each grid cell do the following:
806
- # Use the table from step [1d] as a “lookup-table” to derive the T-year water depth. Use a 1-d interpolation technique:
807
- # h(T) = interp1 (log(T*), h*, log(T))
808
- # in which t* and h* are the values from the table and T is the return period (T) of interest
809
- # The resulting T-year water depths for all grids combined form the T-year hazard map
810
- rp_da = xr.DataArray(rp_zs, dims=zs.dims)
811
-
812
- # no_data_value = -999 # in SFINCS
813
- # sorted_zs = xr.where(sorted_zs == no_data_value, np.nan, sorted_zs)
814
-
815
- valid_cells = np.where(mask == 1)[
816
- 0
817
- ] # only loop over cells where model is not masked
818
- h = matlib.repmat(
819
- np.copy(zb), len(floodmap_rp), 1
820
- ) # if not flooded (i.e. not in valid_cells) revert to bed_level, read from SFINCS results so it is the minimum bed level in a grid cell
821
-
822
778
  self.logger.info("Calculating flood risk maps, this may take some time")
823
- for jj in valid_cells: # looping over all non-masked cells.
824
- # linear interpolation for all return periods to evaluate
825
- h[:, jj] = np.interp(
826
- np.log10(floodmap_rp),
827
- np.log10(rp_da[::-1, jj]),
828
- sorted_zs[::-1, jj],
829
- left=0,
830
- )
831
-
832
- # Re-fill locations that had nan water level for all simulations with nans
833
- h[:, nan_cells] = np.full(h[:, nan_cells].shape, np.nan)
834
-
835
- # If a cell has the same water-level as the bed elevation it should be dry (turn to nan)
836
- diff = h - np.tile(zb, (h.shape[0], 1))
837
- dry = (
838
- diff < 10e-10
839
- ) # here we use a small number instead of zero for rounding errors
840
- h[dry] = np.nan
779
+ rp_flood_maps = self.calc_rp_maps(
780
+ floodmaps=zs_maps,
781
+ frequencies=frequencies,
782
+ zb=zb,
783
+ mask=mask,
784
+ return_periods=floodmap_rp,
785
+ )
841
786
 
842
787
  for ii, rp in enumerate(floodmap_rp):
843
- # #create single nc
844
- zs_rp_single = xr.DataArray(
845
- data=h[ii, :], coords={"z": zs["z"]}, attrs={"units": "meters"}
846
- ).unstack()
788
+ zs_rp_single = rp_flood_maps[ii]
847
789
  zs_rp_single = zs_rp_single.rio.write_crs(
848
790
  zsmax.raster.crs
849
791
  ) # , inplace=True)
850
- zs_rp_single = zs_rp_single.to_dataset(name="risk_map")
792
+ zs_rp_single = zs_rp_single.to_dataset(name="risk_map").transpose()
851
793
  fn_rp = result_path / f"RP_{rp:04d}_maps.nc"
852
794
  zs_rp_single.to_netcdf(fn_rp)
853
795
 
@@ -885,9 +827,22 @@ class SfincsAdapter(IHazardAdapter):
885
827
  self.preprocess(scenario, event)
886
828
  self.process(scenario, event)
887
829
  self.postprocess(scenario, event)
888
- shutil.rmtree(
889
- self._get_simulation_path(scenario, sub_event=event), ignore_errors=True
890
- )
830
+
831
+ if self.settings.config.save_simulation:
832
+ self._delete_simulation_folder(scenario, sub_event=event)
833
+
834
+ def _delete_simulation_folder(
835
+ self, scenario: Scenario, sub_event: Optional[Event] = None
836
+ ):
837
+ """Delete the simulation folder for a given scenario and optional sub-event."""
838
+ sim_path = self._get_simulation_path(scenario, sub_event=sub_event)
839
+ if sim_path.exists():
840
+ shutil.rmtree(sim_path, ignore_errors=True)
841
+ self.logger.info(f"Deleted simulation folder: {sim_path}")
842
+
843
+ if sim_path.parent.exists() and not any(sim_path.parent.iterdir()):
844
+ # Remove the parent directory `simulations` if it is empty
845
+ sim_path.parent.rmdir()
891
846
 
892
847
  def _run_risk_scenario(self, scenario: Scenario):
893
848
  """Run the whole workflow for a risk scenario.
@@ -911,11 +866,12 @@ class SfincsAdapter(IHazardAdapter):
911
866
  self.calculate_rp_floodmaps(scenario)
912
867
 
913
868
  # Cleanup
914
- for i, sub_event in enumerate(event_set._events):
915
- shutil.rmtree(
916
- self._get_simulation_path(scenario, sub_event=sub_event),
917
- ignore_errors=True,
918
- )
869
+ if not self.settings.config.save_simulation:
870
+ for i, sub_event in enumerate(event_set._events):
871
+ shutil.rmtree(
872
+ self._get_simulation_path(scenario, sub_event=sub_event),
873
+ ignore_errors=True,
874
+ )
919
875
 
920
876
  def _ensure_no_existing_forcings(self):
921
877
  """Check for existing forcings in the model and raise an error if any are found."""
@@ -1819,8 +1775,7 @@ class SfincsAdapter(IHazardAdapter):
1819
1775
  def _add_tide_gauge_plot(
1820
1776
  self, fig, event: Event, units: us.UnitTypesLength
1821
1777
  ) -> None:
1822
- # check if event is historic
1823
- if not isinstance(event, HistoricalEvent):
1778
+ if isinstance(event, SyntheticEvent):
1824
1779
  return
1825
1780
  if self.settings.tide_gauge is None:
1826
1781
  return
@@ -1832,17 +1787,161 @@ class SfincsAdapter(IHazardAdapter):
1832
1787
  units=us.UnitTypesLength(units),
1833
1788
  )
1834
1789
 
1835
- if df_gauge is not None:
1836
- gauge_reference_height = self.settings.water_level.get_datum(
1837
- self.settings.tide_gauge.reference
1838
- ).height.convert(units)
1790
+ if df_gauge is None:
1791
+ self.logger.warning(
1792
+ "No water level data available for the tide gauge. Could not add it to the plot."
1793
+ )
1794
+ return
1795
+
1796
+ gauge_reference_height = self.settings.water_level.get_datum(
1797
+ self.settings.tide_gauge.reference
1798
+ ).height.convert(units)
1799
+
1800
+ waterlevel = df_gauge.iloc[:, 0] + gauge_reference_height
1801
+
1802
+ # If data is available, add to plot
1803
+ fig.add_trace(px.line(waterlevel, color_discrete_sequence=["#ea6404"]).data[0])
1804
+ fig["data"][0]["name"] = "model"
1805
+ fig["data"][1]["name"] = "measurement"
1806
+ fig.update_layout(showlegend=True)
1807
+
1808
+ @staticmethod
1809
+ def calc_rp_maps(
1810
+ floodmaps: list[xr.DataArray],
1811
+ frequencies: list[float],
1812
+ zb: xr.DataArray,
1813
+ mask: xr.DataArray,
1814
+ return_periods: list[float],
1815
+ ) -> list[xr.DataArray]:
1816
+ """
1817
+ Calculate return period (RP) flood maps from a set of flood simulation results.
1839
1818
 
1840
- waterlevel = df_gauge.iloc[:, 0] + gauge_reference_height
1819
+ This function processes multiple flood simulation outputs (water level maps) and their associated frequencies
1820
+ to generate hazard maps for specified return periods. It interpolates water levels for each return period
1821
+ using exceedance probabilities and handles masked or dry cells appropriately.
1841
1822
 
1842
- # If data is available, add to plot
1843
- fig.add_trace(
1844
- px.line(waterlevel, color_discrete_sequence=["#ea6404"]).data[0]
1823
+ Args:
1824
+ floodmaps (list[xr.DataArray]): List of water level maps (xarray DataArrays), one for each simulation.
1825
+ frequencies (list[float]): List of frequencies (probabilities of occurrence) corresponding to each floodmap.
1826
+ zb (np.ndarray): Array of bed elevations for each grid cell.
1827
+ mask (xr.DataArray): Mask indicating valid (1) and invalid (0) grid cells.
1828
+ return_periods (list[float]): List of return periods (in years) for which to generate hazard maps.
1829
+
1830
+ Returns
1831
+ -------
1832
+ list[xr.DataArray]: List of xarray DataArrays, each representing the hazard map for a given return period.
1833
+ Each DataArray contains water levels (meters) for the corresponding return period.
1834
+ """
1835
+ floodmaps = floodmaps.copy() # avoid modifying the original list
1836
+ # Check that all floodmaps have the same shape and dimensions
1837
+ first_shape = floodmaps[0].shape
1838
+ first_dims = floodmaps[0].dims
1839
+ for i, floodmap in enumerate(floodmaps):
1840
+ if floodmap.shape != first_shape or floodmap.dims != first_dims:
1841
+ raise ValueError(
1842
+ f"Floodmap at index {i} does not match the shape or dimensions of the first floodmap. "
1843
+ f"Expected shape {first_shape} and dims {first_dims}, got shape {floodmap.shape} and dims {floodmap.dims}."
1844
+ )
1845
+
1846
+ # Check that zb and mask have the same shape
1847
+ if zb.shape != mask.shape:
1848
+ raise ValueError(
1849
+ "Bed elevation array (zb) and mask must have the same shape."
1850
+ )
1851
+
1852
+ # Check that floodmaps, zb, and mask all have the same shape
1853
+ if (
1854
+ len(first_shape) != len(zb.shape)
1855
+ or first_shape != zb.shape
1856
+ or first_shape != mask.shape
1857
+ ):
1858
+ raise ValueError(
1859
+ f"Floodmaps, bed elevation array (zb), and mask must all have the same shape. "
1860
+ f"Floodmap shape: {first_shape}, zb shape: {zb.shape}, mask shape: {mask.shape}."
1861
+ )
1862
+
1863
+ # stack dimensions if floodmaps are 2D
1864
+ if len(floodmaps[0].shape) > 1:
1865
+ stacking = True
1866
+ for i, floodmap in enumerate(floodmaps):
1867
+ floodmaps[i] = floodmap.stack(z=("x", "y"))
1868
+ zb = zb.stack(z=("x", "y"))
1869
+ mask = mask.stack(z=("x", "y"))
1870
+ else:
1871
+ stacking = False
1872
+
1873
+ # 1a: make a table of all water levels and associated frequencies
1874
+ zs = xr.concat(floodmaps, pd.Index(frequencies, name="frequency"))
1875
+ # Get the indices of columns with all NaN values
1876
+ nan_cells = np.where(np.all(np.isnan(zs), axis=0))[0]
1877
+ # fill nan values with minimum bed levels in each grid cell, np.interp cannot ignore nan values
1878
+ zs = xr.where(np.isnan(zs), np.tile(zb, (zs.shape[0], 1)), zs)
1879
+ # Get table of frequencies
1880
+ freq = np.tile(frequencies, (zs.shape[1], 1)).transpose()
1881
+
1882
+ # 1b: sort water levels in descending order and include the frequencies in the sorting process
1883
+ # (i.e. each h-value should be linked to the same p-values as in step 1a)
1884
+ sort_index = zs.argsort(axis=0)
1885
+ sorted_prob = np.flipud(np.take_along_axis(freq, sort_index, axis=0))
1886
+ sorted_zs = np.flipud(np.take_along_axis(zs.values, sort_index, axis=0))
1887
+
1888
+ # 1c: Compute exceedance probabilities of water depths
1889
+ # Method: accumulate probabilities from top to bottom
1890
+ prob_exceed = np.cumsum(sorted_prob, axis=0)
1891
+
1892
+ # 1d: Compute return periods of water depths
1893
+ # Method: simply take the inverse of the exceedance probability (1/Pex)
1894
+ rp_zs = 1.0 / prob_exceed
1895
+
1896
+ # For each return period (T) of interest do the following:
1897
+ # For each grid cell do the following:
1898
+ # Use the table from step [1d] as a “lookup-table” to derive the T-year water depth. Use a 1-d interpolation technique:
1899
+ # h(T) = interp1 (log(T*), h*, log(T))
1900
+ # in which t* and h* are the values from the table and T is the return period (T) of interest
1901
+ # The resulting T-year water depths for all grids combined form the T-year hazard map
1902
+ rp_da = xr.DataArray(rp_zs, dims=zs.dims)
1903
+
1904
+ # no_data_value = -999 # in SFINCS
1905
+ # sorted_zs = xr.where(sorted_zs == no_data_value, np.nan, sorted_zs)
1906
+
1907
+ valid_cells = np.where(mask == 1)[
1908
+ 0
1909
+ ] # only loop over cells where model is not masked
1910
+ h = matlib.repmat(
1911
+ np.copy(zb), len(return_periods), 1
1912
+ ) # if not flooded (i.e. not in valid_cells) revert to bed_level, read from SFINCS results so it is the minimum bed level in a grid cell
1913
+
1914
+ for jj in valid_cells: # looping over all non-masked cells.
1915
+ # linear interpolation for all return periods to evaluate
1916
+ h[:, jj] = np.interp(
1917
+ np.log10(return_periods),
1918
+ np.log10(rp_da[::-1, jj]),
1919
+ sorted_zs[::-1, jj],
1920
+ left=0,
1845
1921
  )
1846
- fig["data"][0]["name"] = "model"
1847
- fig["data"][1]["name"] = "measurement"
1848
- fig.update_layout(showlegend=True)
1922
+
1923
+ # Re-fill locations that had nan water level for all simulations with nans
1924
+ h[:, nan_cells] = np.full(h[:, nan_cells].shape, np.nan)
1925
+
1926
+ # If a cell has the same water-level as the bed elevation it should be dry (turn to nan)
1927
+ diff = h - np.tile(zb, (h.shape[0], 1))
1928
+ dry = (
1929
+ diff < 10e-10
1930
+ ) # here we use a small number instead of zero for rounding errors
1931
+ h[dry] = np.nan
1932
+
1933
+ rp_maps = []
1934
+ for ii, rp in enumerate(return_periods):
1935
+ da = xr.DataArray(
1936
+ data=h[ii, :], coords={"z": zs["z"]}, attrs={"units": "meters"}
1937
+ )
1938
+ if stacking:
1939
+ # Ensure unstacking creates (y, x) dimensions in the correct order
1940
+ da = da.unstack()
1941
+ # Reorder dimensions if needed
1942
+ if set(da.dims) == {"y", "x"} and da.dims != ("y", "x"):
1943
+ da = da.transpose("y", "x")
1944
+ # #create single nc
1945
+ rp_maps.append(da)
1946
+
1947
+ return rp_maps
@@ -97,14 +97,8 @@ class OffshoreSfincsHandler(IOffshoreSfincsHandler, DatabaseUser):
97
97
  # SfincsAdapter.write() doesnt write the bca file apparently so we need to copy the template
98
98
  if sim_path.exists():
99
99
  shutil.rmtree(sim_path)
100
- shutil.copytree(self.template_path, sim_path)
101
100
 
102
- with SfincsAdapter(model_root=sim_path) as _offshore_model:
103
- if _offshore_model.sfincs_completed(sim_path):
104
- _offshore_model.logger.info(
105
- f"Skip preprocessing offshore model as it has already been run for `{self.scenario.name}`."
106
- )
107
- return
101
+ with SfincsAdapter(model_root=self.template_path) as _offshore_model:
108
102
  # Load objects, set root & write template model
109
103
  _offshore_model._load_scenario_objects(self.scenario, self.event)
110
104
  _offshore_model.write(path_out=sim_path)
flood_adapt/config/gui.py CHANGED
@@ -39,6 +39,7 @@ class Layer(BaseModel):
39
39
  class FloodMapLayer(Layer):
40
40
  zbmax: float
41
41
  depth_min: float
42
+ roads_min_zoom_level: int = 14
42
43
 
43
44
 
44
45
  class AggregationDmgLayer(Layer):