flood-adapt 0.3.2__py3-none-any.whl → 0.3.4__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__ = "0.3.2"
2
+ __version__ = "0.3.4"
3
3
 
4
4
  from flood_adapt import adapter, dbs_classes, objects
5
5
  from flood_adapt.config.config import Settings
@@ -319,9 +319,11 @@ class FiatAdapter(IImpactAdapter):
319
319
  fiat_log = path / "fiat.log"
320
320
  with cd(path):
321
321
  with FloodAdaptLogging.to_file(file_path=fiat_log):
322
+ FiatAdapter._ensure_correct_hash_spacing_in_csv(path)
323
+
322
324
  self.logger.info(f"Running FIAT in {path}")
323
325
  process = subprocess.run(
324
- f'"{exe_path.as_posix()}" run settings.toml',
326
+ args=[Path(exe_path).resolve().as_posix(), "run", "settings.toml"],
325
327
  stdout=subprocess.PIPE,
326
328
  stderr=subprocess.PIPE,
327
329
  text=True,
@@ -1500,3 +1502,35 @@ class FiatAdapter(IImpactAdapter):
1500
1502
  )
1501
1503
  # Save as geopackage
1502
1504
  roads.to_file(output_path, driver="GPKG")
1505
+
1506
+ @staticmethod
1507
+ def _ensure_correct_hash_spacing_in_csv(
1508
+ model_root: Path, hash_spacing: int = 1
1509
+ ) -> None:
1510
+ """
1511
+ Ensure that the CSV file has the correct number of spaces between hashes.
1512
+
1513
+ When writing csv files, FIAT does not add spaces between the hashes and the line, which leads to errors on linux.
1514
+
1515
+
1516
+ Parameters
1517
+ ----------
1518
+ file_path : Path
1519
+ The path to the model root.
1520
+ hash_spacing : int, optional
1521
+ The number of spaces between hashes, by default 1.
1522
+ """
1523
+ for dirpath, _, filenames in os.walk(model_root):
1524
+ for filename in filenames:
1525
+ if not filename.lower().endswith(".csv"):
1526
+ continue
1527
+ file_path = os.path.join(dirpath, filename)
1528
+
1529
+ with open(file_path, "r") as file:
1530
+ lines = file.readlines()
1531
+
1532
+ with open(file_path, "w") as file:
1533
+ for line in lines:
1534
+ if line.startswith("#"):
1535
+ line = "#" + " " * hash_spacing + line.lstrip("#")
1536
+ file.write(line)
@@ -1,7 +1,6 @@
1
1
  from os import environ, listdir
2
2
  from pathlib import Path
3
3
  from platform import system
4
- from typing import ClassVar
5
4
 
6
5
  import tomli
7
6
  import tomli_w
@@ -13,6 +12,48 @@ from pydantic import (
13
12
  )
14
13
  from pydantic_settings import BaseSettings, SettingsConfigDict
15
14
 
15
+ DEFAULT_SYSTEM_FOLDER = Path(__file__).parents[1] / "system"
16
+ DEFAULT_EXE_PATHS: dict[str, dict[str, Path]] = {
17
+ "windows": {
18
+ "sfincs": DEFAULT_SYSTEM_FOLDER / "win-64" / "sfincs" / "sfincs.exe",
19
+ "fiat": DEFAULT_SYSTEM_FOLDER / "win-64" / "fiat" / "fiat.exe",
20
+ },
21
+ "linux": {
22
+ "sfincs": DEFAULT_SYSTEM_FOLDER / "linux-64" / "sfincs" / "bin" / "sfincs",
23
+ "fiat": DEFAULT_SYSTEM_FOLDER / "linux-64" / "fiat" / "fiat",
24
+ },
25
+ }
26
+
27
+
28
+ def _default_exe_path(exe_name: str) -> Path:
29
+ """
30
+ Get the default path for the given executable name based on the system type.
31
+
32
+ Parameters
33
+ ----------
34
+ exe_name : str
35
+ The name of the executable (e.g., "sfincs", "fiat").
36
+
37
+ Returns
38
+ -------
39
+ Path
40
+ The default path to the executable.
41
+
42
+ Raises
43
+ ------
44
+ ValueError
45
+ If the system type is not recognized.
46
+ """
47
+ if system().lower() not in DEFAULT_EXE_PATHS:
48
+ raise ValueError(
49
+ f"System type '{system()}' is not recognized. Supported types are: {', '.join(DEFAULT_EXE_PATHS.keys())}."
50
+ )
51
+ if exe_name not in DEFAULT_EXE_PATHS[system().lower()]:
52
+ raise ValueError(
53
+ f"Executable name '{exe_name}' is not recognized. Supported names are: {', '.join(DEFAULT_EXE_PATHS[system().lower()].keys())}."
54
+ )
55
+ return DEFAULT_EXE_PATHS[system().lower()][exe_name]
56
+
16
57
 
