flood-adapt 1.1.4__py3-none-any.whl → 1.1.5__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,5 +1,5 @@
1
1
  # has to be here at the start to avoid circular imports
2
- __version__ = "1.1.4"
2
+ __version__ = "1.1.5"
3
3
 
4
4
  from flood_adapt import adapter, database_builder, dbs_classes, objects
5
5
  from flood_adapt.config.config import Settings
@@ -1527,9 +1527,13 @@ class DatabaseBuilder:
1527
1527
  )
1528
1528
  # Add tide gauge as obs point if within model region
1529
1529
  if coord.within(model_region):
1530
+ if self.tide_gauge.name is None:
1531
+ name = "tide_gauge"
1532
+ else:
1533
+ name = self.tide_gauge.name
1530
1534
  obs_points.append(
1531
1535
  ObsPointModel(
1532
- name=self.tide_gauge.name,
1536
+ name=name,
1533
1537
  description="Tide gauge observation point",
1534
1538
  ID=self.tide_gauge.ID,
1535
1539
  lon=self.tide_gauge.lon,
@@ -1663,7 +1667,7 @@ class DatabaseBuilder:
1663
1667
  db_file_path.parent.mkdir(parents=True, exist_ok=True)
1664
1668
  shutil.copyfile(self.config.tide_gauge.file, db_file_path)
1665
1669
 
1666
- rel_db_path = Path(db_file_path.relative_to(self.static_path))
1670
+ rel_db_path = Path(db_file_path.relative_to(self.static_path)).as_posix()
1667
1671
  logger.warning(
1668
1672
  f"Tide gauge from file {rel_db_path} assumed to be in {self.unit_system.default_length_units}!"
1669
1673
  )
@@ -1795,26 +1799,22 @@ class DatabaseBuilder:
1795
1799
  if self.sfincs_offshore_model is None:
1796
1800
  return None
1797
1801
  # Connect boundary points of overland to output points of offshore
1802
+ # First read in the boundary locations from the overland model
1798
1803
  fn = Path(self.sfincs_overland_model.root) / "sfincs.bnd"
1799
- bnd = pd.read_csv(fn, sep=" ", lineterminator="\n", header=None)
1800
- bnd = bnd.rename(columns={0: "x", 1: "y"})
1801
- bnd_geo = gpd.GeoDataFrame(
1802
- bnd,
1803
- geometry=gpd.points_from_xy(bnd.x, bnd.y),
1804
+ lines = []
1805
+ if fn.exists():
1806
+ with open(fn) as f:
1807
+ lines = f.readlines()
1808
+ coords = [(float(line.split()[0]), float(line.split()[1])) for line in lines]
1809
+ x, y = zip(*coords)
1810
+ bnd = gpd.GeoDataFrame(
1811
+ geometry=gpd.points_from_xy(x, y),
1804
1812
  crs=self.sfincs_overland_model.config["epsg"],
1805
1813
  )
1806
- obs_geo = bnd_geo.to_crs(4326)
1807
- obs_geo["x"] = obs_geo.geometry.x
1808
- obs_geo["y"] = obs_geo.geometry.y
1809
- del obs_geo["geometry"]
1810
- obs_geo["name"] = [f"bnd_pt{num:02d}" for num in range(1, len(obs_geo) + 1)]
1811
- fn_off = Path(self.sfincs_offshore_model.root) / "sfincs.obs"
1812
- obs_geo.to_csv(
1813
- fn_off,
1814
- sep="\t",
1815
- index=False,
1816
- header=False,
1817
- )
1814
+ # Then transform points to offshore crs and save them as observation points
1815
+ obs_geo = bnd.to_crs(self.sfincs_offshore_model.config["epsg"])
1816
+ self.sfincs_offshore_model.setup_observation_points(obs_geo)
1817
+ self.sfincs_offshore_model.write()
1818
1818
  logger.info(
1819
1819
  "Output points of the offshore SFINCS model were reconfigured to the boundary points of the overland SFINCS model."
1820
1820
  )
@@ -79,7 +79,8 @@ class DbsScenario(DbsTemplate[Scenario]):
79
79
  """
80
80
  event_left = self._database.events.get(left.event)
81
81
  event_right = self._database.events.get(right.event)
82
- equal_events = event_left == event_right
82
+ # Deep-compare events including forcing data contents
83
+ equal_events = event_left.data_equivalent(event_right)
83
84
 
84
85
  left_projection = self._database.projections.get(left.projection)
85
86
  right_projection = self._database.projections.get(right.projection)
@@ -117,6 +117,63 @@ class Event(Object):
117
117
  for forcing in self.get_forcings():
118
118
  forcing.save_additional(output_dir)
119
119
 
120
+ def data_equivalent(self, other: "Event") -> bool:
121
+ """Deep-compare two events, including forcing data contents.
122
+
123
+ Compares core attributes (time, template, mode, rainfall_multiplier) and then
124
+ verifies that each forcing (by type) has the same data fingerprint. For
125
+ path-based forcings, the fingerprint hashes the file bytes; for others,
126
+ a canonical attribute-based hash is used.
127
+
128
+ Parameters
129
+ ----------
130
+ other : Event
131
+ The event to compare against.
132
+
133
+ Returns
134
+ -------
135
+ bool
136
+ True when events are equivalent in terms of their hazard inputs.
137
+ """
138
+ if not isinstance(other, Event):
139
+ return False
140
+
141
+ # Compare high-level attributes first
142
+ if (
143
+ self.template != other.template
144
+ or self.mode != other.mode
145
+ or self.rainfall_multiplier != other.rainfall_multiplier
146
+ or self.time != other.time
147
+ ):
148
+ return False
149
+
150
+ # Compare allowed forcing types present in each event
151
+ if set(self.forcings.keys()) != set(other.forcings.keys()):
152
+ return False
153
+
154
+ # Build comparable, sorted fingerprint lists per forcing type
155
+ def fingerprints(evt: "Event") -> dict[ForcingType, list[tuple[str, str]]]:
156
+ d: dict[ForcingType, list[tuple[str, str]]] = {}
157
+ for ftype, flist in evt.forcings.items():
158
+ fps: list[tuple[str, str]] = []
159
+ for f in flist:
160
+ # Include source and the fingerprint to guard against collisions across different sources
161
+ src = f.source.value if hasattr(f, "source") else ""
162
+ fp = f.content_fingerprint() # type: ignore[attr-defined]
163
+ fps.append((src, fp))
164
+ # Sort for order-insensitive comparison
165
+ d[ftype] = sorted(fps, key=lambda t: (t[0], t[1]))
166
+ return d
167
+
168
+ left_fp = fingerprints(self)
169
+ right_fp = fingerprints(other)
170
+
171
+ for ftype in left_fp.keys():
172
+ if left_fp[ftype] != right_fp[ftype]:
173
+ return False
174
+
175
+ return True
176
+
120
177
  @classmethod
121
178
  def load_file(cls, file_path: Path | str | os.PathLike) -> "Event":
122
179
  """Load object from file.
@@ -1,3 +1,5 @@
1
+ import hashlib
2
+ import json
1
3
  import os
2
4
  from abc import ABC, abstractmethod
3
5
  from enum import Enum
@@ -72,7 +74,7 @@ class IForcing(BaseModel, ABC):
72
74
 
73
75
  def save_additional(self, output_dir: Path | str | os.PathLike) -> None:
74
76
  """Save additional data of the forcing."""
75
- return
77
+ pass
76
78
 
77
79
  @field_serializer("path", check_fields=False)
78
80
  @classmethod
@@ -80,6 +82,49 @@ class IForcing(BaseModel, ABC):
80
82
  """Serialize filepath-like fields by saving only the filename. It is assumed that the file will be saved in the same directory."""
81
83
  return value.name
82
84
 
85
+ def content_fingerprint(self) -> str:
86
+ """Return a stable fingerprint of the forcing's underlying data.
87
+
88
+ - If a file-backed `path` attribute exists and the file exists, hash its bytes (SHA-256).
89
+ - Otherwise, hash a canonical JSON dump of the model (excluding volatile fields like `path`).
90
+
91
+ Returns
92
+ -------
93
+ str
94
+ A fingerprint string that changes when the forcing's effective data changes.
95
+ """
96
+ # Prefer hashing the actual file content when available
97
+ try:
98
+ p = getattr(self, "path", None)
99
+ if isinstance(p, Path) and p and p.exists():
100
+ sha = hashlib.sha256()
101
+ with p.open("rb") as f:
102
+ for chunk in iter(lambda: f.read(8192), b""):
103
+ sha.update(chunk)
104
+ # Include filename to disambiguate multi-forcing scenarios with identical content
105
+ return f"FILE:{p.name}:{sha.hexdigest()}"
106
+ except Exception:
107
+ # Fall through to attribute-based hashing if anything goes wrong
108
+ pass
109
+
110
+ # Fallback: hash the model attributes (excluding volatile fields like path)
111
+ data = self.model_dump(exclude_none=True)
112
+ # Remove potentially non-stable/absolute fields
113
+ data.pop("path", None)
114
+
115
+ # Ensure enums are serialized to their values for stable hashing
116
+ if isinstance(data.get("type"), Enum):
117
+ data["type"] = data["type"].value
118
+ if isinstance(data.get("source"), Enum):
119
+ data["source"] = data["source"].value
120
+
121
+ payload = json.dumps(data, sort_keys=True, default=str).encode("utf-8")
122
+ return f"ATTR:{hashlib.sha256(payload).hexdigest()}"
123
+
124
+ def _post_load(self, file_path: Path | str | os.PathLike, **kwargs) -> None:
125
+ """Post-load hook, called at the end of `load_file`, to perform any additional loading steps after loading from file."""
126
+ return
127
+
83
128
 
84
129
  class IDischarge(IForcing):
85
130
  type: ForcingType = ForcingType.DISCHARGE
@@ -8,6 +8,7 @@ import requests
8
8
  from noaa_coops.station import COOPSAPIError
9
9
  from pydantic import BaseModel, model_validator
10
10
 
11
+ from flood_adapt.config import Settings
11
12
  from flood_adapt.misc.log import FloodAdaptLogging
12
13
  from flood_adapt.objects.forcing import unit_system as us
13
14
  from flood_adapt.objects.forcing.time_frame import TimeFrame
@@ -56,7 +57,7 @@ class TideGauge(BaseModel):
56
57
  source: TideGaugeSource
57
58
  reference: str
58
59
  ID: Optional[int] = None # Attribute used to download from correct gauge
59
- file: Optional[Path] = None # for locally stored data
60
+ file: Optional[str] = None # for locally stored data
60
61
  lat: Optional[float] = None
61
62
  lon: Optional[float] = None
62
63
  units: us.UnitTypesLength = (
@@ -104,7 +105,8 @@ class TideGauge(BaseModel):
104
105
  """
105
106
  logger.info(f"Retrieving waterlevels for tide gauge {self.ID} for {time}")
106
107
  if self.file:
107
- gauge_data = self._read_imported_waterlevels(time=time, path=self.file)
108
+ abs_path = Settings().database_path / "static" / self.file
109
+ gauge_data = self._read_imported_waterlevels(time=time, path=abs_path)
108
110
  else:
109
111
  gauge_data = self._download_tide_gauge_data(time=time)
110
112
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: flood-adapt
3
- Version: 1.1.4
3
+ Version: 1.1.5
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: ====================================================
@@ -1,4 +1,4 @@
1
- flood_adapt/__init__.py,sha256=HuJSWvtJutuwuiXwgvArRmpnG72guATUP3IFvzowaJY,779
1
+ flood_adapt/__init__.py,sha256=qZBLP5qxMYURIbpEB6pRkz3wIeIVQZ0N9wTSJgxs_BA,779
2
2
  flood_adapt/flood_adapt.py,sha256=HVFS4OFhcB0TqHtMw3kbEei0IfJxsciauHfG3XZ38-0,40747
3
3
  flood_adapt/adapter/__init__.py,sha256=vnF8NCkEVX-N-gtGS-J_A1H1YYAjihWjJZFyYGwcp8Q,180
4
4
  flood_adapt/adapter/fiat_adapter.py,sha256=seDjPoumkhUOd7qer3ni1_Ut3dwyq0-_yhJNaTEFc2E,60284
@@ -18,7 +18,7 @@ flood_adapt/config/impacts.py,sha256=O7vE7jB3GSXnkqAvv7TqJiJ_j1uJ3mck_KQ-ScsB3bo
18
18
  flood_adapt/config/sfincs.py,sha256=y8C3PzFwwgMB_sb8rBzgteaQ8fCxep6DnZxuk0q__bc,4825
19
19
  flood_adapt/config/site.py,sha256=VR90jCHWcxgoQJptNyXy7LseGjXUDRtdOjNGCddFVzI,4328
20
20
  flood_adapt/database_builder/__init__.py,sha256=h4ietZ6sAZa7j2kvSzp5-58BueGrfJsXvq8PFu1RLyI,1112
21
- flood_adapt/database_builder/database_builder.py,sha256=cv_uwkKqgc8RLQ5lAmTqRN1fm5gBOYbIVR3v2MgD_gY,109935
21
+ flood_adapt/database_builder/database_builder.py,sha256=nPwkBQpDzl74OrgF3hjQiDJprtxtdJuu0HsY3jNvJiM,110065
22
22
  flood_adapt/database_builder/metrics_utils.py,sha256=aU7YfXLmBjFT0fQQQl3o0yIzdFJ6XJGlld0GnkJytGc,66258
23
23
  flood_adapt/database_builder/templates/default_units/imperial.toml,sha256=zIjPlxIa2kWLUjSYisd8UolXGo5iKdFoDDz_JkKBXTM,295
24
24
  flood_adapt/database_builder/templates/default_units/metric.toml,sha256=tc0XMKs7xGL9noB9lAb0gyQfjYxzokgHa3NqpccxWl0,302
@@ -61,7 +61,7 @@ flood_adapt/dbs_classes/dbs_benefit.py,sha256=ayEYz8ga49HLdYuUsDWZOuZnpRnBpTuyhv
61
61
  flood_adapt/dbs_classes/dbs_event.py,sha256=ak3kHan6L1EfC8agDLKiCe8gaY5leOmj_qUBsI61q9A,1869
62
62
  flood_adapt/dbs_classes/dbs_measure.py,sha256=vVs-LtnHJN7eSGIFUglJdpbtfq_QI_Ftkv4lh5mfnNM,4085
63
63
  flood_adapt/dbs_classes/dbs_projection.py,sha256=lyiU_ctP2ixK28RKnBN6mVJbOuaDsWCj1y6-MHlyi_k,1078
64
- flood_adapt/dbs_classes/dbs_scenario.py,sha256=LHWx3Dr1XR47bPyPRkR70h3VcT0f0MVgB-R8V_G_O04,3993
64
+ flood_adapt/dbs_classes/dbs_scenario.py,sha256=bD5VjuojgE_79ZN7bZNpnbpthI9uYUwMSsrPDDcjlvg,4070
65
65
  flood_adapt/dbs_classes/dbs_static.py,sha256=Yzs-bsfAq2jkZ_-0_ojuzNf81Wifaxw8a1APNNS0mqM,10565
66
66
  flood_adapt/dbs_classes/dbs_strategy.py,sha256=qiEObHZeYL93GmdjSiGQls1ZmxdMZPkRkwzHgmoYwyE,4856
67
67
  flood_adapt/dbs_classes/dbs_template.py,sha256=b2x2sWNYTnaWU8Plgp51PFPrZGEv2kRRn9JBAgYhLbI,11578
@@ -82,20 +82,20 @@ flood_adapt/objects/benefits/benefits.py,sha256=1Di8v2B7YOdMkRlg0A6k6qtMqYE_JaaN
82
82
  flood_adapt/objects/events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  flood_adapt/objects/events/event_factory.py,sha256=CYq8itWurILPGNSDtWKWV5FF4UxjrRFiza6w2OQJEdU,4276
84
84
  flood_adapt/objects/events/event_set.py,sha256=fpmNbrd3qXWPgyqphv_Rf2iNsZCLvwz6YEbjMV7WIXA,2810
85
- flood_adapt/objects/events/events.py,sha256=xuZsmew4oG2TTh7M3lRLr0z2k-T-XU2ahbi-l_uPNGY,8341
85
+ flood_adapt/objects/events/events.py,sha256=Kqwyeh9MMp8ex6DBbg-rqBnp6RsrbR1J7bfO9ybevh4,10580
86
86
  flood_adapt/objects/events/historical.py,sha256=uIQ7icy9F3pOW0R-fBqjEjU857SGlJO81S104mzvtLY,2081
87
87
  flood_adapt/objects/events/hurricane.py,sha256=dOPMa5c4mz1umLne9ydkBqnLTig9D95LmPHSFE5wit8,2401
88
88
  flood_adapt/objects/events/synthetic.py,sha256=Q8OHbBumeEwbrzA1imBrxiGiB92RWyigVcGVUbisSh4,1366
89
89
  flood_adapt/objects/forcing/__init__.py,sha256=wzPrflMLiEolocs3UcLeg05u5ydMDhF00jEf9zsC76c,2141
90
90
  flood_adapt/objects/forcing/csv.py,sha256=AMHlE5K_QiMOqD2pRRbG4ojnZ9VPiQjtk2wMFSouKjk,2027
91
91
  flood_adapt/objects/forcing/discharge.py,sha256=GIuKETXizwuaeIbHlu0fELQwC0ttfZSDiUMRm4HEyWY,2057
92
- flood_adapt/objects/forcing/forcing.py,sha256=MonjhiHP6ge_5UFDMcLmj0pt5jK76Eznb4F7q3d8nAg,4356
92
+ flood_adapt/objects/forcing/forcing.py,sha256=n2IEk21Ih0GkddH3IqoV8egAYh4UCAVEyz_O0kk2fvE,6386
93
93
  flood_adapt/objects/forcing/forcing_factory.py,sha256=lpugKlMt0MN8BxyykottMOuQd_fp1MHrkuuqbHWWdRY,5965
94
94
  flood_adapt/objects/forcing/meteo_handler.py,sha256=rTxY5WNobK_Ifzj2eVcoSPGgb3TzuZljSv_dLn5FLo8,3016
95
95
  flood_adapt/objects/forcing/netcdf.py,sha256=ZBzFtN5joVs36lVjvYErVaHEylUQ6eKIhR0uk_MD-zM,1388
96
96
  flood_adapt/objects/forcing/plotting.py,sha256=Y7f_9bY8d9jbd7BqEAeRmof-aaJhlznM3_wGBOI7g-s,14828
97
97
  flood_adapt/objects/forcing/rainfall.py,sha256=e6P3IMzItvnsmXbcMXl1oV-d9LDuh3jTIc_vt6Kz5zo,3282
98
- flood_adapt/objects/forcing/tide_gauge.py,sha256=XhplyNHtCn0hRM1oeD5v-fMYAOLAJIKidmxKxVxCUlw,7188
98
+ flood_adapt/objects/forcing/tide_gauge.py,sha256=uPgvlcvDtZFQrtTyXX2d5YmSvCnjYYg0Xsu8NAwQZPQ,7299
99
99
  flood_adapt/objects/forcing/time_frame.py,sha256=1X3G0Ax18BHRvAomf-CW_ISRk_3qgAakwgZCIBxIkL4,2855
100
100
  flood_adapt/objects/forcing/timeseries.py,sha256=bD27JWzC3owq5ah3zPzJ7xoUzSH_t4J03s_SycYW0mQ,19740
101
101
  flood_adapt/objects/forcing/unit_system.py,sha256=7FFOmaxq6EOvXx64QDxlpNU4uMExqridFcdFwyTJ4Lo,16542
@@ -114,8 +114,8 @@ flood_adapt/objects/strategies/strategies.py,sha256=Jw-WJDCamL9p_7VEir3AdmYPMVAi
114
114
  flood_adapt/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  flood_adapt/workflows/benefit_runner.py,sha256=eA21TuHdeZ6QYO8ehXri6BHlkyHsVsZphIdIca5g0KA,21824
116
116
  flood_adapt/workflows/scenario_runner.py,sha256=9_Y6GmMYhYoTRkBUIlju0eBy6DosGf4Zl2tgu1QEubI,4119
117
- flood_adapt-1.1.4.dist-info/LICENSE,sha256=Ui5E03pQ0EVKxvKA54lTPA1xrtgA2HMGLQai95eOzoE,36321
118
- flood_adapt-1.1.4.dist-info/METADATA,sha256=70kUiml9cQzOes7B0XAgpqzYCU1kAYuhSAiXLdmqa8U,48806
119
- flood_adapt-1.1.4.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
120
- flood_adapt-1.1.4.dist-info/top_level.txt,sha256=JvzMi6cTcQPEThCfpgMEeVny3ghI1urSH0CCgVIqSzw,12
121
- flood_adapt-1.1.4.dist-info/RECORD,,
117
+ flood_adapt-1.1.5.dist-info/LICENSE,sha256=Ui5E03pQ0EVKxvKA54lTPA1xrtgA2HMGLQai95eOzoE,36321
118
+ flood_adapt-1.1.5.dist-info/METADATA,sha256=yGKj5wuwDfXXwLSnJVfZmywRC4hZAJ_g2NrYeuCvg5I,48806
119
+ flood_adapt-1.1.5.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
120
+ flood_adapt-1.1.5.dist-info/top_level.txt,sha256=JvzMi6cTcQPEThCfpgMEeVny3ghI1urSH0CCgVIqSzw,12
121
+ flood_adapt-1.1.5.dist-info/RECORD,,