flood-adapt 0.3.11__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.
@@ -1,9 +1,12 @@
1
1
  import datetime
2
+ import logging
2
3
  import math
3
4
  import os
4
5
  import shutil
6
+ import time
5
7
  import warnings
6
8
  from enum import Enum
9
+ from functools import wraps
7
10
  from pathlib import Path
8
11
  from typing import Optional, Union
9
12
  from urllib.request import urlretrieve
@@ -80,6 +83,29 @@ from flood_adapt.objects.projections.projections import (
80
83
  )
81
84
  from flood_adapt.objects.strategies.strategies import Strategy
82
85
 
86
+ logger = FloodAdaptLogging.getLogger("DatabaseBuilder")
87
+
88
+
89
+ def debug_timer(func):
90
+ @wraps(func)
91
+ def wrapper(*args, **kwargs):
92
+ logger = FloodAdaptLogging.getLogger("DatabaseBuilder") # No forced log level
93
+ if logger.isEnabledFor(logging.DEBUG):
94
+ logger.debug(f"Started '{func.__name__}'")
95
+ start_time = time.perf_counter()
96
+
97
+ result = func(*args, **kwargs)
98
+
99
+ end_time = time.perf_counter()
100
+ elapsed = end_time - start_time
101
+ logger.debug(f"Finished '{func.__name__}' in {elapsed:.4f} seconds")
102
+ else:
103
+ result = func(*args, **kwargs)
104
+
105
+ return result
106
+
107
+ return wrapper
108
+
83
109
 
84
110
  def path_check(str_path: str, config_path: Optional[Path] = None) -> str:
85
111
  """
@@ -243,32 +269,52 @@ class ConfigModel(BaseModel):
243
269
  ----------
244
270
  name : str
245
271
  The name of the site.
246
- description : Optional[str], default ""
272
+ description : Optional[str], default None
247
273
  The description of the site.
248
274
  database_path : Optional[str], default None
249
275
  The path to the database where all the sites are located.
250
- sfincs : str
251
- The SFINCS model path.
252
- sfincs_offshore : Optional[str], default None
253
- The offshore SFINCS model path.
254
- fiat : str
255
- The FIAT model path.
256
276
  unit_system : UnitSystems
257
277
  The unit system.
258
- gui : GuiModel
278
+ gui : GuiConfigModel
259
279
  The GUI model representing scaling values for the layers.
260
- building_footprints : Optional[SpatialJoinModel], default None
261
- The building footprints model.
262
- slr_scenarios : Optional[SlrModelDef], default SlrModelDef()
263
- The sea level rise model.
264
- tide_gauge : Optional[TideGaugeConfigModel], default None
265
- The tide gauge model.
280
+ infographics : Optional[bool], default True
281
+ Indicates if infographics are enabled.
282
+ fiat : str
283
+ The FIAT model path.
284
+ aggregation_areas : Optional[list[SpatialJoinModel]], default None
285
+ The list of aggregation area models.
286
+ building_footprints : Optional[SpatialJoinModel | FootprintsOptions], default FootprintsOptions.OSM
287
+ The building footprints model or OSM option.
288
+ fiat_buildings_name : Optional[str], default "buildings"
289
+ The name of the buildings geometry in the FIAT model.
290
+ fiat_roads_name : Optional[str], default "roads"
291
+ The name of the roads geometry in the FIAT model.
266
292
  bfe : Optional[SpatialJoinModel], default None
267
293
  The BFE model.
268
- svi : Optional[SviModel], default None
294
+ svi : Optional[SviConfigModel], default None
269
295
  The SVI model.
270
- road_width : Optional[float], default 2
296
+ road_width : Optional[float], default 5
271
297
  The road width in meters.
298
+ return_periods : list[int], default []
299
+ The list of return periods for risk calculations.
300
+ floodmap_type : Optional[FloodmapType], default None
301
+ The type of floodmap to use.
302
+ references : WaterlevelReferenceModel, default WaterlevelReferenceModel(...)
303
+ The water level reference model.
304
+ sfincs_overland : FloodModel
305
+ The overland SFINCS model.
306
+ sfincs_offshore : Optional[FloodModel], default None
307
+ The offshore SFINCS model.
308
+ dem : Optional[DemModel], default None
309
+ The DEM model.
310
+ excluded_datums : list[str], default []
311
+ List of datums to exclude from plotting.
312
+ slr_scenarios : Optional[SlrScenariosModel], default None
313
+ The sea level rise scenarios model.
314
+ scs : Optional[SCSModel], default None
315
+ The SCS model.
316
+ tide_gauge : Optional[TideGaugeConfigModel], default None
317
+ The tide gauge model.
272
318
  cyclones : Optional[bool], default True
273
319
  Indicates if cyclones are enabled.
274
320
  cyclone_basin : Optional[Basins], default None
@@ -277,8 +323,6 @@ class ConfigModel(BaseModel):
277
323
  The list of observation point models.
278
324
  probabilistic_set : Optional[str], default None
279
325
  The probabilistic set path.
280
- infographics : Optional[bool], default True
281
- Indicates if infographics are enabled.
282
326
  """
283
327
 
284
328
  # General
@@ -295,12 +339,13 @@ class ConfigModel(BaseModel):
295
339
  building_footprints: Optional[SpatialJoinModel | FootprintsOptions] = (
296
340
  FootprintsOptions.OSM
297
341
  )
298
- fiat_buildings_name: Optional[str] = "buildings"
342
+ fiat_buildings_name: str | list[str] = "buildings"
299
343
  fiat_roads_name: Optional[str] = "roads"
300
344
  bfe: Optional[SpatialJoinModel] = None
301
345
  svi: Optional[SviConfigModel] = None
302
346
  road_width: Optional[float] = 5
303
347
  return_periods: list[int] = Field(default_factory=list)
348
+ floodmap_type: Optional[FloodmapType] = None
304
349
 
305
350
  # SFINCS
306
351
  references: WaterlevelReferenceModel = WaterlevelReferenceModel(
@@ -379,8 +424,6 @@ class ConfigModel(BaseModel):
379
424
 
380
425
 
381
426
  class DatabaseBuilder:
382
- logger = FloodAdaptLogging.getLogger("DatabaseBuilder")
383
-
384
427
  _has_roads: bool = False
385
428
  _aggregation_areas: Optional[list] = None
386
429
  _probabilistic_set_name: Optional[str] = None
@@ -406,6 +449,7 @@ class DatabaseBuilder:
406
449
  def static_path(self) -> Path:
407
450
  return self.root / "static"
408
451
 
452
+ @debug_timer
409
453
  def build(self, overwrite: bool = False) -> None:
410
454
  # Check if database already exists
411
455
  if self.root.exists() and not overwrite:
@@ -423,9 +467,7 @@ class DatabaseBuilder:
423
467
  with FloodAdaptLogging.to_file(
424
468
  file_path=self.root.joinpath("database_builder.log")
425
469
  ):
426
- self.logger.info(
427
- f"Creating a FloodAdapt database in '{self.root.as_posix()}'"
428
- )
470
+ logger.info(f"Creating a FloodAdapt database in '{self.root.as_posix()}'")
429
471
 
430
472
  # Make folder structure and read models
431
473
  self.setup()
@@ -441,8 +483,9 @@ class DatabaseBuilder:
441
483
  self.create_standard_objects()
442
484
 
443
485
  # Save log file
444
- self.logger.info("FloodAdapt database creation finished!")
486
+ logger.info("FloodAdapt database creation finished!")
445
487
 
488
+ @debug_timer
446
489
  def setup(self) -> None:
447
490
  # Create the models
448
491
  self.make_folder_structure()
@@ -452,6 +495,7 @@ class DatabaseBuilder:
452
495
  self.read_template_sfincs_overland_model()
453
496
  self.read_template_sfincs_offshore_model()
454
497
 
498
+ @debug_timer
455
499
  def set_standard_objects(self):
456
500
  # Define name and create object
457
501
  self._no_measures_strategy_name = "no_measures"
@@ -467,14 +511,13 @@ class DatabaseBuilder:
467
511
  )