17
58
  class Settings(BaseSettings):
18
59
  """
@@ -25,7 +66,7 @@ class Settings(BaseSettings):
25
66
 
26
67
  Usage
27
68
  -----
28
- from flood_adapt.config import Settings
69
+ from flood_adapt import Settings
29
70
 
30
71
  One of the following:
31
72
 
@@ -36,7 +77,7 @@ class Settings(BaseSettings):
36
77
  `settings = Settings.read(toml_path: Path)`
37
78
 
38
79
  3) Load settings from keyword arguments, overwriting any environment variables:
39
- `settings = Settings(DATABASE_ROOT="path/to/database", DATABASE_NAME="database_name", SYSTEM_FOLDER="path/to/system_folder")`
80
+ `settings = Settings(DATABASE_ROOT="path/to/database", DATABASE_NAME="database_name")`
40
81
 
41
82
  Attributes
42
83
  ----------
@@ -44,19 +85,21 @@ class Settings(BaseSettings):
44
85
  The name of the database.
45
86
  database_root : Path
46
87
  The root directory of the database.
47
- system_folder : Path
48
- The root directory of the system folder containing the kernels.
49
88
  delete_crashed_runs : bool
50
89
  Whether to delete crashed/corrupted runs immediately after they are detected.
90
+ sfincs_path : Path
91
+ The path to the SFINCS binary.
92
+ fiat_path : Path
93
+ The path to the FIAT binary.
94
+ validate_allowed_forcings : bool
95
+ Whether to validate the forcing types and sources against the allowed forcings in the event model.
96
+ validate_bin_paths : bool
97
+ Whether to validate the existence of the paths to the SFINCS and FIAT binaries.
51
98
 
52
99
  Properties
53
100
  ----------
54
101
  database_path : Path
55
102
  The full path to the database.
56
- sfincs_path : Path
57
- The path to the SFINCS binary.
58
- fiat_path : Path
59
- The path to the FIAT binary.
60
103
 
61
104
  Raises
62
105
  ------
@@ -64,12 +107,6 @@ class Settings(BaseSettings):
64
107
  If required settings are missing or invalid.
65
108
  """
66
109
 
67
- SYSTEM_SUFFIXES: ClassVar[dict[str, str]] = {
68
- "Windows": ".exe",
69
- "Linux": "",
70
- "Darwin": "",
71
- }
72
-
73
110
  model_config = SettingsConfigDict(env_ignore_empty=True, validate_default=True)
74
111
 
75
112
  database_root: Path = Field(
@@ -83,33 +120,39 @@ class Settings(BaseSettings):
83
120
  default="",
84
121
  description="The name of the database site, should be a folder inside the database root. The site must contain an 'input' and 'static' folder.",
85
122
  )
86
- system_folder: Path = Field(
87
- alias="SYSTEM_FOLDER", # environment variable: SYSTEM_FOLDER
88
- default=Path(__file__).parents[1] / "system",
89
- description="The path of the system folder containing the kernels that run the calculations. Default is to look for the system folder in `FloodAdapt/flood_adapt/system`",
90
- )
123
+
91
124
  delete_crashed_runs: bool = Field(
92
125
  alias="DELETE_CRASHED_RUNS", # environment variable: DELETE_CRASHED_RUNS
93
- default=True,
126
+ default=False,
94
127
  description="Whether to delete the output of crashed/corrupted runs. Be careful when setting this to False, as it may lead to a broken database that cannot be read in anymore.",
95
128
  exclude=True,
96
129
  )
97
130
  validate_allowed_forcings: bool = Field(
98
131
  alias="VALIDATE_ALLOWED_FORCINGS", # environment variable: VALIDATE_ALLOWED_FORCINGS
99
- default=True,
132
+ default=False,
100
133
  description="Whether to validate the forcing types and sources against the allowed forcings in the event model.",
101
134
  exclude=True,
102
135
  )
136
+ validate_bin_paths: bool = Field(
137
+ alias="VALIDATE_BINARIES", # environment variable: VALIDATE_BINARIES
138
+ default=False,
139
+ description="Whether to validate the existence of the paths to the SFINCS and FIAT binaries.",
140
+ exclude=True,
141
+ )
103
142
 
104
- @computed_field
105
- @property
106
- def sfincs_path(self) -> Path:
107
- return self.system_folder / "sfincs" / f"sfincs{Settings._system_extension()}"
143
+ sfincs_path: Path = Field(
144
+ default=_default_exe_path("sfincs"),
145
+ alias="SFINCS_BIN_PATH", # environment variable: SFINCS_BIN_PATH
146
+ description="The path of the sfincs binary.",
147
+ exclude=True,
148
+ )
108
149
 
109
- @computed_field
110
- @property
111
- def fiat_path(self) -> Path:
112
- return self.system_folder / "fiat" / f"fiat{Settings._system_extension()}"
150
+ fiat_path: Path = Field(
151
+ default=_default_exe_path("fiat"),
152
+ alias="FIAT_BIN_PATH", # environment variable: FIAT_BIN_PATH
153
+ description="The path of the fiat binary.",
154
+ exclude=True,
155
+ )
113
156
 
114
157
  @computed_field
115
158
  @property
@@ -119,18 +162,33 @@ class Settings(BaseSettings):
119
162
  @model_validator(mode="after")
120
163
  def validate_settings(self):
121
164
  self._validate_database_path()
122
- self._validate_system_folder()
123
- self._validate_fiat_path()
124
- self._validate_sfincs_path()
165
+ if self.validate_bin_paths:
166
+ self._validate_fiat_path()
167
+ self._validate_sfincs_path()
125
168
  self._update_environment_variables()
126
169
  return self
127
170
 
128
171
  def _update_environment_variables(self):
129
172
  environ["DATABASE_ROOT"] = str(self.database_root)
130
173
  environ["DATABASE_NAME"] = self.database_name
131
- environ["SYSTEM_FOLDER"] = str(self.system_folder)
132
- environ["DELETE_CRASHED_RUNS"] = str(self.delete_crashed_runs)
133
- environ["VALIDATE_ALLOWED_FORCINGS"] = str(self.validate_allowed_forcings)
174
+ environ["SFINCS_BIN_PATH"] = str(self.sfincs_path)
175
+ environ["FIAT_BIN_PATH"] = str(self.fiat_path)
176
+ environ["SFINCS_PATH"] = str(self.sfincs_path)
177
+ environ["FIAT_PATH"] = str(self.fiat_path)
178
+
179
+ if self.delete_crashed_runs:
180
+ environ["DELETE_CRASHED_RUNS"] = str(self.delete_crashed_runs)
181
+ else:
182
+ environ.pop("DELETE_CRASHED_RUNS", None)
183
+ if self.validate_allowed_forcings:
184
+ environ["VALIDATE_ALLOWED_FORCINGS"] = str(self.validate_allowed_forcings)
185
+ else:
186
+ environ.pop("VALIDATE_ALLOWED_FORCINGS", None)
187
+ if self.validate_bin_paths:
188
+ environ["VALIDATE_BINARIES"] = str(self.validate_bin_paths)
189
+ else:
190
+ environ.pop("VALIDATE_BINARIES", None)
191
+
134
192
  return self
135
193
 
136
194
  def _validate_database_path(self):
@@ -165,11 +223,6 @@ class Settings(BaseSettings):
165
223
 
166
224
  return self
167
225
 
168
- def _validate_system_folder(self):
169
- if not self.system_folder.is_dir():
170
- raise ValueError(f"System folder {self.system_folder} does not exist.")
171
- return self
172
-
173
226
  def _validate_sfincs_path(self):
174
227
  if not self.sfincs_path.exists():
175
228
  raise ValueError(f"SFINCS binary {self.sfincs_path} does not exist.")
@@ -180,18 +233,10 @@ class Settings(BaseSettings):
180
233
  raise ValueError(f"FIAT binary {self.fiat_path} does not exist.")