468
512
  return std_obj
469
513
 
514
+ @debug_timer
470
515
  def create_standard_objects(self):
471
516
  with modified_environ(
472
517
  DATABASE_ROOT=str(self.root.parent),
473
518
  DATABASE_NAME=self.root.name,
474
519
  ):
475
- self.logger.info(
476
- "Creating `no measures` strategy and `current` projection."
477
- )
520
+ logger.info("Creating `no measures` strategy and `current` projection.")
478
521
  # Create database instance
479
522
  db = Database(self.root.parent, self.config.name)
480
523
  # Create no measures strategy
@@ -506,6 +549,7 @@ class DatabaseBuilder:
506
549
  )
507
550
 
508
551
  ### TEMPLATE READERS ###
552
+ @debug_timer
509
553
  def read_template_fiat_model(self):
510
554
  user_provided = self._check_exists_and_absolute(self.config.fiat)
511
555
 
@@ -543,6 +587,7 @@ class DatabaseBuilder:
543
587
 
544
588
  self.fiat_model = in_db
545
589
 
590
+ @debug_timer
546
591
  def read_template_sfincs_overland_model(self):
547
592
  user_provided = self._check_exists_and_absolute(
548
593
  self.config.sfincs_overland.name
@@ -560,6 +605,7 @@ class DatabaseBuilder:
560
605
  in_db.read()
561
606
  self.sfincs_overland_model = in_db
562
607
 
608
+ @debug_timer
563
609
  def read_template_sfincs_offshore_model(self):
564
610
  if self.config.sfincs_offshore is None:
565
611
  self.sfincs_offshore_model = None
@@ -582,6 +628,7 @@ class DatabaseBuilder:
582
628
  self.sfincs_offshore_model = in_db
583
629
 
584
630
  ### FIAT ###
631
+ @debug_timer
585
632
  def create_fiat_model(self) -> FiatModel:
586
633
  fiat = FiatModel(
587
634
  config=self.create_fiat_config(),
@@ -590,17 +637,18 @@ class DatabaseBuilder:
590
637
  )
591
638
  return fiat
592
639
 
640
+ @debug_timer
593
641
  def create_risk_model(self) -> Optional[RiskModel]:
594
642
  # Check if return periods are provided
595
643
  if not self.config.return_periods:
596
644
  if self._probabilistic_set_name:
597
645
  risk = RiskModel()
598
- self.logger.warning(
646
+ logger.warning(
599
647
  f"No return periods provided, but a probabilistic set is available. Using default return periods {risk.return_periods}."
600
648
  )
601
649
  return risk
602
650
  else:
603
- self.logger.warning(
651
+ logger.warning(
604
652
  "No return periods provided and no probabilistic set available. Risk calculations will not be performed."
605
653
  )
606
654
  return None
@@ -608,9 +656,10 @@ class DatabaseBuilder:
608
656
  risk = RiskModel(return_periods=self.config.return_periods)
609
657
  return risk
610
658
 
659
+ @debug_timer
611
660
  def create_benefit_config(self) -> Optional[BenefitsModel]:
612
661
  if self._probabilistic_set_name is None:
613
- self.logger.warning(
662
+ logger.warning(
614
663
  "No probabilistic set found in the config, benefits will not be available."
615
664
  )
616
665
  return None
@@ -621,14 +670,10 @@ class DatabaseBuilder:
621
670
  event_set=self._probabilistic_set_name,
622
671
  )
623
672
 
673
+ @debug_timer
624
674
  def create_fiat_config(self) -> FiatConfigModel:
625
675
  # Make sure only csv objects have geometries
626
- for i, geoms in enumerate(self.fiat_model.exposure.exposure_geoms):
627
- keep = geoms[_FIAT_COLUMNS.object_id].isin(
628
- self.fiat_model.exposure.exposure_db[_FIAT_COLUMNS.object_id]
629
- )
630
- geoms = geoms[keep].reset_index(drop=True)
631
- self.fiat_model.exposure.exposure_geoms[i] = geoms
676
+ self._delete_extra_geometries()
632
677
 
633
678
  footprints = self.create_footprints()
634
679
  if footprints is not None:
@@ -642,9 +687,16 @@ class DatabaseBuilder:
642
687
  self._aggregation_areas = self.create_aggregation_areas()
643
688
 
644
689
  roads_gpkg = self.create_roads()
645
- non_building_names = []
646
- if roads_gpkg is not None:
647
- non_building_names.append("road")
690
+
691
+ # Get classes of non-building objects
692
+ non_buildings = ~self.fiat_model.exposure.exposure_db[
693
+ _FIAT_COLUMNS.object_id
694
+ ].isin(self._get_fiat_building_geoms()[_FIAT_COLUMNS.object_id])
695
+ non_building_names = list(
696
+ self.fiat_model.exposure.exposure_db[_FIAT_COLUMNS.primary_object_type][
697
+ non_buildings
698
+ ].unique()
699
+ )
648
700
 
649
701
  # Update elevations
650
702
  self.update_fiat_elevation()
@@ -676,11 +728,18 @@ class DatabaseBuilder:
676
728
  self.fiat_model.config["exposure"]["geom"][key]
677
729
  ).name
678
730
  self.fiat_model.config["output"]["geom"] = output_geom
731
+ # Make sure objects are ordered based on object id
732
+ self.fiat_model.exposure.exposure_db = (
733
+ self.fiat_model.exposure.exposure_db.sort_values(
734
+ by=[_FIAT_COLUMNS.object_id], ignore_index=True
735
+ )
736
+ )
679
737
  # Update FIAT model with the new config
680
738
  self.fiat_model.write()
681
739
 
682
740
  return config
683
741
 
742
+ @debug_timer
684
743
  def update_fiat_elevation(self):
685
744
  """
686
745
  Update the ground elevations of FIAT objects based on the SFINCS ground elevation map.
@@ -691,7 +750,7 @@ class DatabaseBuilder:
691
750
  dem_file = self._dem_path
692
751
  # TODO resolve issue with double geometries in hydromt-FIAT and use update_ground_elevation method instead
693
752
  # self.fiat_model.update_ground_elevation(dem_file, grnd_elev_unit="meters")
694
- self.logger.info(
753
+ logger.info(
695
754
  "Updating FIAT objects ground elevations from SFINCS ground elevation map."
696
755
  )
697
756
  SFINCS_units = us.UnitfulLength(
@@ -701,88 +760,60 @@ class DatabaseBuilder:
701
760
  conversion_factor = SFINCS_units.convert(FIAT_units)
702
761
 
703
762
  if not math.isclose(conversion_factor, 1):
704
- self.logger.info(
763
+ logger.info(
705
764
  f"Ground elevation for FIAT objects is in '{FIAT_units}', while SFINCS ground elevation is in 'meters'. Values in the exposure csv will be converted by a factor of {conversion_factor}"
706
765
  )
707
766
 
708
767
  exposure = self.fiat_model.exposure.exposure_db
709
768
  dem = rxr.open_rasterio(dem_file)
710
- # TODO make sure only fiat_model object changes take place!
711
- if self.config.fiat_roads_name in self.fiat_model.exposure.geom_names:
712
- roads = self.fiat_model.exposure.exposure_geoms[
713
- self._get_fiat_road_index()
714
- ].to_crs(dem.spatial_ref.crs_wkt)
715
- roads["centroid"] = roads.geometry.centroid # get centroids
716
-
717
- x_points = xr.DataArray(roads["centroid"].x, dims="points")
718
- y_points = xr.DataArray(roads["centroid"].y, dims="points")
719
- roads["elev"] = (
720
- dem.sel(x=x_points, y=y_points, band=1, method="nearest").to_numpy()
721
- * conversion_factor
722
- )
723
-
724
- exposure.loc[
725
- exposure[_FIAT_COLUMNS.primary_object_type] == "road",
726
- _FIAT_COLUMNS.ground_floor_height,
727
- ] = 0
728
- exposure = exposure.merge(
729
- roads[[_FIAT_COLUMNS.object_id, "elev"]],
730
- on=_FIAT_COLUMNS.object_id,
731
- how="left",
732
- )
733
- exposure.loc[
734
- exposure[_FIAT_COLUMNS.primary_object_type] == "road",
735
- _FIAT_COLUMNS.ground_elevation,
736
- ] = exposure.loc[
737
- exposure[_FIAT_COLUMNS.primary_object_type] == "road", "elev"
738
- ]
739
- del exposure["elev"]
740
- self.fiat_model.exposure.exposure_db = exposure
741
-
742
- buildings = self.fiat_model.exposure.exposure_geoms[
743
- self._get_fiat_building_index()
744
- ].to_crs(dem.spatial_ref.crs_wkt)
745
- buildings["geometry"] = buildings.geometry.centroid
746
- x_points = xr.DataArray(buildings["geometry"].x, dims="points")
747
- y_points = xr.DataArray(buildings["geometry"].y, dims="points")
748
- buildings["elev"] = (
769
+
770
+ gdf = self._get_fiat_gdf_full()
771
+ gdf["centroid"] = gdf.geometry.centroid
772
+ x_points = xr.DataArray(gdf["centroid"].x, dims="points")
773
+ y_points = xr.DataArray(gdf["centroid"].y, dims="points")
774
+ gdf["elev"] = (
749
775
  dem.sel(x=x_points, y=y_points, band=1, method="nearest").to_numpy()
750
776
  * conversion_factor
751
777
  )
778
+
752
779
  exposure = exposure.merge(
753
- buildings[[_FIAT_COLUMNS.object_id, "elev"]],
780
+ gdf[[_FIAT_COLUMNS.object_id, "elev"]],
754
781
  on=_FIAT_COLUMNS.object_id,
755
782
  how="left",
756
783
  )
757
- exposure.loc[
758
- exposure[_FIAT_COLUMNS.primary_object_type] != "road",
759
- _FIAT_COLUMNS.ground_elevation,
760
- ] = exposure.loc[exposure[_FIAT_COLUMNS.primary_object_type] != "road", "elev"]
784
+ exposure[_FIAT_COLUMNS.ground_elevation] = exposure["elev"]
761
785
  del exposure["elev"]
762
786
 
787
+ self.fiat_model.exposure.exposure_db = exposure
788
+
763
789
  def read_damage_unit(self) -> str:
764
790
  if self.fiat_model.exposure.damage_unit is not None:
765
791
  return self.fiat_model.exposure.damage_unit
766
792
  else:
767
- self.logger.warning(
793
+ logger.warning(
768
794
  "Delft-FIAT model was missing damage units so '$' was assumed."
769
795
  )
770
796
  return "$"
771
797
 
798
+ @debug_timer
772
799
  def read_floodmap_type(self) -> FloodmapType:
773
- # If there is at least on object that uses the area method, use water depths for FA calcs
774
- if (
775
- self.fiat_model.exposure.exposure_db[_FIAT_COLUMNS.extraction_method]
776
- == "area"
777
- ).any():
778
- return FloodmapType.water_depth
800
+ if self.config.floodmap_type is not None:
801
+ return self.config.floodmap_type
779
802
  else:
780
- return FloodmapType.water_level
803
+ # If there is at least on object that uses the area method, use water depths for FA calcs
804
+ if (
805
+ self.fiat_model.exposure.exposure_db[_FIAT_COLUMNS.extraction_method]
806
+ == "area"
807
+ ).any():
808
+ return FloodmapType.water_depth
809
+ else:
810
+ return FloodmapType.water_level
781
811
 
812
+ @debug_timer
782
813
  def create_roads(self) -> Optional[str]:
783
814
  # Make sure that FIAT roads are polygons
784
815
  if self.config.fiat_roads_name not in self.fiat_model.exposure.geom_names:
785
- self.logger.warning(
816
+ logger.warning(
786
817
  "Road objects are not available in the FIAT model and thus would not be available in FloodAdapt."
787
818
  )
788
819
  # TODO check how this naming of output geoms should become more explicit!
@@ -795,7 +826,7 @@ class DatabaseBuilder:
795
826
  _FIAT_COLUMNS.segment_length
796
827
  not in self.fiat_model.exposure.exposure_db.columns
797
828
  ):
798
- self.logger.warning(
829
+ logger.warning(
799
830
  f"'{_FIAT_COLUMNS.segment_length}' column not present in the FIAT exposure csv. Road impact infometrics cannot be produced."
800
831
  )
801
832
 
@@ -807,16 +838,18 @@ class DatabaseBuilder:
807
838
  )
808
839
  roads = roads.to_crs(self.fiat_model.exposure.crs)
809
840
  self.fiat_model.exposure.exposure_geoms[self._get_fiat_road_index()] = roads
810
- self.logger.info(
841
+ logger.info(
811
842
  f"FIAT road objects transformed from lines to polygons assuming a road width of {self.config.road_width} meters."
812
843
  )
813
844
 
814
845
  self._has_roads = True
815
846
  return f"{self.config.fiat_roads_name}.gpkg"
816
847
 
848
+ @debug_timer
817
849
  def create_new_developments(self) -> Optional[str]:
818
850
  return "new_development_area.gpkg"
819
851
 
852
+ @debug_timer
820
853
  def create_footprints(self) -> Optional[Path]:
821
854
  if isinstance(self.config.building_footprints, SpatialJoinModel):
822
855
  # Use the provided building footprints
@@ -824,7 +857,7 @@ class DatabaseBuilder:
824
857
  self.config.building_footprints.file
825
858
  )
826
859
 
827
- self.logger.info(
860
+ logger.info(
828
861
  f"Using building footprints from {Path(building_footprints_file).as_posix()}."
829
862
  )
830
863
  # Spatially join buildings and map
@@ -835,7 +868,7 @@ class DatabaseBuilder:
835
868
  )
836
869
  return path
837
870
  elif self.config.building_footprints == FootprintsOptions.OSM:
838
- self.logger.info(
871
+ logger.info(
839
872
  "Building footprint data will be downloaded from Open Street Maps."
840
873
  )
841
874
  region = self.fiat_model.region
@@ -857,12 +890,10 @@ class DatabaseBuilder:
857
890
  return path
858
891
  # Then check if geometries are already footprints
859
892
  elif isinstance(
860
- self.fiat_model.exposure.exposure_geoms[
861
- self._get_fiat_building_index()
862
- ].geometry.iloc[0],
893
+ self._get_fiat_building_geoms().geometry.iloc[0],
863
894
  (Polygon, MultiPolygon),
864
895
  ):
865
- self.logger.info(
896
+ logger.info(
866
897
  "Building footprints are already available in the FIAT model geometry files."
867
898
  )
868
899
  return None
@@ -885,21 +916,20 @@ class DatabaseBuilder:
885
916
  f"While 'BF_FID' column exists, building footprints file {footprints_path} not found."
886
917
  )
887
918
 
888
- self.logger.info(
889
- f"Using the building footprints located at {footprints_path}."
890
- )
919
+ logger.info(f"Using the building footprints located at {footprints_path}.")
891
920
  return footprints_path.relative_to(self.static_path)
892
921
 
893
922
  # Other methods
894
923
  else:
895
- self.logger.warning(
924
+ logger.warning(
896
925
  "No building footprints are available. Buildings will be plotted with a default shape in FloodAdapt."
897
926
  )
898
927
  return None
899
928
 
929
+ @debug_timer
900
930
  def create_bfe(self) -> Optional[BFEModel]:
901
931
  if self.config.bfe is None:
902
- self.logger.warning(
932
+ logger.warning(
903
933
  "No base flood elevation provided. Elevating building relative to base flood elevation will not be possible in FloodAdapt."
904
934
  )
905
935
  return None
@@ -907,13 +937,13 @@ class DatabaseBuilder:
907
937
  # TODO can we use hydromt-FIAT?
908
938
  bfe_file = self._check_exists_and_absolute(self.config.bfe.file)
909
939
 
910
- self.logger.info(
940
+ logger.info(
911
941
  f"Using map from {Path(bfe_file).as_posix()} as base flood elevation."
912
942
  )
913
943
 
914
944
  # Spatially join buildings and map
915
945
  buildings_joined, bfe = self.spatial_join(
916
- self.fiat_model.exposure.exposure_geoms[self._get_fiat_building_index()],
946
+ self._get_fiat_building_geoms(),
917
947
  bfe_file,
918
948
  self.config.bfe.field_name,
919
949
  )
@@ -940,6 +970,7 @@ class DatabaseBuilder:
940
970
  field_name=self.config.bfe.field_name,
941
971
  )
942
972
 
973
+ @debug_timer
943
974
  def create_aggregation_areas(self) -> list[AggregationModel]:
944
975
  # TODO split this to 3 methods?
945
976
  aggregation_areas = []
@@ -982,7 +1013,7 @@ class DatabaseBuilder:
982
1013
  )
983
1014
  aggregation_areas.append(aggr)
984
1015
 
985
- self.logger.info(
1016
+ logger.info(
986
1017
  f"Aggregation areas: {aggr.name} from the FIAT model are going to be used."
987
1018
  )
988
1019
 
@@ -1002,10 +1033,9 @@ class DatabaseBuilder:
1002
1033
  )
1003
1034
  # Do spatial join of FIAT objects and aggregation areas
1004
1035
  exposure_csv = self.fiat_model.exposure.exposure_db
1005
- buildings_joined, aggr_areas = self.spatial_join(
1006
- objects=self.fiat_model.exposure.exposure_geoms[
1007
- self._get_fiat_building_index()
1008
- ],
1036
+ gdf = self._get_fiat_gdf_full()
1037
+ gdf_joined, aggr_areas = self.spatial_join(
1038
+ objects=gdf[[_FIAT_COLUMNS.object_id, "geometry"]],
1009
1039
  layer=str(self._check_exists_and_absolute(aggr.file)),
1010
1040
  field_name=aggr.field_name,
1011
1041
  rename=_FIAT_COLUMNS.aggregation_label.format(name=aggr_name),
@@ -1016,7 +1046,7 @@ class DatabaseBuilder:
1016
1046
  aggr_path.parent.mkdir(parents=True, exist_ok=True)
1017
1047
  aggr_areas.to_file(aggr_path)
1018
1048
  exposure_csv = exposure_csv.merge(
1019
- buildings_joined, on=_FIAT_COLUMNS.object_id, how="left"
1049
+ gdf_joined, on=_FIAT_COLUMNS.object_id, how="left"
1020
1050
  )
1021
1051
  self.fiat_model.exposure.exposure_db = exposure_csv
1022
1052
  # Update spatial joins in FIAT model
@@ -1067,31 +1097,29 @@ class DatabaseBuilder:
1067
1097
  aggregation_areas.append(aggr)
1068
1098
 
1069
1099
  # Add column in FIAT
1070
- buildings_joined, _ = self.spatial_join(
1071
- objects=self.fiat_model.exposure.exposure_geoms[
1072
- self._get_fiat_building_index()
1073
- ],
1100
+ gdf = self._get_fiat_gdf_full()
1101
+ gdf_joined, aggr_areas = self.spatial_join(
1102
+ objects=gdf[[_FIAT_COLUMNS.object_id, "geometry"]],
1074
1103
  layer=region,
1075
1104
  field_name="aggr_id",
1076
1105
  rename=_FIAT_COLUMNS.aggregation_label.format(name="region"),
1077
1106
  )
1078
1107
  exposure_csv = exposure_csv.merge(
1079
- buildings_joined, on=_FIAT_COLUMNS.object_id, how="left"
1108
+ gdf_joined, on=_FIAT_COLUMNS.object_id, how="left"
1080
1109
  )
1081
1110
  self.fiat_model.exposure.exposure_db = exposure_csv
1082
- self.logger.warning(
1111
+ logger.warning(
1083
1112
  "No aggregation areas were available in the FIAT model and none were provided in the config file. The region file will be used as a mock aggregation area."
1084
1113
  )
1085
1114
  return aggregation_areas
1086
1115
 
1116
+ @debug_timer
1087
1117
  def create_svi(self) -> Optional[SVIModel]:
1088
1118
  if self.config.svi:
1089
1119
  svi_file = self._check_exists_and_absolute(self.config.svi.file)
1090
1120
  exposure_csv = self.fiat_model.exposure.exposure_db
1091
1121
  buildings_joined, svi = self.spatial_join(
1092
- self.fiat_model.exposure.exposure_geoms[
1093
- self._get_fiat_building_index()
1094
- ],
1122
+ self._get_fiat_building_geoms(),
1095
1123
  svi_file,
1096
1124
  self.config.svi.field_name,
1097
1125
  rename="SVI",
@@ -1099,12 +1127,12 @@ class DatabaseBuilder:
1099
1127
  )
1100
1128
  # Add column to exposure
1101
1129
  if "SVI" in exposure_csv.columns:
1102
- self.logger.info(
1130
+ logger.info(
1103
1131
  f"'SVI' column in the FIAT exposure csv will be replaced by {svi_file.as_posix()}."
1104
1132
  )
1105
1133
  del exposure_csv["SVI"]
1106
1134
  else:
1107
- self.logger.info(
1135
+ logger.info(
1108
1136
  f"'SVI' column in the FIAT exposure csv will be filled by {svi_file.as_posix()}."
1109
1137
  )
1110
1138
  exposure_csv = exposure_csv.merge(
@@ -1116,7 +1144,7 @@ class DatabaseBuilder:
1116
1144
  svi_path = self.static_path / "templates" / "fiat" / "svi" / "svi.gpkg"
1117
1145
  svi_path.parent.mkdir(parents=True, exist_ok=True)
1118
1146
  svi.to_file(svi_path)
1119
- self.logger.info(
1147
+ logger.info(
1120
1148
  f"An SVI map can be shown in FloodAdapt GUI using '{self.config.svi.field_name}' column from {svi_file.as_posix()}"
1121
1149
  )
1122
1150
 
@@ -1125,19 +1153,17 @@ class DatabaseBuilder:
1125
1153
  field_name="SVI",
1126
1154
  )
1127
1155
  elif "SVI" in self.fiat_model.exposure.exposure_db.columns:
1128
- self.logger.info(
1156
+ logger.info(
1129
1157
  "'SVI' column present in the FIAT exposure csv. Vulnerability type infometrics can be produced."
1130
1158
  )
1131
1159
  add_attrs = self.fiat_model.spatial_joins["additional_attributes"]
1132
1160
  if "SVI" not in [attr["name"] for attr in add_attrs]:
1133
- self.logger.warning(
1134
- "No SVI map found to display in the FloodAdapt GUI!"
1135
- )
1161
+ logger.warning("No SVI map found to display in the FloodAdapt GUI!")
1136
1162
 
1137
1163
  ind = [attr["name"] for attr in add_attrs].index("SVI")
1138
1164
  svi = add_attrs[ind]
1139
1165
  svi_path = self.static_path / "templates" / "fiat" / svi["file"]
1140
- self.logger.info(
1166
+ logger.info(
1141
1167
  f"An SVI map can be shown in FloodAdapt GUI using '{svi['field_name']}' column from {svi['file']}"
1142
1168
  )
1143
1169
  # Save site attributes
@@ -1147,12 +1173,13 @@ class DatabaseBuilder:
1147
1173
  )
1148
1174
 
1149
1175
  else:
1150
- self.logger.warning(
1176
+ logger.warning(
1151
1177
  "'SVI' column not present in the FIAT exposure csv. Vulnerability type infometrics cannot be produced."
1152
1178
  )
1153
1179
  return None
1154
1180
 
1155
1181
  ### SFINCS ###
1182
+ @debug_timer
1156
1183
  def create_sfincs_config(self) -> SfincsModel:
1157
1184
  # call these functions before others to make sure water level references are updated
1158
1185
  config = self.create_sfincs_model_config()
@@ -1172,9 +1199,10 @@ class DatabaseBuilder:
1172
1199
 
1173
1200
  return sfincs
1174
1201
 
1202
+ @debug_timer
1175
1203
  def create_cyclone_track_database(self) -> Optional[CycloneTrackDatabaseModel]:
1176
1204
  if not self.config.cyclones or not self.config.sfincs_offshore:
1177
- self.logger.warning("No cyclones will be available in the database.")
1205
+ logger.warning("No cyclones will be available in the database.")
1178
1206
  return None
1179
1207
 
1180
1208
  if self.config.cyclone_basin:
@@ -1184,7 +1212,7 @@ class DatabaseBuilder:
1184
1212
 
1185
1213
  name = f"IBTrACS.{basin.value}.v04r01.nc"
1186
1214
  url = f"https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/netcdf/{name}"
1187
- self.logger.info(f"Downloading cyclone track database from {url}")
1215
+ logger.info(f"Downloading cyclone track database from {url}")
1188
1216
  fn = Path(self.root) / "static" / "cyclone_track_database" / name
1189
1217
  fn.parent.mkdir(parents=True, exist_ok=True)
1190
1218
 
@@ -1195,6 +1223,7 @@ class DatabaseBuilder:
1195
1223
 
1196
1224
  return CycloneTrackDatabaseModel(file=name)
1197
1225
 
1226
+ @debug_timer
1198
1227
  def create_scs_model(self) -> Optional[SCSModel]:
1199
1228
  if self.config.scs is None:
1200
1229
  return None
@@ -1205,11 +1234,12 @@ class DatabaseBuilder:
1205
1234
 
1206
1235
  return SCSModel(file=scs_file.name, type=self.config.scs.type)
1207
1236
 
1237
+ @debug_timer
1208
1238
  def create_dem_model(self) -> DemModel:
1209
1239
  if self.config.dem:
1210
1240
  subgrid_sfincs = Path(self.config.dem.filename)
1211
1241
  else:
1212
- self.logger.warning(
1242
+ logger.warning(
1213
1243
  "No subgrid depth geotiff file provided in the config file. Using the one from the SFINCS model."
1214
1244
  )
1215
1245
  subgrid_sfincs = (
@@ -1227,7 +1257,7 @@ class DatabaseBuilder:
1227
1257
  shutil.move(tiles_sfincs, fa_tiles_path)
1228
1258
  if (fa_tiles_path / "index").exists():
1229
1259
  os.rename(fa_tiles_path / "index", fa_tiles_path / "indices")
1230
- self.logger.info(
1260
+ logger.info(
1231
1261
  "Tiles were already available in the SFINCS model and will directly be used in FloodAdapt."
1232
1262
  )
1233
1263
  else:
@@ -1239,7 +1269,7 @@ class DatabaseBuilder:
1239
1269
  zoom_range=[0, 13],
1240
1270
  fmt="png",
1241
1271
  )
1242
- self.logger.info(
1272
+ logger.info(
1243
1273
  f"Tiles were created using the {subgrid_sfincs.as_posix()} as the elevation map."
1244
1274
  )
1245
1275
 
@@ -1249,6 +1279,7 @@ class DatabaseBuilder:
1249
1279
  filename=fa_subgrid_path.name, units=us.UnitTypesLength.meters
1250
1280
  ) # always in meters
1251
1281
 
1282
+ @debug_timer
1252
1283
  def create_sfincs_model_config(self) -> SfincsConfigModel:
1253
1284
  config = SfincsConfigModel(
1254
1285
  csname=self.sfincs_overland_model.crs.name,
@@ -1263,6 +1294,7 @@ class DatabaseBuilder:
1263
1294
 
1264
1295
  return config
1265
1296
 
1297
+ @debug_timer
1266
1298
  def create_slr(self) -> Optional[SlrScenariosModel]:
1267
1299
  if self.config.slr_scenarios is None:
1268
1300
  return None
@@ -1280,17 +1312,19 @@ class DatabaseBuilder:
1280
1312
  relative_to_year=self.config.slr_scenarios.relative_to_year,
1281
1313
  )
1282
1314
 
1315
+ @debug_timer
1283
1316
  def create_observation_points(self) -> Union[list[ObsPointModel], None]:
1284
1317
  if self.config.obs_point is None:
1285
1318
  return None
1286
1319
 
1287
- self.logger.info("Observation points were provided in the config file.")
1320
+ logger.info("Observation points were provided in the config file.")
1288
1321
  return self.config.obs_point
1289
1322
 
1323
+ @debug_timer
1290
1324
  def create_rivers(self) -> list[RiverModel]:
1291
1325
  src_file = Path(self.sfincs_overland_model.root) / "sfincs.src"
1292
1326
  if not src_file.exists():
1293
- self.logger.warning("No rivers found in the SFINCS model.")
1327
+ logger.warning("No rivers found in the SFINCS model.")
1294
1328
  return []
1295
1329
 
1296
1330
  df = pd.read_csv(src_file, delim_whitespace=True, header=None, names=["x", "y"])
@@ -1310,7 +1344,7 @@ class DatabaseBuilder:
1310
1344
  )
1311
1345
  else:
1312
1346
  discharge = 0
1313
- self.logger.warning(
1347
+ logger.warning(
1314
1348
  f"No river discharge conditions were found in the SFINCS model for river {idx}. A default value of 0 will be used."
1315
1349
  )
1316
1350
 
@@ -1324,18 +1358,19 @@ class DatabaseBuilder:
1324
1358
  )
1325
1359
  rivers.append(river)
1326
1360
 
1327
- self.logger.info(
1361
+ logger.info(
1328
1362
  f"{len(river_locs)} river(s) were identified from the SFINCS model and will be available in FloodAdapt for discharge input."
1329
1363
  )
1330
1364
 
1331
1365
  return rivers
1332
1366
 
1367
+ @debug_timer
1333
1368
  def create_tide_gauge(self) -> Optional[TideGauge]:
1334
1369
  if self.config.tide_gauge is None:
1335
- self.logger.warning(
1370
+ logger.warning(
1336
1371
  "Tide gauge information not provided. Historical events will not have an option to use gauged data in FloodAdapt!"
1337
1372
  )
1338
- self.logger.warning(
1373
+ logger.warning(
1339
1374
  "No water level references were found. It is assumed that MSL is equal to the datum used in the SFINCS overland model. You can provide these values with the tide_gauge.ref attribute in the site.toml."
1340
1375
  )
1341
1376
  return None
@@ -1346,7 +1381,7 @@ class DatabaseBuilder:
1346
1381
  "Tide gauge file needs to be provided when 'file' is selected as the source."
1347
1382
  )
1348
1383
  if self.config.tide_gauge.ref is None:
1349
- self.logger.warning(
1384
+ logger.warning(
1350
1385
  "Tide gauge reference not provided. MSL is assumed as the reference of the water levels in the file."
1351
1386
  )
1352
1387
  self.config.tide_gauge.ref = "MSL"
@@ -1360,7 +1395,7 @@ class DatabaseBuilder:
1360
1395
  shutil.copyfile(self.config.tide_gauge.file, db_file_path)
1361
1396
 
1362
1397
  rel_db_path = Path(db_file_path.relative_to(self.static_path))
1363
- self.logger.warning(
1398
+ logger.warning(
1364
1399
  f"Tide gauge from file {rel_db_path} assumed to be in {self.unit_system.default_length_units}!"
1365
1400
  )
1366
1401
  tide_gauge = TideGauge(
@@ -1387,12 +1422,12 @@ class DatabaseBuilder:
1387
1422
 
1388
1423
  if self.config.tide_gauge.id is None:
1389
1424
  station_id = self._get_closest_station()
1390
- self.logger.info(
1425
+ logger.info(
1391
1426
  "The closest NOAA tide gauge station to the site will be searched."
1392
1427
  )
1393
1428
  else:
1394
1429
  station_id = self.config.tide_gauge.id
1395
- self.logger.info(
1430
+ logger.info(
1396
1431
  f"The NOAA tide gauge station with the provided ID {station_id} will be used."
1397
1432
  )
1398
1433
  station = self._get_station_metadata(station_id=station_id, ref=ref)
@@ -1448,11 +1483,12 @@ class DatabaseBuilder:
1448
1483
  self.water_level_references.datums.append(wl_info)
1449
1484
  return tide_gauge
1450
1485
  else:
1451
- self.logger.warning(
1486
+ logger.warning(
1452
1487
  f"Tide gauge source not recognized: {self.config.tide_gauge.source}. Historical events will not have an option to use gauged data in FloodAdapt!"
1453
1488
  )
1454
1489
  return None
1455
1490
 
1491
+ @debug_timer
1456
1492
  def create_offshore_model(self) -> Optional[FloodModel]:
1457
1493
  if self.sfincs_offshore_model is None:
1458
1494
  return None
@@ -1477,7 +1513,7 @@ class DatabaseBuilder:
1477
1513
  index=False,
1478
1514
  header=False,
1479
1515
  )
1480
- self.logger.info(
1516
+ logger.info(
1481
1517
  "Output points of the offshore SFINCS model were reconfigured to the boundary points of the overland SFINCS model."
1482
1518
  )
1483
1519
 
@@ -1487,6 +1523,7 @@ class DatabaseBuilder:
1487
1523
  vertical_offset=self.config.sfincs_offshore.vertical_offset,
1488
1524
  )
1489
1525
 
1526
+ @debug_timer
1490
1527
  def create_overland_model(self) -> FloodModel:
1491
1528
  return FloodModel(
1492
1529
  name="overland",
@@ -1494,6 +1531,7 @@ class DatabaseBuilder:
1494
1531
  )
1495
1532
 
1496
1533
  ### SITE ###
1534
+ @debug_timer
1497
1535
  def create_site_config(self) -> Site:
1498
1536
  """Create the site configuration for the FloodAdapt model.
1499
1537
 
@@ -1533,18 +1571,16 @@ class DatabaseBuilder:
1533
1571
  )
1534
1572
  return config
1535
1573
 
1574
+ @debug_timer
1536
1575
  def read_location(self) -> tuple[float, float]:
1537
1576
  # Get center of area of interest
1538
1577
  if not self.fiat_model.region.empty:
1539
1578
  center = self.fiat_model.region.dissolve().centroid.to_crs(4326)[0]
1540
1579
  else:
1541
- center = (
1542
- self.fiat_model.exposure.exposure_geoms[self._get_fiat_building_index()]
1543
- .dissolve()
1544
- .centroid.to_crs(4326)[0]
1545
- )
1580
+ center = self._get_fiat_building_geoms().dissolve().centroid.to_crs(4326)[0]
1546
1581
  return center.x, center.y
1547
1582
 
1583
+ @debug_timer
1548
1584
  def create_gui_config(self) -> GuiModel:
1549
1585
  gui = GuiModel(
1550
1586
  units=self.unit_system,
@@ -1555,6 +1591,7 @@ class DatabaseBuilder:
1555
1591
 
1556
1592
  return gui
1557
1593
 
1594
+ @debug_timer
1558
1595
  def create_default_units(self) -> GuiUnitModel:
1559
1596
  if self.config.unit_system == UnitSystems.imperial:
1560
1597
  return GuiUnitModel.imperial()
@@ -1565,6 +1602,7 @@ class DatabaseBuilder:
1565
1602
  f"Unit system {self.config.unit_system} not recognized. Please choose 'imperial' or 'metric'."
1566
1603
  )
1567
1604
 
1605
+ @debug_timer
1568
1606
  def create_visualization_layers(self) -> VisualizationLayers:
1569
1607
  visualization_layers = VisualizationLayers()
1570
1608
  if self._svi is not None:
@@ -1578,6 +1616,7 @@ class DatabaseBuilder:
1578
1616
  )
1579
1617
  return visualization_layers
1580
1618
 
1619
+ @debug_timer
1581
1620
  def create_output_layers_config(self) -> OutputLayers:
1582
1621
  # Read default colors from template
1583
1622
  fd_max = self.config.gui.max_flood_depth
@@ -1633,6 +1672,7 @@ class DatabaseBuilder:
1633
1672
  )
1634
1673
  return output_layers
1635
1674
 
1675
+ @debug_timer
1636
1676
  def create_hazard_plotting_config(self) -> PlottingModel:
1637
1677
  datum_names = [datum.name for datum in self.water_level_references.datums]
1638
1678
  if "MHHW" in datum_names:
@@ -1640,20 +1680,20 @@ class DatabaseBuilder:
1640
1680
  self.water_level_references.get_datum("MHHW").height
1641
1681
  - self.water_level_references.get_datum("MSL").height
1642
1682
  )
1643
- self.logger.info(
1683
+ logger.info(
1644
1684
  f"The default tidal amplitude in the GUI will be {amplitude.transform(self.unit_system.default_length_units)}, calculated as the difference between MHHW and MSL from the tide gauge data."
1645
1685
  )
1646
1686
  else:
1647
1687
  amplitude = us.UnitfulLength(
1648
1688
  value=0.0, units=self.unit_system.default_length_units
1649
1689
  )
1650
- self.logger.warning(
1690
+ logger.warning(
1651
1691
  "The default tidal amplitude in the GUI will be 0.0, since no tide-gauge water levels are available. You can change this in the site.toml with the 'gui.tide_harmonic_amplitude' attribute."
1652
1692
  )
1653
1693
 
1654
1694
  ref = "MSL"
1655
1695
  if ref not in datum_names:
1656
- self.logger.warning(
1696
+ logger.warning(
1657
1697
  f"The Mean Sea Level (MSL) datum is not available in the site.toml. The synthetic tide will be created relative to the main reference: {self.water_level_references.reference}."
1658
1698
  )
1659
1699
  ref = self.water_level_references.reference
@@ -1668,6 +1708,7 @@ class DatabaseBuilder:
1668
1708
 
1669
1709
  return plotting
1670
1710
 
1711
+ @debug_timer
1671
1712
  def create_infometrics(self):
1672
1713
  """
1673
1714
  Copy the infometrics and infographics templates to the appropriate location and modifies the metrics_config.toml files.
@@ -1728,6 +1769,7 @@ class DatabaseBuilder:
1728
1769
  with open(file, "wb") as f:
1729
1770
  tomli_w.dump(attrs, f)
1730
1771
 
1772
+ @debug_timer
1731
1773
  def _create_optional_infometrics(self, templates_path: Path, path_im: Path):
1732
1774
  # If infographics are going to be created in FA, get template metric configurations
1733
1775
  if not self.config.infographics:
@@ -1736,14 +1778,10 @@ class DatabaseBuilder:
1736
1778
  # Check what type of infographics should be used
1737
1779
  if self.config.unit_system == UnitSystems.imperial:
1738
1780
  metrics_folder_name = "US_NSI"
1739
- self.logger.info(
1740
- "Default NSI infometrics and infographics will be created."
1741
- )
1781
+ logger.info("Default NSI infometrics and infographics will be created.")
1742
1782
  elif self.config.unit_system == UnitSystems.metric:
1743
1783
  metrics_folder_name = "OSM"
1744
- self.logger.info(
1745
- "Default OSM infometrics and infographics will be created."
1746
- )
1784
+ logger.info("Default OSM infometrics and infographics will be created.")
1747
1785
  else:
1748
1786
  raise ValueError(
1749
1787
  f"Unit system {self.config.unit_system} is not recognized. Please choose 'imperial' or 'metric'."
@@ -1790,6 +1828,7 @@ class DatabaseBuilder:
1790
1828
  path_1 = self.root.joinpath("static", "templates", "infographics", "images")
1791
1829
  shutil.copytree(path_0, path_1)
1792
1830
 
1831
+ @debug_timer
1793
1832
  def add_static_files(self):
1794
1833
  """
1795
1834
  Copy static files from the 'templates' folder to the 'static' folder.
@@ -1804,10 +1843,11 @@ class DatabaseBuilder:
1804
1843
  path_1 = self.static_path / folder
1805
1844
  shutil.copytree(path_0, path_1)
1806
1845
 
1846
+ @debug_timer
1807
1847
  def add_probabilistic_set(self):
1808
1848
  # Copy prob set if given
1809
1849
  if self.config.probabilistic_set:
1810
- self.logger.info(
1850
+ logger.info(
1811
1851
  f"Probabilistic event set imported from {self.config.probabilistic_set}"
1812
1852
  )
1813
1853
  prob_event_name = Path(self.config.probabilistic_set).name
@@ -1815,7 +1855,7 @@ class DatabaseBuilder:
1815
1855
  shutil.copytree(self.config.probabilistic_set, path_db)
1816
1856
  self._probabilistic_set_name = prob_event_name
1817
1857
  else:
1818
- self.logger.warning(
1858
+ logger.warning(
1819
1859
  "Probabilistic event set not provided. Risk scenarios cannot be run in FloodAdapt."
1820
1860
  )
1821
1861
  self._probabilistic_set_name = None
@@ -1829,7 +1869,7 @@ class DatabaseBuilder:
1829
1869
  the input and static folders. It also creates subfolders within the input and
1830
1870
  static folders based on a predefined list of names.
1831
1871
  """
1832
- self.logger.info("Preparing the database folder structure.")
1872
+ logger.info("Preparing the database folder structure.")
1833
1873
  inputs = [
1834
1874
  "events",
1835
1875
  "projections",
@@ -1856,6 +1896,23 @@ class DatabaseBuilder:
1856
1896
  else:
1857
1897
  raise ValueError(f"Path {path} is not absolute.")
1858
1898
 
1899
+ def _get_fiat_building_geoms(self) -> gpd.GeoDataFrame:
1900
+ """
1901
+ Get the building geometries from the FIAT model.
1902
+
1903
+ Returns
1904
+ -------
1905
+ gpd.GeoDataFrame
1906
+ A GeoDataFrame containing the building geometries.
1907
+ """
1908
+ building_indices = self._get_fiat_building_index()
1909
+ buildings = pd.concat(
1910
+ [self.fiat_model.exposure.exposure_geoms[i] for i in building_indices],
1911
+ ignore_index=True,
1912
+ )
1913
+ return buildings
1914
+
1915
+ @debug_timer
1859
1916
  def _join_building_footprints(
1860
1917
  self, building_footprints: gpd.GeoDataFrame, field_name: str
1861
1918
  ) -> Path:
@@ -1878,12 +1935,10 @@ class DatabaseBuilder:
1878
1935
  7. Updates the site attributes with the relative path to the saved building footprints.
1879
1936
  8. Logs the location where the building footprints are saved.
1880
1937
  """
1881
- buildings = self.fiat_model.exposure.exposure_geoms[
1882
- self._get_fiat_building_index()
1883
- ]
1938
+ buildings = self._get_fiat_building_geoms()
1884
1939
  exposure_csv = self.fiat_model.exposure.exposure_db
1885
1940
  if "BF_FID" in exposure_csv.columns:
1886
- self.logger.warning(
1941
+ logger.warning(
1887
1942
  "Column 'BF_FID' already exists in the exposure columns and will be replaced."
1888
1943
  )
1889
1944
  del exposure_csv["BF_FID"]
@@ -1919,12 +1974,13 @@ class DatabaseBuilder:
1919
1974
 
1920
1975
  # Save site attributes
1921
1976
  buildings_path = geo_path.relative_to(self.static_path)
1922
- self.logger.info(
1977
+ logger.info(
1923
1978
  f"Building footprints saved at {(self.static_path / buildings_path).resolve().as_posix()}"
1924
1979
  )
1925
1980
 
1926
1981
  return buildings_path
1927
1982
 
1983
+ @debug_timer
1928
1984
  def _clip_hazard_extend(self, clip_footprints=True):
1929
1985
  """
1930
1986
  Clip the exposure data to the bounding box of the hazard data.
@@ -1943,9 +1999,8 @@ class DatabaseBuilder:
1943
1999
  -------
1944
2000
  None
1945
2001
  """
1946
- gdf = self.fiat_model.exposure.get_full_gdf(
1947
- self.fiat_model.exposure.exposure_db
1948
- )
2002
+ gdf = self._get_fiat_gdf_full()
2003
+
1949
2004
  crs = gdf.crs
1950
2005
  sfincs_extend = self.sfincs_overland_model.region
1951
2006
  sfincs_extend = sfincs_extend.to_crs(crs)
@@ -1955,55 +2010,21 @@ class DatabaseBuilder:
1955
2010
  self.fiat_model.geoms["region"] = clipped_region
1956
2011
 
1957
2012
  # Clip the exposure geometries
1958
- # Filter buildings and roads
1959
- road_inds = gdf[_FIAT_COLUMNS.primary_object_type].str.contains("road")
1960
- # Ensure road_inds is a boolean Series
1961
- if not road_inds.dtype == bool:
1962
- road_inds = road_inds.astype(bool)
1963
- # Clip buildings
1964
- gdf_buildings = gdf[~road_inds]
1965
- gdf_buildings = self._clip_gdf(
1966
- gdf_buildings, clipped_region, predicate="within"
1967
- ).reset_index(drop=True)
1968
-
1969
- if road_inds.any():
1970
- # Clip roads
1971
- gdf_roads = gdf[road_inds]
1972
- gdf_roads = self._clip_gdf(
1973
- gdf_roads, clipped_region, predicate="within"
1974
- ).reset_index(drop=True)
1975
-
1976
- idx_buildings = self.fiat_model.exposure.geom_names.index(
1977
- self.config.fiat_buildings_name
1978
- )
1979
- idx_roads = self.fiat_model.exposure.geom_names.index(
1980
- self.config.fiat_roads_name
1981
- )
1982
- self.fiat_model.exposure.exposure_geoms[idx_buildings] = gdf_buildings[
1983
- [_FIAT_COLUMNS.object_id, "geometry"]
1984
- ]
1985
- self.fiat_model.exposure.exposure_geoms[idx_roads] = gdf_roads[
1986
- [_FIAT_COLUMNS.object_id, "geometry"]
1987
- ]
1988
- gdf = pd.concat([gdf_buildings, gdf_roads])
1989
- else:
1990
- gdf = gdf_buildings
1991
- self.fiat_model.exposure.exposure_geoms[0] = gdf[
1992
- [_FIAT_COLUMNS.object_id, "geometry"]
1993
- ]
2013
+ gdf = self._clip_gdf(gdf, sfincs_extend, predicate="within")
1994
2014
 
1995
2015
  # Save exposure dataframe
1996
2016
  del gdf["geometry"]
1997
2017
  self.fiat_model.exposure.exposure_db = gdf.reset_index(drop=True)
1998
2018
 
2019
+ # Make
2020
+ self._delete_extra_geometries()
2021
+
1999
2022
  # Clip the building footprints
2000
2023
  fieldname = "BF_FID"
2001
2024
  if clip_footprints and not self.fiat_model.building_footprint.empty:
2002
2025
  # Get buildings after filtering and their footprint id
2003
2026
  self.fiat_model.building_footprint = self.fiat_model.building_footprint[
2004
- self.fiat_model.building_footprint[fieldname].isin(
2005
- gdf_buildings[fieldname]
2006
- )
2027
+ self.fiat_model.building_footprint[fieldname].isin(gdf[fieldname])
2007
2028
  ].reset_index(drop=True)
2008
2029
 
2009
2030
  @staticmethod
@@ -2022,6 +2043,7 @@ class DatabaseBuilder:
2022
2043
  return gdf_new
2023
2044
 
2024
2045
  @staticmethod
2046
+ @debug_timer
2025
2047
  def spatial_join(
2026
2048
  objects: gpd.GeoDataFrame,
2027
2049
  layer: Union[str, gpd.GeoDataFrame],
@@ -2070,14 +2092,25 @@ class DatabaseBuilder:
2070
2092
  layer = layer.rename(columns={field_name: rename})
2071
2093
  return objects_joined, layer
2072
2094
 
2073
- def _get_fiat_building_index(self) -> int:
2074
- return self.fiat_model.exposure.geom_names.index(
2075
- self.config.fiat_buildings_name
2095
+ def _get_fiat_building_index(self) -> list[int]:
2096
+ names = self.config.fiat_buildings_name
2097
+ if isinstance(names, str):
2098
+ names = [names]
2099
+ indices = [
2100
+ self.fiat_model.exposure.geom_names.index(name)
2101
+ for name in names
2102
+ if name in self.fiat_model.exposure.geom_names
2103
+ ]
2104
+ if indices:
2105
+ return indices
2106
+ raise ValueError(
2107
+ f"None of the specified building geometry names {names} found in FIAT model exposure geom_names."
2076
2108
  )
2077
2109
 
2078
2110
  def _get_fiat_road_index(self) -> int:
2079
2111
  return self.fiat_model.exposure.geom_names.index(self.config.fiat_roads_name)
2080
2112
 
2113
+ @debug_timer
2081
2114
  def _get_closest_station(self):
2082
2115
  # Get available stations from source
2083
2116
  obs_data = obs.source(self.config.tide_gauge.source)
@@ -2099,7 +2132,7 @@ class DatabaseBuilder:
2099
2132
  )
2100
2133
 
2101
2134
  distance = us.UnitfulLength(value=distance, units=us.UnitTypesLength.meters)
2102
- self.logger.info(
2135
+ logger.info(
2103
2136
  f"The closest tide gauge from {self.config.tide_gauge.source} is located {distance.transform(self.unit_system.default_length_units)} from the SFINCS domain"
2104
2137
  )
2105
2138
  # Check if user provided max distance
@@ -2110,7 +2143,7 @@ class DatabaseBuilder:
2110
2143
  value=distance.convert(units_new), units=units_new
2111
2144
  )
2112
2145
  if distance_new.value > self.config.tide_gauge.max_distance.value:
2113
- self.logger.warning(
2146
+ logger.warning(
2114
2147
  f"This distance is larger than the 'max_distance' value of {self.config.tide_gauge.max_distance.value} {units_new} provided in the config file. The station cannot be used."
2115
2148
  )
2116
2149
  return None
@@ -2120,6 +2153,7 @@ class DatabaseBuilder:
2120
2153
 
2121
2154
  return station_id
2122
2155
 
2156
+ @debug_timer
2123
2157
  def _get_station_metadata(self, station_id: str, ref: str = "MLLW"):
2124
2158
  """
2125
2159
  Find the closest tide gauge station to the SFINCS domain and retrieves its metadata.
@@ -2166,11 +2200,11 @@ class DatabaseBuilder:
2166
2200
  "lat": station_metadata["lat"],
2167
2201
  }
2168
2202
 
2169
- self.logger.info(
2203
+ logger.info(
2170
2204
  f"The tide gauge station '{station_metadata['name']}' from {self.config.tide_gauge.source} will be used to download nearshore historical water level time-series."
2171
2205
  )
2172
2206
 
2173
- self.logger.info(
2207
+ logger.info(
2174
2208
  f"The station metadata will be used to fill in the water_level attribute in the site.toml. The reference level will be {ref}."
2175
2209
  )
2176
2210
 
@@ -2191,14 +2225,69 @@ class DatabaseBuilder:
2191
2225
  bin_colors = tomli.load(f)
2192
2226
  return bin_colors
2193
2227
 
2228
+ def _delete_extra_geometries(self) -> None:
2229
+ """
2230
+ Remove extra geometries from the exposure_geoms list that do not have a corresponding object_id in the exposure_db DataFrame.
2194
2231
 
2195
- if __name__ == "__main__":
2232
+ Returns
2233
+ -------
2234
+ None
2235
+ """
2236
+ # Make sure only csv objects have geometries
2237
+ for i, geoms in enumerate(self.fiat_model.exposure.exposure_geoms):
2238
+ keep = geoms[_FIAT_COLUMNS.object_id].isin(
2239
+ self.fiat_model.exposure.exposure_db[_FIAT_COLUMNS.object_id]
2240
+ )
2241
+ geoms = geoms[keep].reset_index(drop=True)
2242
+ self.fiat_model.exposure.exposure_geoms[i] = geoms
2243
+
2244
+ def _get_fiat_gdf_full(self) -> gpd.GeoDataFrame:
2245
+ """
2246
+ Get the full GeoDataFrame of the Fiat model.
2247
+
2248
+ Returns
2249
+ -------
2250
+ gpd.GeoDataFrame: The full GeoDataFrame of the Fiat model.
2251
+ """
2252
+ gdf = self.fiat_model.exposure.get_full_gdf(
2253
+ self.fiat_model.exposure.exposure_db
2254
+ )
2255
+ # Keep only unique "object_id" rows, keeping the first occurrence
2256
+ gdf = gdf.drop_duplicates(
2257
+ subset=_FIAT_COLUMNS.object_id, keep="first"
2258
+ ).reset_index(drop=True)
2259
+
2260
+ return gdf
2261
+
2262
+
2263
+ def main():
2196
2264
  while True:
2197
2265
  config_path = Path(
2198
2266
  input(
2199
2267
  "Please provide the path to the database creation configuration toml: \n"
2200
2268
  )
2201
2269
  )
2270
+ print(
2271
+ "Please select the log verbosity level for the database creation process.\n"
2272
+ "From most verbose to least verbose: `DEBUG`, `INFO`, `WARNING`.'n"
2273
+ )
2274
+ log_level = input("Enter log level: ")
2275
+ match log_level:
2276
+ case "DEBUG":
2277
+ level = logging.DEBUG
2278
+ case "INFO":
2279
+ level = logging.INFO
2280
+ case "WARNING":
2281
+ level = logging.WARNING
2282
+ case _:
2283
+ print(
2284
+ f"Log level `{log_level}` not recognized. Defaulting to INFO. Please choose from: `DEBUG`, `INFO`, `WARNING`."
2285
+ )
2286
+ log_level = "INFO"
2287
+ level = logging.INFO
2288
+
2289
+ FloodAdaptLogging(level=level)
2290
+
2202
2291
  try:
2203
2292
  config = ConfigModel.read(config_path)
2204
2293
  dbs = DatabaseBuilder(config)
@@ -2208,3 +2297,7 @@ if __name__ == "__main__":
2208
2297
  quit = input("Do you want to quit? (y/n)")
2209
2298
  if quit == "y":
2210
2299
  exit()
2300
+
2301
+
2302
+ if __name__ == "__main__":
2303
+ main()