181
234
  return self
182
235
 
183
- @field_serializer(
184
- "database_root", "system_folder", "sfincs_path", "fiat_path", "database_path"
185
- )
236
+ @field_serializer("database_root", "database_path")
186
237
  def serialize_path(self, path: Path) -> str:
187
238
  return str(path)
188
239
 
189
- @staticmethod
190
- def _system_extension() -> str:
191
- if system() not in Settings.SYSTEM_SUFFIXES:
192
- raise ValueError(f"Unsupported system {system()}")
193
- return Settings.SYSTEM_SUFFIXES[system()]
194
-
195
240
  @staticmethod
196
241
  def read(toml_path: Path) -> "Settings":
197
242
  """
flood_adapt/config/gui.py CHANGED
@@ -1,103 +1,210 @@
1
1
  from pathlib import Path
2
2
  from typing import Optional
3
3
 
4
- from pydantic import BaseModel, Field
5
- from tomli import load as load_toml
4
+ import geopandas as gpd
5
+ import numpy as np
6
+ import pandas as pd
7
+ import tomli
8
+ from pydantic import BaseModel, Field, model_validator
6
9
 
7
10
  from flood_adapt.config.fiat import DamageType
8
11
  from flood_adapt.objects.forcing import unit_system as us
9
12
 
10
13
 
11
- class MapboxLayersModel(BaseModel):
14
+ class Layer(BaseModel):
15
+ """
16
+ Base class for layers in the GUI.
17
+
18
+ Attributes
19
+ ----------
20
+ bins : list[float]
21
+ The bins for the layer.
22
+ colors : list[str]
23
+ The colors for the layer.
24
+ """
25
+
26
+ bins: list[float]
27
+ colors: list[str]
28
+
29
+ @model_validator(mode="after")
30
+ def check_bins_and_colors(self) -> "Layer":
31
+ """Check that the bins and colors have the same length."""
32
+ if (len(self.bins) + 1) != len(self.colors):
33
+ raise ValueError(
34
+ f"Number of bins ({len(self.bins)}) must be one less than number of colors ({len(self.colors)})"
35
+ )
36
+ return self
37
+
38
+
39
+ class FloodMapLayer(Layer):
40
+ zbmax: float
41
+ depth_min: float
42
+
43
+
44
+ class AggregationDmgLayer(Layer):
45
+ damage_decimals: Optional[int] = 0
46
+
47
+
48
+ class FootprintsDmgLayer(Layer):
49
+ type: DamageType = DamageType.absolute
50
+ damage_decimals: Optional[int] = 0
51
+ buildings_min_zoom_level: int = 13
52
+
53
+
54
+ class BenefitsLayer(Layer):
55
+ threshold: Optional[float] = None
56
+
57
+
58
+ class OutputLayers(BaseModel):
12
59
  """The configuration of the mapbox layers in the gui.
13
60
 
14
61
  Attributes
15
62
  ----------
16
- buildings_min_zoom_level : int
17
- The minimum zoom level for the buildings layer.
18
- flood_map_depth_min : float
19
- The minimum depth for the flood map layer.
20
- flood_map_zbmax : float
21
- The maximum depth for the flood map layer.
22
- flood_map_bins : list[float]
23
- The bins for the flood map layer.
24
- flood_map_colors : list[str]
25
- The colors for the flood map layer.
26
- aggregation_dmg_bins : list[float]
27
- The bins for the aggregation damage layer.
28
- aggregation_dmg_colors : list[str]
29
- The colors for the aggregation damage layer.
30
- footprints_dmg_type : DamageType
31
- The type of damage for the footprints layer.
32
- footprints_dmg_bins : list[float]
33
- The bins for the footprints layer.
34
- footprints_dmg_colors : list[str]
35
- The colors for the footprints layer.
36
- svi_bins : Optional[list[float]]
37
- The bins for the SVI layer.
38
- svi_colors : Optional[list[str]]
39
- The colors for the SVI layer.
40
- benefits_bins : list[float]
41
- The bins for the benefits layer.
42
- benefits_colors : list[str]
43
- The colors for the benefits layer.
44
- benefits_threshold : Optional[float], default=None
45
- The threshold for the benefits layer.
46
- damage_decimals : Optional[int], default=0
47
- The number of decimals for the damage layer.
63
+ floodmap : FloodMapLayer
64
+ The configuration of the floodmap layer.
65
+ aggregation_dmg : AggregationDmgLayer
66
+ The configuration of the aggregation damage layer.
67
+ footprints_dmg : FootprintsDmgLayer
68
+ The configuration of the footprints damage layer.
48
69
 
70
+ benefits : BenefitsLayer
71
+ The configuration of the benefits layer.
49
72
  """
50
73
 
51
- buildings_min_zoom_level: int = 13
52
- flood_map_depth_min: float
53
- flood_map_zbmax: float
54
- flood_map_bins: list[float]
55
- flood_map_colors: list[str]
56
- aggregation_dmg_bins: list[float]
57
- aggregation_dmg_colors: list[str]
58
- footprints_dmg_type: DamageType = DamageType.absolute
59
- footprints_dmg_bins: list[float]
60
- footprints_dmg_colors: list[str]
61
- svi_bins: Optional[list[float]] = Field(default_factory=list)
62
- svi_colors: Optional[list[str]] = Field(default_factory=list)
63
- benefits_bins: list[float]
64
- benefits_colors: list[str]
65
- benefits_threshold: Optional[float] = None
66
- damage_decimals: Optional[int] = 0
74
+ floodmap: FloodMapLayer
75
+ aggregation_dmg: AggregationDmgLayer
76
+ footprints_dmg: FootprintsDmgLayer
77
+
78
+ benefits: Optional[BenefitsLayer] = None
67
79
 
68
80
 
69
- class VisualizationLayersModel(BaseModel):
81
+ class VisualizationLayer(Layer):
82
+ """The configuration of a layer to visualize in the gui.
83
+
84
+ name : str
85
+ The name of the layer to visualize.
86
+ long_name : str
87
+ The long name of the layer to visualize.
88
+ path : str
89
+ The path to the layer data to visualize.
90
+ field_name : str
91
+ The field names of the layer to visualize.
92
+ decimals : Optional[int]
93
+ The number of decimals to use for the layer to visualize. default is None.
94
+ """
95
+
96
+ name: str
97
+ long_name: str
98
+ path: str
99
+ field_name: str
100
+ decimals: Optional[int] = None
101
+
102
+
103
+ _DEFAULT_BIN_NR = 4
104
+
105
+
106
+ def interpolate_hex_colors(
107
+ start_hex="#FFFFFF", end_hex="#860000", number_bins=_DEFAULT_BIN_NR
108
+ ):
109
+ """
110
+ Interpolate between two hex colors and returns a list of number_bins hex color codes.
111
+
112
+ Parameters
113
+ ----------
114
+ start_hex : str
115
+ Starting color in hex format (e.g., "#FFFFFF").
116
+ end_hex : str
117
+ Ending color in hex format (e.g., "#000000").
118
+ number_bins : int
119
+ Number of colors to generate between the start and end colors.
120
+
121
+ Returns
122
+ -------
123
+ list[str]
124
+ List of hex color codes interpolated between the start and end colors.
125
+ """
126
+
127
+ def hex_to_rgb(hex_color):
128
+ hex_color = hex_color.lstrip("#")
129
+ return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
130
+
131
+ def rgb_to_hex(rgb_color):
132
+ return "#{:02X}{:02X}{:02X}".format(*rgb_color)
133
+
134
+ start_rgb = hex_to_rgb(start_hex)
135
+ end_rgb = hex_to_rgb(end_hex)
136
+
137
+ interpolated_colors = []
138
+ for i in range(number_bins):
139
+ ratio = i / (number_bins - 1) if number_bins > 1 else 0
140
+ interpolated_rgb = tuple(
141
+ int(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb)
142
+ )
143
+ interpolated_colors.append(rgb_to_hex(interpolated_rgb))
144
+
145
+ return interpolated_colors
146
+
147
+
148
+ class VisualizationLayers(BaseModel):
70
149
  """The configuration of the layers you might want to visualize in the gui.
71
150
 
72
151
  Attributes
73
152
  ----------
74
- default_bin_number : int
75
- The default number of bins for the visualization layers.
76
- default_colors : list[str]
77
- The default colors for the visualization layers.
78
- layer_names : list[str]
79
- The names of the layers to visualize.
80
- layer_long_names : list[str]
81
- The long names of the layers to visualize.
82
- layer_paths : list[str]
83
- The paths to the layers to visualize.
84
- field_names : list[str]
85
- The field names of the layers to visualize.
86
- bins : Optional[list[list[float]]]
87
- The bins for the layers to visualize.
88
- colors : Optional[list[list[str]]]
89
- The colors for the layers to visualize.
153
+ default : Layer
154
+ The default layer settings the visualization layers.
155
+ layers : list[VisualizationLayer]
156
+ The layers to visualize.
90
157
  """
91
158
 
92
- # TODO add check for default_bin_number and default_colors to have the same length
93
- default_bin_number: int
94
- default_colors: list[str]
95
- layer_names: list[str] = Field(default_factory=list)
96
- layer_long_names: list[str] = Field(default_factory=list)
97
- layer_paths: list[str] = Field(default_factory=list)
98
- field_names: list[str] = Field(default_factory=list)
99
- bins: Optional[list[list[float]]] = Field(default_factory=list)
100
- colors: Optional[list[list[str]]] = Field(default_factory=list)
159
+ layers: list[VisualizationLayer] = Field(default_factory=list)
160
+
161
+ def add_layer(
162
+ self,
163
+ name: str,
164
+ long_name: str,
165
+ path: str,
166
+ field_name: str,
167
+ database_path: Path,
168
+ decimals: Optional[int] = None,
169
+ bins: Optional[list[float]] = None,
170
+ colors: Optional[list[str]] = None,
171
+ ) -> None:
172
+ if not Path(path).is_absolute():
173
+ raise ValueError(f"Path {path} must be absolute.")
174
+
175
+ data = gpd.read_file(path)
176
+ if field_name not in data.columns:
177
+ raise ValueError(
178
+ f"Field name {field_name} not found in data. Available fields: {data.columns.tolist()}"
179
+ )
180
+
181
+ if bins is None:
182
+ _, _bins = pd.qcut(
183
+ data[field_name], _DEFAULT_BIN_NR, retbins=True, duplicates="drop"
184
+ )
185
+ bins = _bins.tolist()[1:-1]
186
+
187
+ if decimals is None:
188
+ non_zero_bins = [abs(b) for b in bins if b != 0]
189
+ min_non_zero = min(non_zero_bins) if non_zero_bins else 1
190
+ decimals = max(int(-np.floor(np.log10(min_non_zero))), 0)
191
+
192
+ if colors is None:
193
+ nr_bins = len(bins) + 1
194
+ colors = interpolate_hex_colors(number_bins=nr_bins)
195
+
196
+ relative_path = Path(path).relative_to(database_path / "static")
197
+ self.layers.append(
198
+ VisualizationLayer(
199
+ bins=bins,
200
+ colors=colors,
201
+ name=name,
202
+ long_name=long_name,
203
+ path=relative_path.as_posix(),
204
+ field_name=field_name,
205
+ decimals=decimals,
206
+ )
207
+ )
101
208
 
102
209
 
103
210
  class GuiUnitModel(BaseModel):
@@ -203,22 +310,22 @@ class GuiModel(BaseModel):
203
310
  ----------
204
311
  units : GuiUnitModel
205
312
  The unit system used in the GUI.
206
- mapbox_layers : MapboxLayersModel
313
+ output_layers : OutputLayers
207
314
  The configuration of the mapbox layers in the GUI.
208
- visualization_layers : VisualizationLayersModel
315
+ visualization_layers : VisualizationLayers
209
316
  The configuration of the visualization layers in the GUI.
210
317
  plotting : PlottingModel
211
318
  The configuration for creating hazard forcing plots.
212
319
  """
213
320
 
214
321
  units: GuiUnitModel
215
- mapbox_layers: MapboxLayersModel
216
- visualization_layers: VisualizationLayersModel
322
+ output_layers: OutputLayers
323
+ visualization_layers: VisualizationLayers
217
324
  plotting: PlottingModel
218
325
 
219
326
  @staticmethod
220
327
  def read_toml(path: Path) -> "GuiModel":
221
328
  with open(path, mode="rb") as fp:
222
- toml_contents = load_toml(fp)
329
+ toml_contents = tomli.load(fp)
223
330
 
224
331
  return GuiModel(**toml_contents)