├── README.md ├── src └── dfasttf │ ├── .png │ ├── __init__.py │ ├── ship_dimensions.ini │ ├── cmd.py │ ├── batch │ ├── support.py │ ├── ice.py │ ├── cross_flow.py │ ├── core.py │ ├── operations.py │ ├── geometry.py │ ├── dflowfm.py │ └── plotting.py │ ├── kernel │ ├── froude.py │ └── flow.py │ ├── __main__.py │ ├── config.py │ └── Dutch_rivers_v3.ini ├── pyproject.toml ├── .gitignore ├── LICENSE.md └── docs └── rapport.tex /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dfasttf/.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deltares/D-FAST_transverse_flow/main/src/dfasttf/.png -------------------------------------------------------------------------------- /src/dfasttf/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typing import Any, List 4 | 5 | __version__ = "0.0.1" 6 | 7 | __all__: List[Any] = [] 8 | -------------------------------------------------------------------------------- /src/dfasttf/ship_dimensions.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Version = 1.0 3 | 4 | [Bovenrijn] 5 | Length = 269.5 6 | Depth = 4.5 7 | 8 | [Waal] 9 | Length = 269.5 10 | Depth = 4.5 11 | 12 | [Pannerdensch Kanaal] 13 | Length = 186.5 14 | Depth = 3.5 15 | 16 | [Nederrijn-Lek] 17 | Length = 186.5 18 | Depth = 3.5 19 | 20 | [IJssel] 21 | Length = 110.0 22 | Depth = 3.5 23 | 24 | [Zwarte Water] 25 | Length = 110.0 26 | Depth = 3.25 27 | 28 | [Maas] 29 | Length = 193.0 30 | Depth = 3.5 31 | 32 | [Merwedes] 33 | Length = 225.0 34 | Depth = 4.5 35 | 36 | [Amer] 37 | Length = 225.0 38 | Depth = 4.75 39 | 40 | [Haringvliet] 41 | Length = 225.0 42 | Depth = 4.75 43 | 44 | [Hollands Diep] 45 | Length = 225.0 46 | Depth = 4.75 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/dfasttf/cmd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from dfasttf.batch.core import preprocess_1d, run_analysis 4 | from dfasttf.batch.dflowfm import Variables 5 | from dfasttf.config import Config 6 | 7 | logging.basicConfig(filename="dfasttf.log", level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | # TODO: make figfiles optional, now depends on SavePlots=True 12 | def run(config_file: str, ships_file: str) -> None: 13 | """Main entry point for running the analysis.""" 14 | logger.info("Running analysis...") 15 | 16 | configuration = Config(config_file, ships_file) 17 | 18 | variables = Variables( 19 | h="mesh2d_waterdepth", 20 | uc="mesh2d_ucmag", 21 | ucx="mesh2d_ucx", 22 | ucy="mesh2d_ucy", 23 | bl="mesh2d_flowelem_bl", 24 | ) 25 | 26 | prof_line_df = None 27 | prof_line_df, riverkm = preprocess_1d(configuration) 28 | 29 | for section in configuration.keys(): 30 | if "Reference" in configuration.config[section]: 31 | run_analysis(configuration, section, variables, prof_line_df, riverkm) 32 | 33 | logger.info("Finished analysis.") 34 | -------------------------------------------------------------------------------- /src/dfasttf/batch/support.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pandas as pd 5 | 6 | 7 | def get_abs_path(rootdir, filename): 8 | return Path(rootdir / filename).resolve() 9 | 10 | 11 | def to_csv(outputfile: Path, column_labels: tuple, *column_values) -> None: 12 | if len(column_labels) != len(column_values): 13 | raise ValueError( 14 | "Number of column labels must match number of column value arrays." 15 | ) 16 | 17 | df = pd.DataFrame(np.column_stack(column_values), columns=column_labels) 18 | df.to_csv(outputfile, header=True, index=False, float_format="%.3f") 19 | 20 | 21 | def to_excel( 22 | writer: pd.ExcelWriter, column_labels: tuple, sheet_name: str, *column_values 23 | ) -> None: 24 | if len(column_labels) != len(column_values): 25 | raise ValueError( 26 | "Number of column labels must match number of column value arrays." 27 | ) 28 | 29 | df = pd.DataFrame(np.column_stack(column_values), columns=column_labels) 30 | df.to_excel( 31 | writer, sheet_name=sheet_name, header=True, index=False, float_format="%.3f" 32 | ) 33 | -------------------------------------------------------------------------------- /src/dfasttf/kernel/froude.py: -------------------------------------------------------------------------------- 1 | """Performs corrections on the modelled Froude number specific for the discharge of ice """ 2 | 3 | import numpy as np 4 | from xarray import DataArray 5 | 6 | 7 | def calculate_froude_number( 8 | water_depth: DataArray, flow_velocity: DataArray, grav_constant=9.81 9 | ) -> DataArray: 10 | """Calculates the Froude number from flow velocity and water depth""" 11 | froude_number = flow_velocity / np.sqrt(grav_constant * water_depth) 12 | return froude_number 13 | 14 | 15 | def water_uplift(froude_number: DataArray) -> DataArray: 16 | """correction from water level uplift due to downstream ice cover""" 17 | froude_corrected = froude_number / np.sqrt(2) 18 | return froude_corrected 19 | 20 | 21 | def bed_change( 22 | froude_number: DataArray, bedlevel_change: DataArray, water_depth: DataArray 23 | ) -> DataArray: 24 | """correction from bed level change due to the measure (as calculated by D-FAST-MI)""" 25 | # requires change in bed level calculated with d-fast-mi 26 | correction_term = (1 - bedlevel_change / water_depth) ** (-1.5) 27 | froude_corrected = froude_number * correction_term 28 | return froude_corrected 29 | -------------------------------------------------------------------------------- /src/dfasttf/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import dfasttf.cmd 4 | 5 | 6 | def parse_arguments() -> tuple: 7 | """ 8 | Parse the command line arguments. 9 | 10 | Arguments 11 | --------- 12 | None 13 | 14 | Returns 15 | ------- 16 | config_name : Optional[str] 17 | Name of the analysis configuration file. 18 | rivers_file : str 19 | Name of rivers configuration file. 20 | """ 21 | 22 | parser = argparse.ArgumentParser(description="D-FAST-RBK") 23 | 24 | parser.add_argument( 25 | "--config", 26 | default="unspecified", 27 | help="name of analysis configuration file ('%(default)s' is default)", 28 | ) 29 | 30 | parser.add_argument( 31 | "--rivers", 32 | default="unspecified", 33 | help="name of rivers configuration file ('Dutch_rivers_v3.ini' is default)", 34 | ) 35 | 36 | parser.add_argument( 37 | "--ships", 38 | default="unspecified", 39 | help="name of ship dimensions file ('ship_dimensions.ini' is default)", 40 | ) 41 | 42 | parser.set_defaults(reduced_output=False) 43 | args = parser.parse_args() 44 | 45 | config_file = args.__dict__["config"] 46 | rivers_file = args.__dict__["rivers"] 47 | ships_file = args.__dict__["ships"] 48 | if config_file == "unspecified": 49 | config_file = "examples/c01 - Waal/config.ini" 50 | if rivers_file == "unspecified": 51 | rivers_file = "Dutch_rivers_v3.ini" 52 | if ships_file == "unspecified": 53 | # TODO: fix this path 54 | ships_file = "dfasttf/src/ship_dimensions.ini" 55 | 56 | return config_file, rivers_file, ships_file 57 | 58 | 59 | if __name__ == "__main__": 60 | config_file, rivers_file, ships_file = parse_arguments() 61 | dfasttf.cmd.run(config_file, ships_file) 62 | -------------------------------------------------------------------------------- /src/dfasttf/kernel/flow.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def face_len(face_x_bnd: np.ndarray, face_y_bnd: np.ndarray) -> np.ndarray: 5 | """ 6 | Calculates the length of mesh faces. 7 | face_x_bnd: x-coordinates of the face boundaries 8 | face_y_bnd: y-coordinates of the face boundaries 9 | """ 10 | dx = np.gradient(face_x_bnd.values, axis=0) 11 | dy = np.gradient(face_y_bnd.values, axis=0) 12 | face_len = np.sqrt(dx**2 + dy**2) # length of faces 13 | return face_len 14 | 15 | 16 | def trans_velocity(u0: np.ndarray, v0: np.ndarray, angles: np.ndarray) -> np.ndarray: 17 | """ 18 | Calculates the transversal (perpendicular) component of the flow velocity. 19 | u0: x-component of velocity 20 | v0: y-component of velocity 21 | angles: angles in degrees (0 degrees is to the right of the x-axis) 22 | """ 23 | 24 | angles_rad = np.radians(angles) # convert angles in degrees to radians 25 | w0 = u0 * (-np.sin(angles_rad)) + v0 * np.cos(angles_rad) 26 | return w0 27 | 28 | 29 | # def representative_trans_velocity(face_len: np.ndarray, 30 | # water_depth: np.ndarray, 31 | # trans_velocity: np.ndarray, 32 | # SHIP_DEPTH: float) -> np.ndarray: 33 | # """ 34 | # Calculates the representative transversal velocity at mesh faces according to RBK specifications. 35 | # """ 36 | 37 | # Q_trans = water_depth * face_len * trans_velocity # transversal discharge 38 | # urepr = Q_trans / (face_len * np.fmax(water_depth, SHIP_DEPTH)) # representative transversal velocity 39 | 40 | # return urepr 41 | 42 | 43 | def trans_discharge(u_integral: np.ndarray, SHIP_DEPTH: float) -> np.ndarray: 44 | """ 45 | Calculates the transversal discharge. 46 | u_integral = integral of flow velocity over cross-sectional width 47 | SHIP_DEPTH: depth of the ship 48 | """ 49 | 50 | q = u_integral * SHIP_DEPTH 51 | return q 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "d-fast-traverse-flow" 3 | version = "0.1.0" 4 | description = "A tool to perform a bank erosion analysis based on a number of D-Flow FM simulations." 5 | authors = ["Stichting Deltares "] 6 | readme = "README.md" 7 | packages = [{ include = "dfasttf", from= "src"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "~3.11.0" 11 | dfastio = { git = "https://github.com/Deltares/D-FAST_Commons.git", rev = "0.1.0" } 12 | d-fast-bank-erosion = { git = "https://github.com/Deltares/D-FAST_Bank_Erosion.git", branch = "build/bump-up-versions" } 13 | dfastmi = { git = "https://github.com/Deltares/D-FAST_Morphological_Impact", branch = "build/bump-up-versions"} 14 | tqdm = "^4.67.1" 15 | xugrid = "^0.14.3" 16 | matplotlib = "^3.8.4" 17 | 18 | 19 | 20 | [tool.poetry.group.dev.dependencies] 21 | pytest = "^7.2.1" 22 | pytest-cov = "^6.0.0" 23 | pyfakefs = "^5.1.0" 24 | teamcity-messages = "^1.32" 25 | pre-commit = "^4.2.0" 26 | commitizen = "^4.8.3" 27 | 28 | 29 | [tool.poetry.group.docs.dependencies] 30 | mkdocs = "^1.2" 31 | mkdocs-material = "^9.5" 32 | mkdocstrings = {extras = ["python"], version = "^0.27.0"} 33 | pymdown-extensions = "^10.12" 34 | mkdocs-autorefs = "^1.2" 35 | mkdocs-macros-plugin = "^1.3.7" 36 | mkdocs-table-reader-plugin = "^3.1.0" 37 | mkdocs-jupyter = "^0.25.1" 38 | mkdocs-mermaid2-plugin = "^1.2.1" 39 | mike = "^2.1.3" 40 | jupyter-contrib-nbextensions = "^0.7.0" 41 | notebook = "<7.0" 42 | 43 | 44 | 45 | [tool.coverage.run] 46 | omit = [ 47 | "tests/test_binaries/*", 48 | ] 49 | 50 | [tool.coverage.report] 51 | show_missing = true 52 | fail_under = 61 53 | 54 | 55 | [tool.flake8] 56 | ignore = ["E501", "E203", "F821", "E722", "B001", "D401", "B006", "D202", "W503"] 57 | max-line-length = 88 58 | 59 | [tool.black] 60 | profile = "flake8" 61 | line-length = 88 62 | skip-string-normalization = true # Avoid unnecessary quote changes 63 | 64 | [tool.isort] 65 | profile = "black" 66 | line_length = 88 67 | multi_line_output = 3 68 | 69 | [tool.commitizen] 70 | name = "cz_conventional_commits" 71 | version_provider = "poetry" 72 | tag_format = "$version" 73 | version_files = [ 74 | "src/dfastbe/__init__.py:__version__", 75 | ] 76 | changelog_file = "docs/mkdocs/change-log.md" 77 | update_changelog_on_bump = true 78 | changelog_incremental = true 79 | version_scheme = "pep440" 80 | 81 | 82 | 83 | [tool.pytest.ini_options] 84 | addopts = [ "--cov", 85 | "--cov-report=term", 86 | "--cov-report=html", 87 | "--cov-report=xml:coverage-reports/coverage.xml", 88 | ] 89 | testpaths="tests" 90 | 91 | markers = [ 92 | "e2e: marks tests as e2e (deselect with '-m \"not e2e\"')", 93 | "binaries: marks tests as binaries (deselect with '-m \"not binaries\"')", 94 | "unit: marks tests as unit (deselect with '-m \"not unit\"')", 95 | "integration: marks tests as integration (deselect with '-m \"not integration\"')", 96 | ] 97 | 98 | [build-system] 99 | requires = ["poetry-core>=1.0.0"] 100 | build-backend = "poetry.core.masonry.api" 101 | -------------------------------------------------------------------------------- /src/dfasttf/batch/ice.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import geopandas as gpd 4 | import numpy as np 5 | import pandas as pd 6 | import xarray as xr 7 | import xugrid as xu 8 | from xarray import DataArray 9 | from xugrid import UgridDataArray 10 | 11 | from dfasttf.batch import geometry, plotting, support 12 | from dfasttf.batch.dflowfm import clip_simulation_data 13 | from dfasttf.config import Config 14 | from dfasttf.kernel import froude 15 | 16 | 17 | def run_1d( 18 | uc: list[np.ndarray], 19 | ucx: list[np.ndarray], 20 | ucy: list[np.ndarray], 21 | profile_angles: np.ndarray, 22 | rkm: np.ndarray, 23 | configuration: Config, 24 | figfile: Path, 25 | outputfile: Path, 26 | ) -> None: 27 | 28 | COLUMN_LABELS = ( 29 | "afstand (rkm)", 30 | "stroomsnelheid (m/s)", 31 | "stromingshoek (graden)", 32 | "profiellijn (graden)", 33 | "stromingshoek t.o.v. profiellijn (graden)", 34 | ) 35 | 36 | velocity_magnitude = [] 37 | velocity_angle = [] 38 | angle_diff = [] 39 | rkm_km = rkm / 1000 40 | 41 | for m, x, y in zip(uc, ucx, ucy): 42 | velocity_magnitude.append(m) 43 | flow_angle = geometry.vector_angle(x, y) 44 | velocity_angle.append(flow_angle) 45 | 46 | # shortest angular difference 47 | angle_diff = [ 48 | (angles - profile_angles + 180) % 360 - 180 for angles in velocity_angle 49 | ] 50 | angle_diff = [np.where(angles > 90, angles - 180, angles) for angles in angle_diff] 51 | angle_diff = [np.where(angles < -90, angles + 180, angles) for angles in angle_diff] 52 | 53 | labels = ["Reference", "WithIntervention", "Difference"] 54 | data = [ 55 | (velocity_magnitude[0], velocity_angle[0], angle_diff[0]), 56 | ( 57 | (velocity_magnitude[1], velocity_angle[1], angle_diff[1]) 58 | if len(velocity_magnitude) > 1 59 | else None 60 | ), 61 | ( 62 | ( 63 | velocity_magnitude[1] - velocity_magnitude[0], 64 | velocity_angle[1] - velocity_angle[0], 65 | angle_diff[1] - angle_diff[0], 66 | ) 67 | if len(velocity_magnitude) > 1 68 | else None 69 | ), 70 | ] 71 | 72 | with pd.ExcelWriter(outputfile) as writer: 73 | for label, d in zip(labels, data): 74 | if d is not None: 75 | support.to_excel( 76 | writer, 77 | COLUMN_LABELS, 78 | label, 79 | rkm_km, 80 | d[0], 81 | d[1], 82 | profile_angles, 83 | d[2], 84 | ) 85 | 86 | plotting.Ice1D().create_figure( 87 | rkm, velocity_magnitude, angle_diff, configuration, figfile 88 | ) 89 | 90 | 91 | def run_2d( 92 | water_depth: list[DataArray], 93 | flow_velocity: list[DataArray], 94 | configuration: Config, 95 | filenames: list[Path], 96 | ) -> None: 97 | 98 | riverkm = configuration.general.riverkm 99 | if configuration.general.profiles_file != "": 100 | profile_lines = gpd.read_file(Path(configuration.general.profiles_file)) 101 | 102 | froude_number = [] 103 | for idx, (h, u) in enumerate(zip(water_depth, flow_velocity)): 104 | fr = froude.calculate_froude_number(h, u) 105 | fr = correct_model_results(fr, h, configuration) 106 | froude_number.append(fr) 107 | plotting.Ice2D().create_map(fr, riverkm, profile_lines, filenames[idx]) 108 | 109 | if len(froude_number) > 1: 110 | plotting.Ice2D().create_diff_map( 111 | froude_number[0], froude_number[1], riverkm, profile_lines, filenames[2] 112 | ) 113 | 114 | 115 | def correct_model_results( 116 | froude_number: DataArray, water_depth: DataArray, configuration: Config 117 | ) -> DataArray: 118 | water_uplift = configuration.general.bool_flags["waterupliftcorrection"] 119 | bed_change = configuration.general.bool_flags["bedchangecorrection"] 120 | bed_change_file = configuration.general.bedchangefile 121 | bbox = configuration.general.bbox 122 | if bed_change: 123 | if bed_change_file is None: 124 | raise ValueError("No bed change file specified in configuration.") 125 | bedlevel_change = get_bedlevel_change(bed_change_file, bbox) 126 | froude_number = froude.bed_change(froude_number, bedlevel_change, water_depth) 127 | if water_uplift: 128 | froude_number = froude.water_uplift(froude_number) 129 | return froude_number 130 | 131 | 132 | def get_bedlevel_change(file: Path, bbox: list) -> UgridDataArray: 133 | ds = xu.open_dataset(file) 134 | dfast_name = "avgdzb" 135 | data_vars = list(ds.data_vars) 136 | if dfast_name in data_vars: 137 | da = ds[dfast_name] 138 | elif len(data_vars) == 1: 139 | da = ds[data_vars[0]] 140 | else: 141 | raise IOError(f"NetCDF file must contain {dfast_name} or exactly one variable.") 142 | 143 | return clip_simulation_data(da, bbox) 144 | -------------------------------------------------------------------------------- /src/dfasttf/config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from dataclasses import dataclass 3 | from pathlib import Path 4 | 5 | from dfastio import xyc 6 | from shapely import LineString 7 | 8 | from dfastmi.batch.core import _get_output_dir 9 | from dfastmi.batch.PlotOptions import PlotOptions 10 | from dfastmi.config.ConfigFileOperations import ConfigFileOperations 11 | from dfastmi.io.DFastAnalysisConfigFileParser import DFastAnalysisConfigFileParser 12 | 13 | GENERAL_SECTION = "General" 14 | BOUNDING_BOX_SECTION = "BoundingBox" 15 | BEDCHANGEFILE_KEY = "BedChangeFile" 16 | WITHINTERVENTION_KEY = "WithIntervention" 17 | 18 | 19 | @dataclass 20 | class Ship: 21 | length: float 22 | depth: float 23 | 24 | @classmethod 25 | def from_config(cls, reach: str, ships_file: Path) -> "Ship": 26 | config = ConfigParser() 27 | config.read(ships_file) 28 | try: 29 | length = float(config[reach]["Length"]) 30 | depth = float(config[reach]["Depth"]) 31 | except KeyError as e: 32 | raise ValueError(f"Missing key in ships file for reach '{reach}': {e}") 33 | return cls(length=length, depth=depth) 34 | 35 | 36 | class Config: 37 | """ 38 | Loads and manages configuration for D-FAST analysis. 39 | """ 40 | 41 | def __init__(self, config_file: str, ships_file: str): 42 | configfile = Path(config_file).resolve() 43 | self.configdir = configfile.parent 44 | 45 | self.config = ConfigFileOperations.load_configuration_file(str(config_file)) 46 | self.keys = self.config.keys 47 | 48 | self.data = DFastAnalysisConfigFileParser(self.config) 49 | self.general = GeneralSettings.from_config( 50 | self.data, self.config, self.configdir 51 | ) 52 | self.outputdir = _get_output_dir(str(self.configdir), True, self.data) 53 | 54 | shipsfile = Path(ships_file).resolve() 55 | self.ship_params = Ship.from_config(self.general.reach, shipsfile) 56 | 57 | self.plotsettings = PlotSettings(self.configdir, self.data) 58 | 59 | 60 | def get_output_files(config: ConfigParser, configdir: Path, section: str): 61 | """ 62 | Adds output files from config file section to configuration. 63 | """ 64 | output_files = [] 65 | 66 | reference_file = config.get(section, "Reference") 67 | output_files.append( 68 | ConfigFileOperations._get_absolute_path_from_relative_path( 69 | str(configdir), reference_file 70 | ) 71 | ) 72 | 73 | if WITHINTERVENTION_KEY in config[section]: 74 | with_intervention = config.get(section, "WithIntervention") 75 | output_files.append( 76 | ConfigFileOperations._get_absolute_path_from_relative_path( 77 | str(configdir), with_intervention 78 | ) 79 | ) 80 | return output_files 81 | 82 | 83 | @dataclass 84 | class GeneralSettings: 85 | """Sets the general settings""" 86 | 87 | branch: str 88 | reach: str 89 | bool_flags: dict 90 | riverkm: LineString | None 91 | profiles_file: Path | None 92 | bedchangefile: Path | None 93 | bbox: list | None 94 | 95 | @classmethod 96 | def from_config( 97 | cls, data: DFastAnalysisConfigFileParser, config: ConfigParser, configdir: Path 98 | ) -> "GeneralSettings": 99 | reach = data.getstring(GENERAL_SECTION, "Reach") 100 | branch = data.getstring(GENERAL_SECTION, "Branch") 101 | 102 | bool_flags = { 103 | flag.lower(): data.getboolean(GENERAL_SECTION, flag, fallback=False) 104 | for flag in ["InvertXAxis", "WaterUpliftCorrection", "BedChangeCorrection"] 105 | } 106 | 107 | riverkm = None 108 | riverkm_file = data.getstring(GENERAL_SECTION, "RiverKM") 109 | riverkm = xyc.models.XYCModel.read(riverkm_file, num_columns=3) 110 | 111 | profiles_file = None 112 | profiles_file = Path( 113 | ConfigFileOperations._get_absolute_path_from_relative_path( 114 | str(configdir), data.getstring(GENERAL_SECTION, "ProfileLines") 115 | ) 116 | ) 117 | 118 | bedchangefile = None 119 | if BEDCHANGEFILE_KEY in config[GENERAL_SECTION]: 120 | bedchangefile = Path( 121 | ConfigFileOperations._get_absolute_path_from_relative_path( 122 | str(configdir), data.getstring(GENERAL_SECTION, BEDCHANGEFILE_KEY) 123 | ) 124 | ) 125 | 126 | bbox = None 127 | if BOUNDING_BOX_SECTION in config: 128 | bbox = [ 129 | float(config[BOUNDING_BOX_SECTION][key]) 130 | for key in config[BOUNDING_BOX_SECTION] 131 | ] 132 | 133 | return cls( 134 | branch=branch, 135 | reach=reach, 136 | bool_flags=bool_flags, 137 | riverkm=riverkm, 138 | profiles_file=profiles_file, 139 | bedchangefile=bedchangefile, 140 | bbox=bbox, 141 | ) 142 | 143 | 144 | class PlotSettings: 145 | def __init__(self, config_dir: Path, data: DFastAnalysisConfigFileParser): 146 | self.type = data.getstring(GENERAL_SECTION, "PlotType", "both") 147 | self.options = PlotOptions() 148 | self.options.set_plotting_flags(config_dir, False, data) 149 | -------------------------------------------------------------------------------- /src/dfasttf/Dutch_rivers_v3.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Version = 3.0 3 | UCrit = 0.3 4 | CelerForm = 1 5 | checksum = 2374437366 6 | 7 | [Bovenrijn & Waal] 8 | QLocation = Lobith 9 | HydroQ = 1300 2000 3000 4000 6000 8000 10 | AutoTime = True 11 | QStagnant = 800 12 | QFit = 800 1280 13 | 14 | Reach1 = Bovenrijn km 859-867 15 | NWidth1 = 340 16 | PropQ1 = 1300 2000 3000 4000 6000 8000 17 | PropC1 = 0.42 0.98 1.86 2.63 4.79 8.76 18 | 19 | Reach2 = Boven-Waal km 868-886 20 | NWidth2 = 260 21 | PropQ2 = 1300 2000 3000 4000 6000 8000 22 | PropC2 = 0.63 0.97 1.51 2.06 3.16 5.35 23 | 24 | Reach3 = Midden-Waal km 887-915 25 | NWidth3 = 260 26 | PropQ3 = 1300 2000 3000 4000 6000 8000 27 | PropC3 = 0.85 1.12 1.43 1.78 2.93 3.83 28 | 29 | Reach4 = Beneden-Waal km 916-951 30 | NWidth4 = 260 31 | PropQ4 = 1300 2000 3000 4000 6000 8000 32 | PropC4 = 0.47 0.94 1.70 2.70 5.04 6.86 33 | 34 | [Pannerdensch Kanaal & Nederrijn-Lek] 35 | QLocation = Lobith 36 | HydroQ = 1300 2000 3000 4000 6000 8000 37 | AutoTime = True 38 | QFit = 800 1280 39 | 40 | Reach1 = Pannerdensch Kanaal km 868-879 41 | NWidth1 = 140 42 | QStagnant1 = 800 43 | PropQ1 = 1300 2000 3000 4000 6000 8000 44 | PropC1 = 0.03 0.56 2.10 3.15 5.79 9.99 45 | 46 | Reach2 = Nederrijn stuwpand Driel km 880-891 47 | NWidth2 = 100 48 | QStagnant2 = 1500 49 | PropQ2 = 1300 2000 3000 4000 6000 8000 50 | PropC2 = 0.00 0.21 2.17 3.03 3.13 3.59 51 | 52 | Reach3 = Nederrijn stuwpand Amerongen km 892-922 53 | NWidth3 = 115 54 | QStagnant3 = 1500 55 | PropQ3 = 1300 2000 3000 4000 6000 8000 56 | PropC3 = 0.79 1.14 1.61 1.99 2.61 3.04 57 | 58 | Reach4 = Lek stuwpand Hagestein km 923-947 59 | NWidth4 = 130 60 | QStagnant4 = 1500 61 | PropQ4 = 1300 2000 3000 4000 6000 8000 62 | PropC4 = 0.41 0.90 1.93 3.09 4.56 6.00 63 | 64 | Reach5 = Lek km 948-989 65 | NWidth5 = 165 66 | QStagnant5 = 1500 67 | PropQ5 = 1300 2000 3000 4000 6000 8000 68 | PropC5 = 0.00 0.05 0.85 2.54 11.68 30.62 69 | 70 | [IJssel] 71 | QLocation = Lobith 72 | HydroQ = 1300 2000 3000 4000 6000 8000 73 | AutoTime = True 74 | QStagnant = 800 75 | QFit = 800 1280 76 | 77 | Reach1 = Boven-IJssel km 880-930 78 | NWidth1 = 80 79 | PropQ1 = 1300 2000 3000 4000 6000 8000 80 | PropC1 = 1.23 1.59 1.98 2.37 3.40 3.89 81 | 82 | Reach2 = Midden-IJssel km 931-970 83 | NWidth2 = 95 84 | PropQ2 = 1300 2000 3000 4000 6000 8000 85 | PropC2 = 0.74 1.18 1.86 2.58 3.50 3.26 86 | 87 | Reach3 = Beneden-IJssel km 970-1000 88 | NWidth3 = 140 89 | PropQ3 = 1300 2000 3000 4000 6000 8000 90 | PropC3 = 0.26 0.56 1.38 3.12 8.78 18.67 91 | 92 | [Merwedes] 93 | QLocation = Lobith 94 | HydroQ = 1300 2000 3000 4000 6000 8000 95 | AutoTime = True 96 | QStagnant = 800 97 | QFit = 800 1280 98 | 99 | Reach1 = Bovenmerwede km 951-961 100 | NWidth1 = 420 101 | PropQ1 = 1300 2000 3000 4000 6000 8000 102 | PropC1 = 0.06 0.31 1.21 3.72 11.66 20.51 103 | 104 | Reach2 = Beneden Merwede km 962-977 105 | NWidth2 = 240 106 | PropQ2 = 1300 2000 3000 4000 6000 8000 107 | PropC2 = 0.06 0.31 1.21 3.72 11.66 20.51 108 | 109 | Reach3 = Nieuwe Merwede km 962-980 110 | NWidth3 = 520 111 | PropQ3 = 1300 2000 3000 4000 6000 8000 112 | PropC3 = 0.06 0.31 1.21 3.72 11.66 20.51 113 | 114 | [Maas] 115 | QLocation = Borgharen 116 | HydroQ = 750 1300 1700 2100 2500 3200 117 | AutoTime = True 118 | QStagnant = 1000 119 | QFit = 0 300 120 | 121 | Reach1 = Grensmaas km 16-69 122 | NWidth1 = 100 123 | PropQ1 = 750 1300 1700 2100 2500 3200 124 | PropC1 = 0.003 0.01 0.01 0.02 0.02 0.03 125 | 126 | Reach2 = Linne-Roermond km 69-80 127 | NWidth2 = 140 128 | PropQ2 = 750 1300 1700 2100 2500 3200 129 | PropC2 = 0.58 1.26 1.02 0.75 0.23 0.09 130 | 131 | Reach3 = Roermond-Belfeld km 81-100 132 | NWidth3 = 140 133 | PropQ3 = 750 1300 1700 2100 2500 3200 134 | PropC3 = 1.13 2.74 2.13 1.72 1.95 2.22 135 | 136 | Reach4 = Belfeld Sambeek km 101-146 137 | NWidth4 = 140 138 | PropQ4 = 750 1300 1700 2100 2500 3200 139 | PropC4 = 0.95 3.62 3.70 4.04 4.52 5.74 140 | 141 | Reach5 = Sambeek-Grave km 147-175 142 | NWidth5 = 140 143 | PropQ5 = 750 1300 1700 2100 2500 3200 144 | PropC5 = 0.29 2.13 2.61 2.59 2.46 2.82 145 | 146 | Reach6 = Grave-Lith km 176-200 147 | NWidth6 = 140 148 | PropQ6 = 750 1300 1700 2100 2500 3200 149 | PropC6 = 0.60 5.40 5.98 5.50 5.53 6.34 150 | 151 | Reach7 = Lith-Ammerzoden km 201-227 152 | NWidth7 = 200 153 | PropQ7 = 750 1300 1700 2100 2500 3200 154 | PropC7 = 0.40 1.88 2.26 1.83 2.16 2.54 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | #poetry.toml 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 115 | #pdm.lock 116 | #pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # pixi 121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 122 | #pixi.lock 123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 124 | # in the .venv directory. It is recommended not to include this directory in version control. 125 | .pixi 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .envrc 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Abstra 179 | # Abstra is an AI-powered process automation framework. 180 | # Ignore directories containing user credentials, local state, and settings. 181 | # Learn more at https://abstra.io/docs 182 | .abstra/ 183 | 184 | # Visual Studio Code 185 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 186 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 187 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 188 | # you could uncomment the following to ignore the entire vscode folder 189 | # .vscode/ 190 | 191 | # Ruff stuff: 192 | .ruff_cache/ 193 | 194 | # PyPI configuration file 195 | .pypirc 196 | 197 | # Cursor 198 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 199 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 200 | # refer to https://docs.cursor.com/context/ignore-files 201 | .cursorignore 202 | .cursorindexingignore 203 | 204 | # Marimo 205 | marimo/_static/ 206 | marimo/_lsp/ 207 | __marimo__/ 208 | -------------------------------------------------------------------------------- /src/dfasttf/batch/cross_flow.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pandas as pd 5 | 6 | from dfasttf.batch import operations, plotting, support 7 | from dfasttf.config import Config 8 | from dfasttf.kernel import flow 9 | 10 | 11 | def run( 12 | ucx: list[np.ndarray], 13 | ucy: list[np.ndarray], 14 | path_distances: np.ndarray, 15 | profile_angles: np.ndarray, 16 | rkm: np.ndarray, 17 | configuration: Config, 18 | figfile: Path, 19 | outputfiles: Path, 20 | ) -> None: 21 | """ 22 | Input: 23 | ucx: (n,) 24 | x-component of flow velocity 25 | ucy: (n,) 26 | y-component of flow velocity 27 | path_distances: (n,) 28 | distance between intersection points 29 | profile_angles: (n,) 30 | angle of profile line segments 31 | rkm: (n,) 32 | projected riverkm values 33 | """ 34 | 35 | SHEET_LABELS = ("Reference", "WithIntervention", "Difference") 36 | CRITERIA: tuple[float, float] = (0.15, 0.3) # criteria for transverse velocity 37 | 38 | rkm_km = rkm / 1000 39 | 40 | # Transverse velocity: 41 | COLUMN_LABELS = ("afstand (rkm)", "dwarsstroomsnelheid (m/s)") 42 | transverse_velocity = [] 43 | for x, y in zip(ucx, ucy): 44 | cross_flow = flow.trans_velocity(x, y, profile_angles) 45 | transverse_velocity.append(cross_flow.compute()) 46 | 47 | data = [ 48 | transverse_velocity[0], 49 | transverse_velocity[1] if len(transverse_velocity) > 1 else None, 50 | ( 51 | transverse_velocity[1] - transverse_velocity[0] 52 | if len(transverse_velocity) > 1 53 | else None 54 | ), 55 | ] 56 | 57 | with pd.ExcelWriter(outputfiles[0]) as writer: 58 | for label, d in zip(SHEET_LABELS, data): 59 | if d is not None: 60 | support.to_excel(writer, COLUMN_LABELS, label, rkm_km, d) 61 | 62 | # Transverse discharge: 63 | COLUMN_LABELS = ( 64 | "start (rkm)", 65 | "eind (rkm)", 66 | "dwarsstroomdebiet (m3/s)", 67 | "max. dwarsstroomsnelheid magnitude (m3/s)", 68 | "criterium (m/s)", 69 | "overschrijding (0=FALSE,1=TRUE)", 70 | ) 71 | discharges, crit_values, xy_blocks = TransverseDischarge().compute( 72 | rkm, 73 | path_distances, 74 | transverse_velocity, 75 | configuration.ship_params.depth, 76 | configuration.ship_params.length, 77 | CRITERIA, 78 | ) 79 | 80 | data = [] 81 | for i, discharge in enumerate(discharges): 82 | data.append(prepare_data_for_excel(xy_blocks[i], discharge, crit_values[i])) 83 | 84 | with pd.ExcelWriter(outputfiles[1]) as writer: 85 | for label, d in zip(SHEET_LABELS, data): 86 | if d is not None: 87 | support.to_excel(writer, COLUMN_LABELS, label, *d) 88 | 89 | plotter = plotting.CrossFlow() 90 | plotter.create_figure( 91 | rkm, 92 | transverse_velocity, 93 | xy_blocks, 94 | crit_values, 95 | configuration.general.bool_flags["invertxaxis"], 96 | figfile, 97 | ) 98 | 99 | 100 | def prepare_data_for_excel(xy_block, discharge, crit_value): 101 | CONVERT_M_TO_KM = 1000 102 | x_start = [xy[0][0] / CONVERT_M_TO_KM for xy in xy_block] 103 | x_end = [xy[0][-1] / CONVERT_M_TO_KM for xy in xy_block] 104 | y_max = [max(abs(xy[1])) for xy in xy_block] 105 | exceedance = y_max > abs(crit_value) 106 | return (x_start, x_end, discharge, y_max, crit_value, exceedance) 107 | 108 | 109 | class TransverseDischarge: 110 | def prepare_data( 111 | self, 112 | rkm: np.ndarray, 113 | path_distance: np.ndarray, 114 | transverse_velocity: np.ndarray, 115 | ) -> tuple[np.ndarray, np.ndarray]: 116 | """Prepare data by densifying, inserting array roots and subsequently splitting into blocks.""" 117 | # because ship length is 0.5 m precision we first densify distance such that diff(distance) <= 0.5 m: 118 | 119 | path_distance_interp = operations.densify_array(path_distance, 0.5) 120 | 121 | transverse_velocity_interp = np.interp( 122 | path_distance_interp, path_distance, transverse_velocity 123 | ) 124 | rkm_interp = np.interp(path_distance_interp, path_distance, rkm) 125 | 126 | rkm_app, transverse_velocity_app, path_distance_app = ( 127 | operations.insert_array_roots( 128 | rkm_interp, transverse_velocity_interp, path_distance_interp 129 | ) 130 | ) 131 | rkm_split, transverse_velocity_split, path_distance_split = ( 132 | operations.split_into_blocks( 133 | rkm_app, transverse_velocity_app, path_distance_app 134 | ) 135 | ) 136 | 137 | return rkm_split, path_distance_split, transverse_velocity_split 138 | 139 | def compute( 140 | self, 141 | rkm: np.ndarray, 142 | path_distances: np.ndarray, 143 | transverse_velocity: list[np.ndarray], 144 | ship_depth: float, 145 | ship_length: float, 146 | criteria: tuple[float, float], 147 | ): 148 | """Computes the transverse discharge from transverse velocity, ship depth and ship length""" 149 | discharges = [] 150 | crit_values = [] 151 | xy_segments = [] 152 | 153 | for tv in transverse_velocity: 154 | rkm_split, path_distances_split, tv_split = self.prepare_data( 155 | rkm, path_distances, tv 156 | ) 157 | discharge_case = [] 158 | crit_case = [] 159 | xy_segments_case = [] 160 | 161 | for xi, prof_distance, yi in zip(rkm_split, path_distances_split, tv_split): 162 | if not np.any(yi): 163 | continue 164 | 165 | max_integral, max_indices = operations.max_rolling_integral( 166 | prof_distance, yi, ship_length 167 | ) 168 | discharge = flow.trans_discharge(max_integral, ship_depth) 169 | discharge_case.append(discharge) 170 | 171 | start_idx, end_idx = max_indices[0], max_indices[-1] + 1 172 | # indices_case.append((start_idx, end_idx)) 173 | 174 | xi_segment = xi[start_idx:end_idx] 175 | yi_segment = yi[start_idx:end_idx] 176 | xy_segments_case.append((xi_segment, yi_segment)) 177 | 178 | crit_case.append(criteria[1] if discharge < 50.0 else criteria[0]) 179 | 180 | discharges.append(np.array(discharge_case)) 181 | crit_values.append(np.array(crit_case)) 182 | xy_segments.append(xy_segments_case) 183 | 184 | return discharges, crit_values, xy_segments 185 | -------------------------------------------------------------------------------- /src/dfasttf/batch/core.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | from pandas import DataFrame 5 | from shapely import LineString 6 | from tqdm import tqdm 7 | from xugrid import UgridDataset 8 | 9 | from dfasttf.batch import cross_flow, dflowfm, ice 10 | from dfasttf.batch.dflowfm import ( 11 | Variables, 12 | clip_simulation_data, 13 | load_simulation_data, 14 | ) 15 | from dfasttf.batch.plotting import Plot2D, construct_figure_filename 16 | from dfasttf.config import Config 17 | 18 | 19 | def run_analysis( 20 | configuration: Config, 21 | section: str, 22 | variables: Variables, 23 | prof_line_df: DataFrame | None, 24 | riverkm: LineString | None, 25 | ): 26 | simulation_data = load_simulation_data(configuration, section) 27 | 28 | plot_actions = { 29 | "1D": lambda: run_1d_analysis( 30 | configuration, section, simulation_data, variables, prof_line_df, riverkm 31 | ), 32 | "2D": lambda: run_2d_analysis( 33 | configuration, section, simulation_data, variables, prof_line_df 34 | ), 35 | } 36 | plot_actions["both"] = lambda: (plot_actions["1D"], plot_actions["2D"]) 37 | 38 | try: 39 | plot_actions[configuration.plotsettings.type]() 40 | except KeyError as exc: 41 | raise ValueError( 42 | f"Unknown plot type {configuration.plotsettings.type}." 43 | ) from exc 44 | 45 | 46 | def preprocess_1d(configuration: Config) -> tuple[DataFrame, LineString]: 47 | prof_line_df = dflowfm.read_profile_lines(Path(configuration.general.profiles_file)) 48 | riverkm = configuration.general.riverkm 49 | return prof_line_df, riverkm 50 | 51 | 52 | def run_1d_analysis( 53 | configuration: Config, 54 | section: str, 55 | simulation_data: list[UgridDataset], 56 | variables: Variables, 57 | prof_line_df: DataFrame, 58 | riverkm: LineString, 59 | ): 60 | """Run 1D profile analysis and plotting.""" 61 | riverkm_coords = np.array(riverkm.coords) 62 | padding = 1000 # metres 63 | 64 | for geom_idx, profile_line in enumerate( 65 | tqdm(prof_line_df.geometry, desc="geometry", position=0, leave=True) 66 | ): 67 | profile_coords = np.array(profile_line.coords) 68 | profile_index = str(prof_line_df.iloc[geom_idx].name) 69 | profile_data = {var: [] for var in variables._fields} 70 | bounds = profile_line.bounds 71 | 72 | for idx, _ in enumerate( 73 | tqdm(simulation_data, desc="simulation data", position=0, leave=True) 74 | ): 75 | data = clip_simulation_data( 76 | simulation_data[idx], 77 | [ 78 | bounds[0] - padding, 79 | bounds[2] + padding, 80 | bounds[1] - padding, 81 | bounds[3] + padding, 82 | ], 83 | ) 84 | 85 | sliced_ugrid = dflowfm.slice_ugrid(data, profile_coords, riverkm_coords) 86 | if sliced_ugrid is None: 87 | continue 88 | 89 | rkm, path_distances, isegment, iface = sliced_ugrid 90 | angles = np.array(prof_line_df["angle"].iloc[geom_idx][isegment]) 91 | for var, name in variables._asdict().items(): 92 | profile_data[var].append(dflowfm.get_profile_data(data, name, iface)) 93 | 94 | if ( 95 | sliced_ugrid is None 96 | ): # profile line does not slice reference nor intervention simulation data 97 | continue 98 | 99 | save_1d_figures( 100 | configuration, 101 | section, 102 | profile_index, 103 | profile_data, 104 | angles, 105 | rkm, 106 | path_distances, 107 | ) 108 | 109 | bedlevel = data[variables.bl].where(lambda x: x != 999) 110 | figfile = construct_figure_filename( 111 | configuration.plotsettings.options.figure_save_directory, 112 | f"profile{profile_index}_location", 113 | configuration.plotsettings.options.plot_extension, 114 | ) 115 | Plot2D().plot_profile_line(profile_line, bedlevel, riverkm, figfile) 116 | 117 | 118 | def save_1d_figures( 119 | configuration: Config, 120 | section: str, 121 | profile_index: str, 122 | profile_data: dict, 123 | angles: np.ndarray, 124 | rkm: np.ndarray, 125 | path_distances: np.ndarray, 126 | ): 127 | """Generate and save 1D figures and CSV files.""" 128 | figdir = configuration.plotsettings.options.figure_save_directory 129 | figext = configuration.plotsettings.options.plot_extension 130 | outputdir = configuration.outputdir 131 | 132 | base = f"{section}_profile{profile_index}_velocity_angle" 133 | figfile = construct_figure_filename(figdir, base, figext) 134 | outputfile = (outputdir / base).with_suffix(".xlsx") 135 | ice.run_1d( 136 | profile_data["uc"], 137 | profile_data["ucx"], 138 | profile_data["ucy"], 139 | angles, 140 | rkm, 141 | configuration, 142 | figfile, 143 | outputfile, 144 | ) 145 | 146 | outputfiles = [] 147 | base = f"{section}_profile{profile_index}_transverse_flow" 148 | figfile = construct_figure_filename(figdir, base, figext) 149 | outputfiles.append((outputdir / base).with_suffix(".xlsx")) 150 | 151 | base = f"{section}_profile{profile_index}_transverse_discharge" 152 | figfile = construct_figure_filename(figdir, base, figext) 153 | outputfiles.append((outputdir / base).with_suffix(".xlsx")) 154 | 155 | cross_flow.run( 156 | profile_data["ucx"], 157 | profile_data["ucy"], 158 | path_distances, 159 | angles, 160 | rkm, 161 | configuration, 162 | figfile, 163 | outputfiles, 164 | ) 165 | 166 | 167 | def run_2d_analysis( 168 | configuration: Config, 169 | section: str, 170 | simulation_data: list[UgridDataset], 171 | variables: Variables, 172 | prof_line_df: DataFrame, 173 | ): 174 | """Run 2D Froude number analysis and plotting.""" 175 | labels = ("reference", "intervention", "difference") 176 | 177 | # TODO: this is already done in ice.run_2d: 178 | waterupliftcorrection = configuration.general.bool_flags["waterupliftcorrection"] 179 | bedchangecorrection = configuration.general.bool_flags["bedchangecorrection"] 180 | 181 | suffix = "" 182 | if waterupliftcorrection: 183 | suffix = suffix + "_wateruplift" 184 | if bedchangecorrection: 185 | suffix = suffix + "_bedchange" 186 | 187 | padding = 1000 # metres 188 | 189 | ## TODO: this is only built in for the report but depends on profile lines, 190 | # which should not be required for 2D analysis 191 | for geom_idx, profile_line in enumerate( 192 | tqdm(prof_line_df.geometry, desc="geometry", position=0, leave=True) 193 | ): 194 | bounds = profile_line.bounds 195 | bbox = [ 196 | bounds[0] - padding, 197 | bounds[2] + padding, 198 | bounds[1] - padding, 199 | bounds[3] + padding, 200 | ] 201 | 202 | water_depth = [ 203 | clip_simulation_data(ds[variables.h], bbox) for ds in simulation_data 204 | ] 205 | flow_velocity = [ 206 | clip_simulation_data(ds[variables.uc], bbox) for ds in simulation_data 207 | ] 208 | figfiles = [ 209 | construct_figure_filename( 210 | configuration.plotsettings.options.figure_save_directory, 211 | f"{section}_{label}_profile{geom_idx}_Froude{suffix}", 212 | configuration.plotsettings.options.plot_extension, 213 | ) 214 | for label in labels 215 | ] 216 | # TODO: make riverkm optional 217 | if water_depth[0].size != 0: 218 | ice.run_2d(water_depth, flow_velocity, configuration, figfiles) 219 | -------------------------------------------------------------------------------- /src/dfasttf/batch/operations.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | # def append_array_roots(x: np.ndarray, y: np.ndarray) -> tuple: 6 | # """ 7 | # Interpolate arrays and append the roots (zero-crossings) 8 | # """ 9 | 10 | # s = np.abs(np.diff(np.sign(y))).astype(bool) 11 | # z = x[:-1][s] + np.diff(x)[s]/(np.abs(y[1:][s]/y[:-1][s])+1) # x-position of zero-crossings, found by linear interpolation 12 | 13 | # x_appended = np.concatenate([x,z],axis=0) 14 | # y_appended = np.concatenate([y,np.zeros(len(z))],axis=0) 15 | # x_sorted = np.sort(x_appended) 16 | # sort_idx = np.argsort(x_appended) 17 | # y_sorted = np.take_along_axis(y_appended,sort_idx,axis=0) 18 | 19 | # # # Make sure there are zero crossings at the beginning and end 20 | # # if (y_appended[0] > almost_zero) | (y_appended[0] < -almost_zero): 21 | # # y_appended = np.insert(y_appended,0,0,axis=0) 22 | # # x_appended = np.insert(x_appended,0,x_appended[0],axis=0) 23 | 24 | # # if (y_appended[-1] > almost_zero) | (y_appended[-1] < -almost_zero): 25 | # # y_appended = np.insert(y_appended,-1,0,axis=0) 26 | # # x_appended = np.insert(x_appended,-1,x_appended[-1],axis=0) 27 | 28 | # return x_sorted, y_sorted 29 | 30 | 31 | def insert_array_roots( 32 | x: np.ndarray, y: np.ndarray, x2: np.ndarray | None = None 33 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]: 34 | """ 35 | Interpolate arrays and append the roots (zero-crossings) 36 | """ 37 | z = find_roots(x, y) 38 | # Insert zero-crossings to the original arrays 39 | # TODO: there's a small bug where the zero-crossing is inserted in front of an element with the same x 40 | idx = x.searchsorted(z, side="right") 41 | x_mod = np.insert(x, idx, z) 42 | y_mod = np.insert(y, idx, 0) 43 | if x2 is not None: 44 | z1 = find_roots(x2, y) 45 | x2_mod = np.insert(x2, idx, z1) 46 | return x_mod, y_mod, x2_mod 47 | 48 | 49 | def find_roots(x: np.ndarray, y: np.ndarray) -> np.ndarray: 50 | """Finds the x-position of zero-crossings""" 51 | s = np.abs(np.diff(np.sign(y))).astype(bool) 52 | return x[:-1][s] + np.diff(x)[s] / (np.abs(y[1:][s] / y[:-1][s]) + 1) 53 | 54 | 55 | def split_into_blocks( 56 | x: np.ndarray, y: np.ndarray, x2: np.ndarray | None = None 57 | ) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray] | None]: 58 | """Splits x and y into blocks, separated by 0 in y.""" 59 | x_split = [] 60 | y_split = [] 61 | if x2 is not None: 62 | x2_split = [] 63 | zero_ind = np.where(y == 0)[0] 64 | 65 | for i in range(len(zero_ind) - 1): 66 | start = zero_ind[i] 67 | end = zero_ind[i + 1] + 1 68 | x_split.append(x[start:end]) 69 | y_split.append(y[start:end]) 70 | if x2 is not None: 71 | x2_split.append(x2[start:end]) 72 | 73 | return x_split, y_split, x2_split 74 | 75 | 76 | def max_rolling_integral(x: np.ndarray, y: np.ndarray, window: float) -> tuple: 77 | """ 78 | Maximum absolute integral over forward rolling windows with width at most window, 79 | without interpolation, and allowing duplicates in x. 80 | 81 | The window for start index i includes indices [i, j] where j is the largest 82 | index such that x[j] - x[i] <= window (i.e., the width is not yet exceeded). 83 | The integral is computed via the trapezoidal rule using *only* the fully 84 | included segments (no partial last segment interpolation). 85 | 86 | If the full range x[-1] - x[0] < window, the integral is taken over the entire arrays. 87 | 88 | Parameters 89 | ---------- 90 | x : (n,) array_like 91 | x-coordinates (can be non-strictly increasing; duplicates allowed). 92 | y : (n,) array_like 93 | values at x 94 | window : float 95 | window width in x-units (must be > 0). 96 | 97 | Returns 98 | ------- 99 | max_abs_area : float 100 | Maximum absolute integral over any window with width <= window (or full array if range < window). 101 | best_i : int 102 | Start index of the window achieving max_abs_area. 103 | best_j : int 104 | End index of the window achieving max_abs_area. 105 | 106 | Notes 107 | ----- 108 | - No interpolation is performed. The end index j is chosen such that 109 | x[j] - x[i] <= window and including j+1 would exceed window. 110 | - Duplicates in x cause zero-width segments; they add no area and do not 111 | affect the width, but they can be included if they fit within the bound. 112 | - If n < 2, area is 0 and (0, 0) is returned for indices. 113 | - Time complexity: O(n). Memory: O(n). 114 | """ 115 | 116 | if x.ndim != 1 or y.ndim != 1 or x.size != y.size: 117 | raise ValueError("x and y must be 1-D arrays of the same length.") 118 | if not np.all(np.isfinite(x)) or not np.all(np.isfinite(y)): 119 | raise ValueError("x and y must be finite.") 120 | if window <= 0: 121 | raise ValueError("Window width window must be positive.") 122 | 123 | n = x.size 124 | if n <= 1: 125 | return 0.0, [0, 0] 126 | 127 | # If whole range is smaller than window, integrate over all data 128 | full_range = x[-1] - x[0] 129 | dx = np.diff(x) 130 | # (Strictly smaller than window per requirement; if equal, the algorithm naturally picks [0, n-1]) 131 | if full_range < window: 132 | seg = 0.5 * (y[1:] + y[:-1]) * dx 133 | total_area = np.sum(seg) 134 | return abs(float(total_area)), [0, n - 1] 135 | 136 | # Precompute cumulative trapezoidal integral: F[k] = ∫ from x[0] to x[k] (using full segments) 137 | seg = 0.5 * (y[1:] + y[:-1]) * dx # zero when dx == 0 138 | F = np.concatenate(([0.0], np.cumsum(seg))) 139 | 140 | max_abs_area = -np.inf 141 | best_i = 0 142 | best_j = 0 143 | 144 | j = 0 # right boundary pointer 145 | for i in range(n): 146 | j = max(j, i) 147 | # Advance j while including next point does NOT exceed width window 148 | # i.e., keep x[j+1] - x[i] <= window 149 | while j + 1 < n and (x[j + 1] - x[i]) <= window: 150 | j += 1 151 | 152 | # Area over [i, j] via cumulative difference; no partial last segment 153 | area_signed = F[j] - F[i] 154 | area_abs = abs(area_signed) 155 | 156 | if area_abs > max_abs_area: 157 | max_abs_area = area_abs 158 | best_i = i 159 | best_j = j 160 | 161 | return float(max_abs_area), [int(best_i), int(best_j)] 162 | 163 | 164 | def densify_array(x: np.ndarray, max_step: float) -> np.ndarray: 165 | """ 166 | Return a densified x array such that np.diff(x_new) <= max_step by inserting 167 | intermediate points between neighbors with too-large gaps. 168 | 169 | Parameters 170 | ---------- 171 | x : (n,) array_like 172 | Input x-values. Intended to be non-decreasing; duplicates allowed. 173 | max_step : float, optional 174 | Maximum allowed step between consecutive x-values (default 0.5). Must be > 0. 175 | 176 | Returns 177 | ------- 178 | x_new : (m,) ndarray 179 | Densified x array including all original points (unless keep_duplicates=False), 180 | with np.diff(x_new) <= max_step (up to tiny float roundoff). 181 | 182 | Notes 183 | ----- 184 | - Duplicates (gap == 0) are fine; they don't add area and don't require inserts. 185 | - For each gap g = x[i+1] - x[i] > 0, we insert n_add = ceil(g/max_step) - 1 points, 186 | placed with np.linspace so the new max sub-step is <= max_step. 187 | - Time complexity: O(n); memory: O(n + #inserted). 188 | """ 189 | if x.ndim != 1: 190 | raise ValueError("x must be a 1-D array.") 191 | if not np.all(np.isfinite(x)): 192 | raise ValueError("x must contain only finite values.") 193 | if max_step <= 0: 194 | raise ValueError("max_step must be positive.") 195 | 196 | if x.size <= 1: 197 | return x.copy() 198 | 199 | parts = [x[0:1]] 200 | for a, b in zip(x[:-1], x[1:]): 201 | gap = b - a 202 | if gap <= 0: 203 | # zero or negative (duplicate or decreasing segment) — just append b 204 | parts.append(np.array([b])) 205 | continue 206 | 207 | # Number of extra points to insert to ensure sub-step <= max_step 208 | n_add = max(0, int(math.ceil(gap / max_step) - 1)) 209 | if n_add > 0: 210 | # Place interior points evenly, excluding endpoints a and b 211 | mids = np.linspace(a, b, n_add + 2, endpoint=True)[1:-1] 212 | parts.append(mids) 213 | parts.append(np.array([b])) 214 | 215 | x_new = np.concatenate(parts) 216 | return x_new 217 | -------------------------------------------------------------------------------- /src/dfasttf/batch/geometry.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from pathlib import Path 3 | 4 | import geopandas as gpd 5 | import numpy as np 6 | import pandas 7 | import shapely 8 | from shapely import LineString 9 | 10 | 11 | def vector_angle(u0: np.ndarray, v0: np.ndarray) -> np.ndarray: 12 | return np.degrees(np.arctan2(v0, u0)) 13 | 14 | 15 | def calculate_dx_dy( 16 | x_coords: np.ndarray, y_coords: np.ndarray 17 | ) -> tuple[np.ndarray, np.ndarray]: 18 | """ 19 | Calculate the differences in x and y coordinates between consecutive points. 20 | 21 | Parameters: 22 | x_coords (np.ndarray): Array of x coordinates. 23 | y_coords (np.ndarray): Array of y coordinates. 24 | 25 | Returns: 26 | tuple: Two arrays containing the differences in x and y coordinates. 27 | """ 28 | dx = np.diff(x_coords) 29 | dy = np.diff(y_coords) 30 | 31 | return dx, dy 32 | 33 | 34 | def calculate_curve_distance(x_coords: np.ndarray, y_coords: np.ndarray) -> np.ndarray: 35 | """Calculate the cumulative distance along a curve.""" 36 | # Ensure the coordinates are of the same length 37 | if len(x_coords) != len(y_coords): 38 | raise ValueError("The arrays of x and y coordinates must have the same length.") 39 | 40 | dx, dy = calculate_dx_dy(x_coords, y_coords) 41 | 42 | # Calculate the distance between each pair of points 43 | distances = np.linalg.norm(np.column_stack((dx, dy)), axis=1) 44 | 45 | # Calculate the cumulative distance 46 | cumulative_distances = np.concatenate(([0], np.cumsum(distances))) 47 | 48 | return cumulative_distances 49 | 50 | 51 | def find_distances_to_points(line_coords: np.ndarray, new_points: np.ndarray): 52 | """Vectorized function to find the distances along the line to new points 53 | array([], dtype=float64) 54 | Returns: 55 | profile_distances: distance of each point along the profile line, to start of line 56 | segment_indices: for each point, the index of the line segment the point is on""" 57 | cumulative_distances = calculate_curve_distance( 58 | line_coords[:, 0], line_coords[:, 1] 59 | ) 60 | x1, y1 = line_coords[:-1].T 61 | x2, y2 = line_coords[1:].T 62 | 63 | # 1) Which segment’s bounding‐box each point falls into? 64 | mask = ( 65 | (new_points[:, 0, None] >= np.minimum(x1, x2)) 66 | & (new_points[:, 0, None] <= np.maximum(x1, x2)) 67 | & (new_points[:, 1, None] >= np.minimum(y1, y2)) 68 | & (new_points[:, 1, None] <= np.maximum(y1, y2)) 69 | ) 70 | 71 | # 2) Distance from each segment start to each point 72 | segment_distances = np.linalg.norm(new_points[:, None] - line_coords[:-1], axis=2) 73 | 74 | # 3) Add on the “already‐traveled” distance along the curve 75 | valid_distances = np.where( 76 | mask, cumulative_distances[:-1] + segment_distances, np.inf 77 | ) 78 | 79 | # 4) For each point, pick the segment with minimal total distance 80 | segment_indices = np.argmin(valid_distances, axis=1) 81 | min_distances = np.min(valid_distances, axis=1) 82 | 83 | # 5) Clamp to the true number of segments 84 | n_segments = len(line_coords) - 1 85 | segment_indices = np.clip(segment_indices, 0, n_segments - 1) 86 | 87 | # 6) Mark points that never hit any segment 88 | no_hit = ~mask.any(axis=1) 89 | segment_indices[no_hit] = -1 90 | min_distances[no_hit] = np.nan 91 | profile_distances = min_distances 92 | 93 | return profile_distances, segment_indices 94 | 95 | 96 | def read_xyc(filepath: Path, num_columns: int = 2) -> shapely.geometry.LineString: 97 | """ 98 | Adapted from D-FAST BE: io.read_xyc() 99 | Read lines from a file. 100 | 101 | Arguments 102 | --------- 103 | filepath : Path 104 | Name of the file to be read. 105 | num_columns : int 106 | Number of columns to be read (2 or 3) 107 | 108 | Returns 109 | ------- 110 | L : shapely.geometry.linestring.LineStringAdapter 111 | Line strings. 112 | """ 113 | if not filepath.exists(): 114 | raise FileNotFoundError(f"File not found: {filepath}") 115 | 116 | if filepath.suffix.lower() == ".xyc": 117 | if num_columns == 3: 118 | column_names = ["Val", "X", "Y"] 119 | else: 120 | column_names = ["X", "Y"] 121 | point_coordinates = pandas.read_csv( 122 | filepath, names=column_names, skipinitialspace=True, sep=r"\s+" 123 | ) 124 | num_points = len(point_coordinates.X) 125 | x = point_coordinates.X.to_numpy().reshape((num_points, 1)) 126 | y = point_coordinates.Y.to_numpy().reshape((num_points, 1)) 127 | if num_columns == 3: 128 | z = point_coordinates.Val.to_numpy().reshape((num_points, 1)) 129 | coords = np.concatenate((x, y, z), axis=1) 130 | else: 131 | coords = np.concatenate((x, y), axis=1) 132 | line_string = shapely.geometry.LineString(coords) 133 | else: 134 | gdf = gpd.read_file(filepath)["geometry"] 135 | line_string = gdf[0] 136 | 137 | return line_string 138 | 139 | 140 | def get_xy_km(km_file) -> shapely.geometry.linestring.LineString: 141 | """From D-FAST BE: io.get_xy_km() 142 | 143 | Returns 144 | ------- 145 | xykm : shapely.geometry.linestring.LineStringAdapter 146 | """ 147 | # get the chainage file 148 | # log_text("read_chainage", dict={"file": km_file}) 149 | xy_km = read_xyc(km_file, num_columns=3) 150 | 151 | # make sure that chainage is increasing with node index 152 | if xy_km.coords[0][2] > xy_km.coords[1][2]: 153 | xy_km = LineString(xy_km.coords[::-1]) 154 | 155 | return xy_km 156 | 157 | 158 | def extract_coordinates(geometries: list) -> np.ndarray: 159 | """Extract coordinates from a list of Point and MultiPoint geometries.""" 160 | coords = [] 161 | for geom in geometries: 162 | if geom.geom_type == "Point": 163 | coords.append((geom.x, geom.y)) 164 | elif geom.geom_type == "MultiPoint": 165 | coords.extend([(point.x, point.y) for point in geom.geoms]) 166 | return np.array(coords) 167 | 168 | 169 | @dataclass 170 | class ProfileLines: 171 | """Class for handling profile lines""" 172 | 173 | filepath: Path 174 | dataframe: gpd.GeoDataFrame = field(init=False, default_factory=gpd.GeoDataFrame) 175 | 176 | def read_file(self) -> gpd.GeoDataFrame: 177 | """ 178 | Read the profile lines from the file and return as a GeoDataFrame. 179 | """ 180 | if not self.filepath.exists(): 181 | raise FileNotFoundError(f"File not found: {self.filepath}") 182 | try: 183 | self.dataframe = gpd.read_file(self.filepath) 184 | first_col = self.dataframe.columns[0] 185 | self.dataframe.set_index(first_col, inplace=True) 186 | self.dataframe = self.get_exploded_df() 187 | except Exception as e: 188 | raise IOError(f"Error reading file {self.filepath}: {e}") from e 189 | return self.get_exploded_df() 190 | 191 | def get_angles(self): 192 | """ 193 | Calculate angles for the geometries in the GeoDataFrame. 194 | """ 195 | self.dataframe["angle"] = self.dataframe.geometry.apply( 196 | lambda geom: ( 197 | calculate_angle(geom.coords) if hasattr(geom, "coords") else np.nan 198 | ) 199 | ) 200 | return self.dataframe["angle"] 201 | 202 | def get_exploded_df(self) -> gpd.GeoDataFrame: 203 | exploded_df = self.dataframe.explode() 204 | exploded_df.reset_index(drop=True, inplace=True) 205 | return exploded_df 206 | 207 | 208 | # def merge_lines(lines: MultiLineString): 209 | # """"Merge individual line segments.""" 210 | # return ops.linemerge(lines) 211 | 212 | 213 | def calculate_angle(coords): 214 | vertices = np.array(coords) 215 | return np.degrees(np.arctan2(np.diff(vertices[:, 1]), np.diff(vertices[:, 0]))) 216 | 217 | 218 | def project_km_on_line(line_xy: np.ndarray, xykm_np: np.ndarray) -> np.ndarray: 219 | """ 220 | From D-FAST BE: support.project_km_on_line 221 | 222 | Project chainage values from source line L1 onto another line L2. 223 | 224 | The chainage values are giving along a line L1 (xykm_np). For each node 225 | of the line L2 (line_xy) on which we would like to know the chainage, first 226 | the closest node (discrete set of nodes) on L1 is determined and 227 | subsequently the exact chainage isobtained by determining the closest point 228 | (continuous line) on L1 for which the chainage is determined using by means 229 | of interpolation. 230 | 231 | Arguments 232 | --------- 233 | line_xy : np.ndarray 234 | Array containing the x,y coordinates of a line. 235 | xykm_np : np.ndarray 236 | Array containing the x,y,chainage data. 237 | 238 | Results 239 | ------- 240 | line_km : np.ndarray 241 | Array containing the chainage for every coordinate specified in line_xy. 242 | """ 243 | # pre-allocate the array for the mapped chainage values 244 | line_km = np.zeros(line_xy.shape[0]) 245 | 246 | # get an array with only the x,y coordinates of line L1 247 | xy_np = xykm_np[:, :2] 248 | last_xykm = xykm_np.shape[0] - 1 249 | 250 | # for each node rp on line L2 get the chainage ... 251 | for i, rp_np in enumerate(line_xy): 252 | # find the node on L1 closest to rp 253 | imin = np.argmin(((rp_np - xy_np) ** 2).sum(axis=1)) 254 | p0 = xy_np[imin] 255 | 256 | # determine the distance between that node and rp 257 | dist2 = ((rp_np - p0) ** 2).sum() 258 | 259 | # chainage value of that node 260 | km = xykm_np[imin, 2] 261 | # print("chainage closest node: ", km) 262 | 263 | # if we didn't get the first node 264 | if imin > 0: 265 | # project rp onto the line segment before this node 266 | p1 = xy_np[imin - 1] 267 | alpha = ( 268 | (p1[0] - p0[0]) * (rp_np[0] - p0[0]) 269 | + (p1[1] - p0[1]) * (rp_np[1] - p0[1]) 270 | ) / ((p1[0] - p0[0]) ** 2 + (p1[1] - p0[1]) ** 2) 271 | # if there is a closest point not coinciding with the nodes ... 272 | if alpha > 0 and alpha < 1: 273 | dist2link = (rp_np[0] - p0[0] - alpha * (p1[0] - p0[0])) ** 2 + ( 274 | rp_np[1] - p0[1] - alpha * (p1[1] - p0[1]) 275 | ) ** 2 276 | # if it's actually closer than the node ... 277 | if dist2link < dist2: 278 | # update the closest point information 279 | dist2 = dist2link 280 | km = xykm_np[imin, 2] + alpha * ( 281 | xykm_np[imin - 1, 2] - xykm_np[imin, 2] 282 | ) 283 | # print("chainage of projection 1: ", km) 284 | 285 | # if we didn't get the last node 286 | if imin < last_xykm: 287 | # project rp onto the line segment after this node 288 | p1 = xy_np[imin + 1] 289 | alpha = ( 290 | (p1[0] - p0[0]) * (rp_np[0] - p0[0]) 291 | + (p1[1] - p0[1]) * (rp_np[1] - p0[1]) 292 | ) / ((p1[0] - p0[0]) ** 2 + (p1[1] - p0[1]) ** 2) 293 | # if there is a closest point not coinciding with the nodes ... 294 | if alpha > 0 and alpha < 1: 295 | dist2link = (rp_np[0] - p0[0] - alpha * (p1[0] - p0[0])) ** 2 + ( 296 | rp_np[1] - p0[1] - alpha * (p1[1] - p0[1]) 297 | ) ** 2 298 | # if it's actually closer than the previous value ... 299 | if dist2link < dist2: 300 | # update the closest point information 301 | dist2 = dist2link 302 | km = xykm_np[imin, 2] + alpha * ( 303 | xykm_np[imin + 1, 2] - xykm_np[imin, 2] 304 | ) 305 | # print("chainage of projection 2: ", km) 306 | 307 | # store the chainage value, loop ... and return 308 | line_km[i] = km 309 | return line_km 310 | -------------------------------------------------------------------------------- /src/dfasttf/batch/dflowfm.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from pathlib import Path 3 | from typing import NamedTuple 4 | 5 | import numpy as np 6 | import xugrid as xu 7 | from pandas import DataFrame 8 | 9 | # import pandas as pd 10 | from shapely import LineString 11 | from xugrid import UgridDataArray, UgridDataset 12 | 13 | from dfasttf.batch import geometry 14 | from dfasttf.config import Config, get_output_files 15 | 16 | VARN_FACE_X_BND = "mesh2d_face_x_bnd" 17 | VARN_FACE_Y_BND = "mesh2d_face_y_bnd" 18 | 19 | 20 | class Variables(NamedTuple): 21 | """Class of relevant variables. 22 | h: water depth 23 | uc: flow velocity magnitude 24 | ucx: flow velocity, x-component 25 | ucy: flow velocity, y-componentn 26 | bl: bed level""" 27 | 28 | h: str 29 | uc: str 30 | ucx: str 31 | ucy: str 32 | bl: str 33 | 34 | 35 | def load_simulation_data(configuration: Config, section: str) -> list[UgridDataset]: 36 | """Load and preprocess simulation datasets.""" 37 | datasets = [] 38 | output_files = get_output_files( 39 | configuration.config, configuration.configdir, section 40 | ) 41 | for file in output_files: 42 | ds = xu.open_dataset(file, chunks={"time": 1, "x": 100, "y": 100}) 43 | 44 | if configuration.general.bbox is not None: 45 | ds = clip_simulation_data(ds, configuration.general.bbox) 46 | ds = extract_variables(ds) 47 | datasets.append(ds) 48 | return datasets 49 | 50 | 51 | def clip_simulation_data( 52 | data: UgridDataArray | UgridDataset, bbox: list 53 | ) -> UgridDataArray | UgridDataset: 54 | # TODO: implement better bbox data structure based on keywords 55 | """Clips simulation data based on bounding box [xmin, xmax, ymin, ymax]""" 56 | return data.ugrid.sel(x=slice(bbox[0], bbox[1]), y=slice(bbox[2], bbox[3])) 57 | 58 | 59 | def extract_variables(ds: xu.UgridDataset) -> xu.UgridDataset: 60 | """Extract and standardize variable names from a NetCDF dataset using lazy loading and Dask.""" 61 | 62 | if "time" in ds.coords: 63 | ds = ds.isel(time=-1) 64 | else: 65 | bl = find_variable(ds, "altitude") 66 | wl = find_variable(ds, "sea_surface_height") 67 | uc = find_variable(ds, "sea_water_speed") 68 | ucx = find_variable(ds, "sea_water_x_velocity") 69 | ucy = find_variable(ds, "sea_water_y_velocity") 70 | 71 | ds[bl] = ds[bl].ugrid.to_face().mean("nmax") # bed elevation on nodes to faces 72 | 73 | ds = ds.assign( 74 | mesh2d_waterdepth=ds[wl] - ds[bl], 75 | mesh2d_ucmag=ds[uc], 76 | mesh2d_ucx=ds[ucx], 77 | mesh2d_ucy=ds[ucy], 78 | ) 79 | 80 | return ds 81 | 82 | 83 | def find_variable(data: UgridDataset, standard_name: str) -> str: 84 | """Finds a variable in a dataset by its 'standard_name' attribute.""" 85 | selected_var = next( 86 | ( 87 | var 88 | for var in data.data_vars 89 | if data[var].attrs.get("standard_name") == standard_name 90 | ), 91 | None, 92 | ) 93 | if selected_var is None: 94 | raise IOError(f"No variable found with standard_name '{standard_name}'") 95 | return selected_var 96 | 97 | 98 | def get_profile_data( 99 | profile_dataset: UgridDataset, variable_name: str, face_idx 100 | ) -> dict: 101 | profile_data = profile_dataset[variable_name].data[face_idx] 102 | return profile_data 103 | 104 | 105 | def slice_ugrid( 106 | simulation_data: UgridDataset, 107 | profile_coords: np.ndarray, 108 | riverkm_coords: np.ndarray, 109 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: 110 | edge_coords = extract_edge_coords(simulation_data, VARN_FACE_X_BND, VARN_FACE_Y_BND) 111 | sliced = slice_mesh_with_polyline(edge_coords, profile_coords, riverkm_coords) 112 | if sliced is None: 113 | return None 114 | rkm, path_distances, segment_idx, face_idx = sliced 115 | return rkm, path_distances, segment_idx, face_idx 116 | 117 | 118 | def read_profile_lines(profiles_file: Path) -> DataFrame: 119 | profile_lines = geometry.ProfileLines(profiles_file) 120 | prof_line_df = profile_lines.read_file() 121 | prof_line_df["angle"] = profile_lines.get_angles() 122 | return prof_line_df 123 | 124 | 125 | def intersect_linestring( 126 | simulation_data: UgridDataset, profile: LineString 127 | ) -> UgridDataset: 128 | """Returns only the data on faces intersected by the profile line""" 129 | return simulation_data.ugrid.intersect_linestring(profile) 130 | 131 | 132 | def extract_edge_coords( 133 | profile_data: UgridDataset, varn_face_x_bnd: str, varn_face_y_bnd: str 134 | ) -> np.ndarray: 135 | x_bnd = profile_data[varn_face_x_bnd].values 136 | y_bnd = profile_data[varn_face_y_bnd].values 137 | return np.stack((x_bnd, y_bnd), axis=-1) 138 | 139 | 140 | def slice_mesh_with_polyline( 141 | edge_coords: np.ndarray, profile_coords: np.ndarray, xykm_coords: np.ndarray 142 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: 143 | """Slices mesh edges with a profile line and returns for each intersection point: 144 | pkm: projected value of xykm, found by interpolation 145 | path_distances: distance along path formed by intersection points 146 | segment_idx: index of segment of profile line 147 | face_idx: index of mesh face""" 148 | intersects, face_indices = find_intersects(edge_coords, profile_coords) 149 | 150 | if len(intersects) == 0: 151 | print( 152 | "No intersects found between profile line(s) and simulation data. " 153 | "Expand the bounding box, or change the profile line(s)" 154 | ) 155 | return None 156 | 157 | profile_distances, segment_indices = calculate_intersect_distance( 158 | profile_coords, intersects 159 | ) 160 | pkm, intersects_ordered, segment_idx, face_idx = _order_intersection_points( 161 | intersects, profile_distances, segment_indices, face_indices, xykm_coords 162 | ) 163 | 164 | path_distances = geometry.calculate_curve_distance( 165 | intersects_ordered[:, 0], intersects_ordered[:, 1] 166 | ) 167 | return pkm, path_distances, segment_idx, face_idx 168 | 169 | 170 | def find_intersects( 171 | edge_coords: np.ndarray, line_coords: np.ndarray 172 | ) -> tuple[np.ndarray, np.ndarray]: 173 | """Find intersection points between mesh edges and a line. 174 | 175 | Parameters: 176 | - edge_coords: (nfaces, nmax, 2), with NaNs for unused vertices 177 | - line_coords: (N, 2) 178 | 179 | Returns: 180 | - intersects: (M, 2) array of intersection points 181 | - face_idx: (M,) array of face indices 182 | """ 183 | 184 | intersects = [] 185 | face_idx = [] 186 | nfaces, nmax, _ = edge_coords.shape 187 | b = LineString(line_coords) 188 | 189 | for i in range(nfaces): 190 | # Extract non-NaN vertices for this face 191 | face_vertices = edge_coords[i] 192 | valid_mask = ~np.isnan(face_vertices[:, 0]) 193 | valid_vertices = face_vertices[valid_mask] 194 | 195 | n_valid = valid_vertices.shape[0] 196 | if n_valid < 2: 197 | continue # skip degenerate faces 198 | 199 | # Loop through valid edges 200 | for j in range(n_valid): 201 | a1 = valid_vertices[j] 202 | a2 = valid_vertices[(j + 1) % n_valid] # wrap around 203 | a = LineString([a1, a2]) 204 | 205 | try: 206 | intersect = a.intersection(b) 207 | if not intersect.is_empty: 208 | coords = extract_coordinates([intersect]) 209 | if coords.size > 0: 210 | intersects.extend(coords) 211 | face_idx.extend([i] * len(coords)) 212 | except: 213 | pass 214 | 215 | intersects = np.array(intersects) 216 | face_idx = np.asarray(face_idx) 217 | 218 | # Optional for debugging: 219 | # pd.DataFrame(np.column_stack((intersects[:,0], intersects[:,1], face_idx))).to_csv('intersects.csv') 220 | return intersects, face_idx 221 | 222 | 223 | def extract_coordinates(geom_list): 224 | coords = [] 225 | for g in geom_list: 226 | if g.geom_type == "Point": 227 | coords.append([g.x, g.y]) 228 | elif g.geom_type == "MultiPoint": 229 | coords.extend([[pt.x, pt.y] for pt in g.geoms]) 230 | elif g.geom_type == "LineString": 231 | mid_idx = len(g.coords) // 2 232 | coords.append(list(g.coords[mid_idx])) 233 | elif g.geom_type == "GeometryCollection": 234 | for subg in g.geoms: 235 | coords.extend(extract_coordinates([subg])) 236 | return np.array(coords) 237 | 238 | 239 | def calculate_intersect_distance( 240 | line_coords: np.ndarray, intersects: np.ndarray 241 | ) -> tuple[np.ndarray, np.ndarray]: 242 | """ 243 | Returns: 244 | profile_distances: distance of intersection points along line. 245 | segment_idx: indices of the line segments where the intersection occurs (N,1)""" 246 | profile_distances, segment_idx = geometry.find_distances_to_points( 247 | line_coords, intersects 248 | ) 249 | return profile_distances, segment_idx 250 | 251 | 252 | def _order_intersection_points( 253 | intersects: np.ndarray, 254 | profile_distances: np.ndarray, 255 | segment_idx: np.ndarray, 256 | face_idx: np.ndarray, 257 | river_km: np.ndarray, 258 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 259 | """Correctly orders the intersection points between a UGRID mesh and profile line. 260 | 261 | Parameters: 262 | intersects: Intersection points. 263 | profile_distances: Distances along the profile line. 264 | segment_idx: Segment indices of the profile line. 265 | face_idx: Face indices of mesh. 266 | river_km: x,y coordinates of river kilometers (rkm) 267 | 268 | Returns: 269 | tuple[np.ndarray, np.ndarray, np.ndarray]: Grouped rkm, segment indices, and face indices. 270 | """ 271 | 272 | # 1. Sort along profile line 273 | sorted_data = [ 274 | sort_a_by_b(a, profile_distances) for a in [intersects, segment_idx, face_idx] 275 | ] 276 | intersects, segment_idx, face_idx = sorted_data 277 | 278 | # 2. Group face indices 279 | face_idx, group_idx = group_duplicates(face_idx) 280 | segment_idx = segment_idx[group_idx] 281 | intersects = intersects[group_idx] 282 | 283 | # 3. Convert to rkm, in metres 284 | rkm = convert_to_rkm(intersects, river_km, 1000) 285 | 286 | # 4. Ensure the overall direction is downstream (so the first rkm < last rkm) 287 | if rkm[0] > rkm[-1]: 288 | rkm = rkm[::-1] 289 | intersects = intersects[::-1] 290 | segment_idx = segment_idx[::-1] 291 | face_idx = face_idx[::-1] 292 | 293 | # 5. strictly increasing sequence of rkm 294 | mask = np.empty_like(rkm, dtype=bool) 295 | mask[0] = True 296 | last_r = rkm[0] 297 | 298 | for i in range(1, len(rkm)): 299 | if rkm[i] >= last_r: 300 | mask[i] = True 301 | last_r = rkm[i] 302 | else: 303 | mask[i] = False 304 | 305 | rkm_ordered = rkm[mask] 306 | intersects_ordered = intersects[mask] 307 | segment_idx_ordered = segment_idx[mask] 308 | face_idx_ordered = face_idx[mask] 309 | 310 | # now this should be guaranteed non‐decreasing (strictly increasing) 311 | assert np.all(np.diff(rkm_ordered) >= 0) 312 | 313 | return rkm_ordered, intersects_ordered, segment_idx_ordered, face_idx_ordered 314 | 315 | 316 | def sort_a_by_b(a: np.ndarray, b: np.ndarray) -> np.ndarray: 317 | """Sorts the array `a` by the argsort of `b`. 318 | 319 | Parameters: 320 | a (np.ndarray): Array to be sorted. 321 | b (np.ndarray): Array to sort by. 322 | 323 | Returns: 324 | np.ndarray: Sorted array `a`. 325 | """ 326 | sort_idx = np.argsort(b) 327 | return ( 328 | np.take_along_axis(a, sort_idx[:, np.newaxis], axis=0) 329 | if a.ndim > 1 330 | else a[sort_idx] 331 | ) 332 | 333 | 334 | def group_duplicates(array: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 335 | """Groups duplicates in an array, preserving insertion order of first occurrences""" 336 | groups = OrderedDict() 337 | for idx, val in enumerate(array): 338 | if val not in groups: 339 | groups[val] = [] 340 | groups[val].append(idx) 341 | 342 | group_indices = np.array([idx for indices in groups.values() for idx in indices]) 343 | grouped_array = array[group_indices] 344 | return grouped_array, group_indices 345 | 346 | 347 | def convert_to_rkm(intersects, river_km, conversion_factor=1): 348 | """Converts an array of points to the corresponding rkm values 349 | 350 | Parameters: 351 | intersects: intersection points 352 | river_km: chainage values 353 | conversion_factor: optional, to convert km to another unit (default = 1)""" 354 | rkm = geometry.project_km_on_line(intersects, river_km) * conversion_factor 355 | return rkm 356 | -------------------------------------------------------------------------------- /src/dfasttf/batch/plotting.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | from typing import Any, Optional 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | import shapely.plotting 8 | import xarray as xr 9 | import xugrid as xu 10 | from geopandas import GeoDataFrame 11 | from matplotlib import ticker 12 | from matplotlib.axes import Axes 13 | from matplotlib.collections import LineCollection 14 | from matplotlib.colors import ListedColormap 15 | from matplotlib.figure import Figure 16 | from matplotlib.lines import Line2D 17 | from matplotlib.patches import Patch 18 | from shapely import LineString 19 | from xarray import DataArray 20 | 21 | from dfastmi.batch.plotting import chainage_markers, savefig 22 | 23 | # from dfastmi.batch.PlotOptions import PlotOptions 24 | from dfasttf.config import Config 25 | 26 | # import contextily as ctx 27 | # from xyzservices import TileProvider 28 | 29 | FIGWIDTH: float = 5.748 # Deltares report width 30 | TEXTFONT = "arial" 31 | TEXTSIZE = 12 32 | CRS: str = "EPSG:28992" # Netherlands 33 | XMAJORTICK: float = 1000 34 | XMINORTICK: float = 100 35 | 36 | 37 | def initialize_figure(figwidth: Optional[float] = FIGWIDTH) -> Figure: 38 | font = {"family": TEXTFONT, "size": TEXTSIZE} 39 | plt.rc("font", **font) 40 | fig = plt.figure(layout="constrained") 41 | # fig.set_figwidth(figwidth) 42 | return fig 43 | 44 | 45 | def initialize_subplot( 46 | fig: Figure, nrows: int, ncols: int, index: int, xlabel: str, ylabel: str 47 | ): 48 | ax = fig.add_subplot(nrows, ncols, index) 49 | ax.set_xlabel(xlabel) 50 | ax.set_ylabel(ylabel) 51 | return ax 52 | 53 | 54 | def difference_plot(ax: Axes, ylabel: str, color: str): 55 | secax_y2 = ax.twinx() 56 | secax_y2.set_ylabel(ylabel) 57 | secax_y2.yaxis.label.set_color(color) 58 | secax_y2.tick_params(color=color, labelcolor=color) 59 | secax_y2.spines["right"].set_color(color) 60 | return secax_y2 61 | 62 | 63 | def invert_xaxis(ax: Axes): 64 | ax.xaxis.set_inverted(True) 65 | 66 | 67 | def plot_variable( 68 | ax: Axes, x: np.ndarray, y: np.ndarray, color: str = "black" 69 | ) -> list[Line2D]: 70 | p = ax.plot(x, y, "-", linewidth=0.5, color=color) 71 | return p 72 | 73 | 74 | def plot_chainage_markers(riverkm: LineString, ax: Axes): 75 | # first filter chainage by 1000 m 76 | filtered_coords = np.array([coord for coord in riverkm.coords if coord[2] % 1 == 0]) 77 | chainage_markers(filtered_coords, ax, scale=1, ndec=0) 78 | 79 | 80 | def align_twinx_grid_centered( 81 | primary, 82 | secondary, 83 | *, 84 | center=0.0, 85 | keep_symmetric=True, 86 | add_centerline=True, 87 | label_formatter=None, 88 | _eps=1e-12, 89 | ): 90 | """ 91 | Align secondary-axis (right) ticks to the primary (left) axis horizontal gridlines, 92 | and (optionally) keep the secondary axis symmetric around `center` (default: 0). 93 | 94 | Parameters 95 | ---------- 96 | primary : matplotlib.axes.Axes 97 | Left axis. 98 | secondary : matplotlib.axes.Axes 99 | Right axis, created with twinx(). 100 | center : float 101 | Value the secondary axis should be centered on (default: 0). 102 | keep_symmetric : bool 103 | If True, force secondary ylim to be [center - M, center + M], where 104 | M = max(|ylow - center|, |yhigh - center|). 105 | add_centerline : bool 106 | If True, draws/updates a dashed horizontal line at secondary==center 107 | (mapped into primary coordinates) so the midline is visible even if 108 | the primary has no tick there. 109 | label_formatter : callable or None 110 | Optional function to format secondary tick labels. Receives float -> str. 111 | _eps : float 112 | Internal epsilon to avoid divide-by-zero loops. 113 | """ 114 | state = {"centerline": None, "in_update": False} 115 | 116 | def update(_evt): 117 | if state["in_update"]: 118 | return 119 | state["in_update"] = True 120 | try: 121 | # Current limits 122 | y1_lo, y1_hi = primary.get_ylim() 123 | y2_lo, y2_hi = secondary.get_ylim() 124 | 125 | # Ensure secondary is symmetric about `center` 126 | if keep_symmetric: 127 | span_lo = abs(y2_lo - center) 128 | span_hi = abs(y2_hi - center) 129 | M = max(span_lo, span_hi, _eps) 130 | new_lo, new_hi = center - M, center + M 131 | # Only set if it actually changes to avoid endless callbacks 132 | if (abs(new_lo - y2_lo) > _eps) or (abs(new_hi - y2_hi) > _eps): 133 | secondary.set_ylim(new_lo, new_hi) 134 | y2_lo, y2_hi = new_lo, new_hi 135 | 136 | # Protect against zero primary span 137 | y1_span = y1_hi - y1_lo 138 | if abs(y1_span) < _eps: 139 | return 140 | 141 | # Linear mapping primary -> secondary: y2 = a*y1 + b 142 | a = (y2_hi - y2_lo) / y1_span 143 | b = y2_lo - a * y1_lo 144 | 145 | # Align secondary ticks to primary gridlines 146 | yt1 = primary.get_yticks() 147 | yt2 = a * yt1 + b 148 | secondary.set_yticks(yt2) 149 | if label_formatter is not None: 150 | secondary.set_yticklabels([label_formatter(val) for val in yt2]) 151 | 152 | # Grid: only on primary (so lines are shared across both) 153 | primary.set_axisbelow(True) 154 | primary.grid(True, axis="y") 155 | secondary.grid(False) 156 | 157 | # Optional: visible centerline at secondary==center 158 | if add_centerline and abs(a) > _eps: 159 | y1_at_center = (center - b) / a 160 | if state["centerline"] is None: 161 | # One line that we update on every callback 162 | state["centerline"] = primary.axhline( 163 | y1_at_center, color="black", ls="--", lw=1 164 | ) 165 | else: 166 | state["centerline"].set_ydata([y1_at_center, y1_at_center]) 167 | # Hide line if center is outside current primary limits (e.g., zoom) 168 | vis = ( 169 | min(y1_lo, y1_hi) - _eps <= y1_at_center <= max(y1_lo, y1_hi) + _eps 170 | ) 171 | state["centerline"].set_visible(vis) 172 | 173 | primary.figure.canvas.draw_idle() 174 | finally: 175 | state["in_update"] = False 176 | 177 | # Recompute whenever y-lims change (zoom/pan/autoscale) 178 | primary.callbacks.connect("ylim_changed", update) 179 | secondary.callbacks.connect("ylim_changed", update) 180 | update(None) 181 | 182 | 183 | # def add_satellite_image(ax: Axes, background_image: TileProvider): 184 | # ctx.add_basemap(ax=ax, source=background_image, crs=CRS, attribution=False, zorder=-1) 185 | 186 | 187 | @dataclass 188 | class Plot1DConfig: 189 | XLABEL: str = "afstand [rivierkilometer]" 190 | COLORS = ("k", "b", "r") # reference, intervention, difference 191 | LABELS = ["Referentie", "Plansituatie"] 192 | 193 | 194 | @dataclass 195 | class Plot2D: 196 | xlabel: str = "x-coördinaat [km]" 197 | ylabel: str = "y-coördinaat [km]" 198 | # background_image = ctx.providers.OpenStreetMap.Mapnik #ctx.providers.Esri.WorldImagery 199 | 200 | def initialize_map(self) -> tuple[Figure, Axes]: 201 | fig = initialize_figure() 202 | ax = initialize_subplot(fig, 1, 1, 1, self.xlabel, self.ylabel) 203 | # add_satellite_image(ax, Plot2D.background_image) 204 | ax.grid(True) 205 | return fig, ax 206 | 207 | def modify_axes(self, ax: Axes) -> Axes: 208 | ax.set_title("") 209 | ax.set_aspect("equal") 210 | ax.set_xlabel(self.xlabel) 211 | ax.set_ylabel(self.ylabel) 212 | ax.xaxis.set_major_formatter( 213 | ticker.FuncFormatter(lambda x, _: f"{x/XMAJORTICK:.1f}") 214 | ) 215 | ax.yaxis.set_major_formatter( 216 | ticker.FuncFormatter(lambda x, _: f"{x/XMAJORTICK:.1f}") 217 | ) 218 | return ax 219 | 220 | def plot_profile_line( 221 | self, 222 | profile: LineString, 223 | bedlevel: xr.DataArray, 224 | riverkm: LineString, 225 | filename: Path, 226 | ) -> tuple[Figure, Axes]: 227 | """Plot the profile line in a 2D plot""" 228 | fig, ax = self.initialize_map() 229 | p = bedlevel.ugrid.plot.pcolormesh( 230 | ax=ax, add_colorbar=False, cmap="terrain", center=False 231 | ) 232 | fig.colorbar( 233 | p, ax=ax, label="bodemligging [m]", orientation="horizontal", shrink=0.25 234 | ) 235 | shapely.plotting.plot_line(profile, ax=ax, add_points=False, color="black") 236 | self.modify_axes(ax) 237 | plot_chainage_markers(riverkm, ax) 238 | savefig(fig, filename) 239 | return fig, ax 240 | 241 | 242 | def modify_axes(ax: Axes, x_major_tick: float) -> Axes: 243 | # x-axis: 244 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x/x_major_tick}")) 245 | ax.tick_params(which="major", length=8) 246 | ax.tick_params(which="minor", length=4) 247 | return ax 248 | 249 | 250 | def construct_figure_filename(figdir: Path, base: str, extension: str) -> Path: 251 | """Construct full path for saving a figure.""" 252 | return Path(figdir) / f"{base}{extension}" 253 | 254 | 255 | @dataclass 256 | class FlowfieldConfig: 257 | VELOCITY_YLABEL: str = "stroomsnelheid\nmagnitude" + r" [$m/s$]" 258 | VELOCITY_DIFF_YLABEL: str = "verschil plansituatie\n-referentie" + r" [$m/s$]" 259 | VELOCITY_YMIN: float = 0.0 260 | ANGLE_YTICKS = ticker.FixedLocator(list(np.arange(-90, 91, 22.5))) 261 | ANGLE_PRIMARY_YLABEL: str = "stromingshoek t.o.v.\nprofiellijn" + r" [$graden$]" 262 | # ANGLE_SECONDARY_YLABEL: str = r'stromingshoek [richting]' 263 | ANGLE_DIFF_YLABEL: str = "verschil plansituatie\n-referentie" + r" [$graden$]" 264 | # ANGLE_SECONDARY_YTICKLABELS = ticker.FixedFormatter(['Z','ZW','W','NW','N','NO','O','ZO','Z']) 265 | 266 | 267 | @dataclass 268 | class FroudeConfig: 269 | legend_title = "Froude getal" 270 | 271 | class Abs: 272 | colorbar_label: str = "Froude getal" 273 | levels: tuple = (0, 0.08, 0.1, 0.15) 274 | colormap: str = "RdBu" 275 | 276 | class Diff: 277 | bins: list = [0, 0.08, 0.1, 0.15, np.inf] 278 | 279 | # the following variables are linked to the classes returned by _compute_change_classes 280 | # colors: tuple = ("#d1ffbf", '#49e801', '#267500', '#f80000', '#fea703', '#fffe00') 281 | colors = ("blue", "red") 282 | labels: list[str] = [ # f"van < {bins[3]} naar >= {bins[3]}", 283 | # f"van < {bins[2]} naar >= {bins[2]}", 284 | f"van < {bins[1]} naar >= {bins[1]}", 285 | f"van > {bins[1]} naar <= {bins[1]}", 286 | # f"van > {bins[2]} naar <= {bins[2]}", 287 | # f"van > {bins[3]} naar <= {bins[3]}" 288 | ] 289 | 290 | 291 | class Ice2D: 292 | 293 | def create_map( 294 | self, 295 | data: DataArray, 296 | riverkm: LineString, 297 | profile_line_df: GeoDataFrame, 298 | filename: Path, 299 | ) -> None: 300 | fig, ax = Plot2D().initialize_map() 301 | p = data.ugrid.plot( 302 | ax=ax, 303 | add_colorbar=False, 304 | levels=FroudeConfig.Abs.levels, 305 | cmap=FroudeConfig.Abs.colormap, 306 | extend="max", 307 | ) 308 | fig.colorbar( 309 | p, 310 | ax=ax, 311 | label=FroudeConfig.Abs.colorbar_label, 312 | orientation="horizontal", 313 | shrink=0.25, 314 | ) 315 | ax = Plot2D().modify_axes(ax) 316 | plot_chainage_markers(riverkm, ax) 317 | profile_line_df.plot(ax=ax, linewidth=1, color="green") 318 | savefig(fig, filename) 319 | 320 | def create_diff_map( 321 | self, 322 | ref_data: xr.DataArray, 323 | variant_data: xr.DataArray, 324 | riverkm: LineString, 325 | profile_line_df: GeoDataFrame, 326 | filename: Path, 327 | ) -> None: 328 | plt.close("all") 329 | bins = FroudeConfig.Diff.bins 330 | colors = FroudeConfig.Diff.colors 331 | labels = FroudeConfig.Diff.labels 332 | 333 | # Step 1: Digitize inputs 334 | ref_data_digitized = self._digitize(ref_data.values, bins) 335 | variant_data_digitized = self._digitize(variant_data.values, bins) 336 | 337 | # Step 2: Classify change categories 338 | classes = self._compute_change_classes( 339 | ref_data_digitized, variant_data_digitized 340 | ) 341 | variant_data.values = classes 342 | 343 | # Step 3: Initialize figure with background plot 344 | fig, ax = Plot2D().initialize_map() 345 | color = "lightgrey" 346 | ref_masked = ref_data[ref_data_digitized == 0] 347 | ref_masked.ugrid.plot( 348 | ax=ax, 349 | cmap=ListedColormap([color]), 350 | add_colorbar=False, 351 | vmin=bins[0], 352 | vmax=bins[1], 353 | ) 354 | 355 | # Step 4: Difference plot 356 | ax, legend_elements = self._plot_diff_map(ax, variant_data, labels, colors) 357 | 358 | # Step 5: finalisation 359 | ax = Plot2D().modify_axes(ax) 360 | lgd = fig.legend( 361 | [Patch(facecolor=color), *legend_elements], 362 | [f"< {bins[1]} in referentie", *labels], 363 | ) 364 | lgd.set_title(FroudeConfig.legend_title) 365 | ax.grid(True) 366 | plot_chainage_markers(riverkm, ax) 367 | profile_line_df.plot(ax=ax, linewidth=0.5, color="black") 368 | savefig(fig, filename) 369 | 370 | def _plot_diff_map( 371 | self, ax: Axes, diff_data: xr.DataArray, labels: list[str], colors: tuple 372 | ) -> tuple[Axes, list]: 373 | 374 | xu.plot.pcolormesh( 375 | diff_data.grid, 376 | diff_data, 377 | ax=ax, 378 | add_colorbar=False, 379 | cmap=ListedColormap(colors), 380 | zorder=1, 381 | ) 382 | 383 | legend_elements = [ 384 | Patch(facecolor=colors[i], label=labels[i]) for i in range(len(labels)) 385 | ] 386 | 387 | return ax, legend_elements 388 | 389 | def _digitize(self, data: Any, bins: Any) -> np.ndarray: 390 | return np.digitize(data, bins) - 1 391 | 392 | def _compute_change_classes( 393 | self, ref_data: np.ndarray, variant_data: np.ndarray 394 | ) -> np.ndarray: 395 | """Computes how classes change between two digitized datasets""" 396 | classes = variant_data * np.nan 397 | 398 | conditions = [ # (ref_data < 3) & (variant_data >= 3), 399 | # (ref_data < 2) & (variant_data >= 2), 400 | (ref_data < 1) & (variant_data >= 1), 401 | (ref_data > 0) & (variant_data <= 0), 402 | # (ref_data > 1) & (variant_data <= 1), 403 | # (ref_data > 2) & (variant_data <= 2) 404 | ] 405 | 406 | for i, cond in enumerate(conditions, start=1): 407 | classes[cond] = i 408 | 409 | return classes 410 | 411 | 412 | class Ice1D: 413 | """Class for plotting 1D river flow velocity and angle.""" 414 | 415 | def plot_velocity_magnitude( 416 | self, ax: Axes, distance: np.ndarray, velocity: np.ndarray, color: str 417 | ) -> Axes: 418 | """ 419 | Plot the velocity magnitude. 420 | """ 421 | plot_variable(ax, distance, velocity, color) 422 | # ax.set_ylim(bottom=FlowfieldConfig.VELOCITY_YMIN) 423 | return ax 424 | 425 | def plot_velocity_angle( 426 | self, ax: Axes, distance: np.ndarray, angle: np.ndarray, color: str 427 | ) -> Axes: 428 | """ 429 | Plot the velocity angle in a separate subplot. 430 | """ 431 | plot_variable(ax, distance, angle, color) 432 | return ax 433 | 434 | # def angle_direction(self, ax: Axes): 435 | # secax_y = ax.secondary_yaxis(-0.2) 436 | # for ax in [ax,secax_y]: 437 | # secax_y.yaxis.set_major_formatter(FlowfieldConfig.ANGLE_SECONDARY_YTICKLABELS) 438 | # secax_y.set_ylabel(FlowfieldConfig.ANGLE_SECONDARY_YLABEL) 439 | # return secax_y 440 | 441 | def create_figure( 442 | self, 443 | distance: np.ndarray, 444 | velocity: list, 445 | angle: list, 446 | configuration: Config, 447 | filename: Path, 448 | ) -> None: 449 | """ 450 | Create and display a figure with velocity magnitude and angle. 451 | """ 452 | plt.close("all") 453 | fig = initialize_figure() 454 | config = Plot1DConfig() 455 | 456 | ax1 = initialize_subplot( 457 | fig, 2, 1, 1, config.XLABEL, FlowfieldConfig.VELOCITY_YLABEL 458 | ) 459 | ax2 = initialize_subplot( 460 | fig, 2, 1, 2, config.XLABEL, FlowfieldConfig.ANGLE_PRIMARY_YLABEL 461 | ) 462 | 463 | for i, (v, a) in enumerate(zip(velocity, angle)): 464 | ax1 = self.plot_velocity_magnitude(ax1, distance, v, Plot1DConfig.COLORS[i]) 465 | ax2 = self.plot_velocity_angle(ax2, distance, a, Plot1DConfig.COLORS[i]) 466 | 467 | axs_diff = [] 468 | if len(velocity) > 1: 469 | for ax, data, ylabel in [ 470 | (ax1, velocity[1] - velocity[0], FlowfieldConfig.VELOCITY_DIFF_YLABEL), 471 | (ax2, angle[1] - angle[0], FlowfieldConfig.ANGLE_DIFF_YLABEL), 472 | ]: 473 | ax_diff = difference_plot(ax, ylabel, Plot1DConfig.COLORS[-1]) 474 | plot_variable(ax_diff, distance, data, Plot1DConfig.COLORS[-1]) 475 | yabs_max = abs(max(ax_diff.get_ylim(), key=abs)) 476 | ax_diff.set_ylim(ymin=-yabs_max, ymax=yabs_max) 477 | axs_diff.append(ax_diff) 478 | 479 | # Align gridlines and keep secondary centered at 0 480 | for primary_axis, secondary_axis in zip([ax1, ax2], axs_diff): 481 | align_twinx_grid_centered( 482 | primary_axis, 483 | secondary_axis, 484 | center=0.0, 485 | keep_symmetric=True, 486 | add_centerline=True, 487 | ) 488 | 489 | for ax in [ax1, ax2]: 490 | ax1 = modify_axes(ax1, XMAJORTICK) 491 | ax2 = modify_axes(ax2, XMAJORTICK) 492 | if configuration.general.bool_flags["invertxaxis"]: 493 | invert_xaxis(ax) 494 | ax2.yaxis.set_major_locator(FlowfieldConfig.ANGLE_YTICKS) 495 | ax2.set_ylim(-90, 90) 496 | # ax2.axhline(0,color='black',ls='--') 497 | 498 | ax1.legend( 499 | Plot1DConfig.LABELS, 500 | bbox_to_anchor=(0.0, 1.02, 1.0, 0.102), 501 | loc="lower left", 502 | ncols=2, 503 | borderaxespad=0.0, 504 | ) 505 | savefig(fig, filename) 506 | 507 | 508 | @dataclass 509 | class CrossFlowConfig: 510 | XLABEL = Plot1DConfig.XLABEL 511 | YLABEL: str = "dwarsstroom-\nsnelheid" + r" [$m/s$]" 512 | DIFF_YLABEL: str = "verschil in dwars-\nstroomsnelheid" + r" [$m/s$]" 513 | 514 | 515 | class CrossFlow: 516 | def __init__(self, config: CrossFlowConfig = CrossFlowConfig()): 517 | self.config = config 518 | 519 | def plot_discharge( 520 | self, 521 | ax: Axes, 522 | xy_segment: list[tuple], 523 | crit_values: list, 524 | ) -> Optional[LineCollection]: 525 | """ 526 | Calculate and plot perpendicular discharge according to RBK specifications, 527 | along with the discharge criteria line. 528 | 529 | Returns: 530 | A matplotlib Line2D object representing the criteria line, or None if no data was plotted. 531 | """ 532 | crit_handle = None 533 | 534 | for (xi, yi), crit_value in zip(xy_segment, crit_values): 535 | # TODO: fix fill between not filling in everything 536 | ax.fill_between(xi, yi, color="lightgrey", interpolate=True) 537 | ax.axvline(xi[0], color="lightgrey", lw=0.5, ls="--") 538 | ax.axvline(xi[-1], color="lightgrey", lw=0.5, ls="--") 539 | 540 | # positive criterium: 541 | crit_handle = ax.hlines( 542 | crit_value, xi[0], xi[-1], color="red", lw=1, ls="-" 543 | ) 544 | # negative criterium: 545 | ax.hlines(-crit_value, xi[0], xi[-1], color="red", lw=1, ls="-") 546 | 547 | return crit_handle 548 | 549 | def create_figure( 550 | self, 551 | distance: np.ndarray, 552 | transverse_velocity: list[np.ndarray], 553 | xy_segments: list[list], 554 | crit_values: list[np.ndarray], 555 | inverse_xaxis: bool, 556 | filename: Path, 557 | ) -> None: 558 | plt.close("all") 559 | fig = initialize_figure() 560 | axs = [] 561 | ax1 = initialize_subplot( 562 | fig, len(transverse_velocity), 1, 1, self.config.XLABEL, self.config.YLABEL 563 | ) 564 | axs.append(ax1) 565 | 566 | crit_handle = self.plot_discharge(ax1, xy_segments[-1], crit_values[-1]) 567 | 568 | lines = [] 569 | for i, v in enumerate(transverse_velocity): 570 | (line,) = plot_variable(ax1, distance, v, Plot1DConfig.COLORS[i]) 571 | lines.append(line) 572 | 573 | if len(transverse_velocity) > 1: 574 | ax2 = initialize_subplot( 575 | fig, 2, 1, 2, self.config.XLABEL, CrossFlowConfig.DIFF_YLABEL 576 | ) 577 | plot_variable( 578 | ax2, distance, transverse_velocity[1] - transverse_velocity[0] 579 | ) 580 | axs.append(ax2) 581 | 582 | for ax in axs: 583 | modify_axes(ax, XMAJORTICK) 584 | yabs_max = abs(max(ax.get_ylim(), key=abs)) 585 | ax.set_ylim(ymin=-yabs_max, ymax=yabs_max) 586 | ax.axhline(0, color="black", ls="--") 587 | if inverse_xaxis: 588 | invert_xaxis(ax) 589 | ax.grid(visible=True, which="major", linestyle="-") 590 | ax.grid( 591 | visible=True, 592 | which="minor", 593 | axis="y", 594 | linestyle="--", 595 | color="lightgrey", 596 | lw=0.5, 597 | ) 598 | 599 | # Combine lines and crit_handle, filtering out None 600 | handles = [*lines] 601 | labels = [*Plot1DConfig.LABELS[0 : len(transverse_velocity)]] 602 | 603 | if crit_handle is not None: 604 | handles.append(crit_handle) 605 | labels.append("criteria") 606 | 607 | ax1.yaxis.set_major_locator(ticker.MultipleLocator(0.15)) 608 | ax1.yaxis.set_minor_locator(ticker.MultipleLocator(0.05)) 609 | ax1.legend( 610 | handles, 611 | labels, 612 | bbox_to_anchor=(0.0, 1.02, 1.0, 0.102), 613 | loc="lower left", 614 | ncols=3, 615 | borderaxespad=0.0, 616 | ) 617 | 618 | savefig(fig, filename) 619 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 2.1, February 1999 4 | 5 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies 9 | of this license document, but changing it is not allowed. 10 | 11 | [This is the first released version of the Lesser GPL. It also counts 12 | as the successor of the GNU Library Public License, version 2, hence 13 | the version number 2.1.] 14 | 15 | ### Preamble 16 | 17 | The licenses for most software are designed to take away your freedom 18 | to share and change it. By contrast, the GNU General Public Licenses 19 | are intended to guarantee your freedom to share and change free 20 | software--to make sure the software is free for all its users. 21 | 22 | This license, the Lesser General Public License, applies to some 23 | specially designated software packages--typically libraries--of the 24 | Free Software Foundation and other authors who decide to use it. You 25 | can use it too, but we suggest you first think carefully about whether 26 | this license or the ordinary General Public License is the better 27 | strategy to use in any particular case, based on the explanations 28 | below. 29 | 30 | When we speak of free software, we are referring to freedom of use, 31 | not price. Our General Public Licenses are designed to make sure that 32 | you have the freedom to distribute copies of free software (and charge 33 | for this service if you wish); that you receive source code or can get 34 | it if you want it; that you can change the software and use pieces of 35 | it in new free programs; and that you are informed that you can do 36 | these things. 37 | 38 | To protect your rights, we need to make restrictions that forbid 39 | distributors to deny you these rights or to ask you to surrender these 40 | rights. These restrictions translate to certain responsibilities for 41 | you if you distribute copies of the library or if you modify it. 42 | 43 | For example, if you distribute copies of the library, whether gratis 44 | or for a fee, you must give the recipients all the rights that we gave 45 | you. You must make sure that they, too, receive or can get the source 46 | code. If you link other code with the library, you must provide 47 | complete object files to the recipients, so that they can relink them 48 | with the library after making changes to the library and recompiling 49 | it. And you must show them these terms so they know their rights. 50 | 51 | We protect your rights with a two-step method: (1) we copyright the 52 | library, and (2) we offer you this license, which gives you legal 53 | permission to copy, distribute and/or modify the library. 54 | 55 | To protect each distributor, we want to make it very clear that there 56 | is no warranty for the free library. Also, if the library is modified 57 | by someone else and passed on, the recipients should know that what 58 | they have is not the original version, so that the original author's 59 | reputation will not be affected by problems that might be introduced 60 | by others. 61 | 62 | Finally, software patents pose a constant threat to the existence of 63 | any free program. We wish to make sure that a company cannot 64 | effectively restrict the users of a free program by obtaining a 65 | restrictive license from a patent holder. Therefore, we insist that 66 | any patent license obtained for a version of the library must be 67 | consistent with the full freedom of use specified in this license. 68 | 69 | Most GNU software, including some libraries, is covered by the 70 | ordinary GNU General Public License. This license, the GNU Lesser 71 | General Public License, applies to certain designated libraries, and 72 | is quite different from the ordinary General Public License. We use 73 | this license for certain libraries in order to permit linking those 74 | libraries into non-free programs. 75 | 76 | When a program is linked with a library, whether statically or using a 77 | shared library, the combination of the two is legally speaking a 78 | combined work, a derivative of the original library. The ordinary 79 | General Public License therefore permits such linking only if the 80 | entire combination fits its criteria of freedom. The Lesser General 81 | Public License permits more lax criteria for linking other code with 82 | the library. 83 | 84 | We call this license the "Lesser" General Public License because it 85 | does Less to protect the user's freedom than the ordinary General 86 | Public License. It also provides other free software developers Less 87 | of an advantage over competing non-free programs. These disadvantages 88 | are the reason we use the ordinary General Public License for many 89 | libraries. However, the Lesser license provides advantages in certain 90 | special circumstances. 91 | 92 | For example, on rare occasions, there may be a special need to 93 | encourage the widest possible use of a certain library, so that it 94 | becomes a de-facto standard. To achieve this, non-free programs must 95 | be allowed to use the library. A more frequent case is that a free 96 | library does the same job as widely used non-free libraries. In this 97 | case, there is little to gain by limiting the free library to free 98 | software only, so we use the Lesser General Public License. 99 | 100 | In other cases, permission to use a particular library in non-free 101 | programs enables a greater number of people to use a large body of 102 | free software. For example, permission to use the GNU C Library in 103 | non-free programs enables many more people to use the whole GNU 104 | operating system, as well as its variant, the GNU/Linux operating 105 | system. 106 | 107 | Although the Lesser General Public License is Less protective of the 108 | users' freedom, it does ensure that the user of a program that is 109 | linked with the Library has the freedom and the wherewithal to run 110 | that program using a modified version of the Library. 111 | 112 | The precise terms and conditions for copying, distribution and 113 | modification follow. Pay close attention to the difference between a 114 | "work based on the library" and a "work that uses the library". The 115 | former contains code derived from the library, whereas the latter must 116 | be combined with the library in order to run. 117 | 118 | ### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 119 | 120 | **0.** This License Agreement applies to any software library or other 121 | program which contains a notice placed by the copyright holder or 122 | other authorized party saying it may be distributed under the terms of 123 | this Lesser General Public License (also called "this License"). Each 124 | licensee is addressed as "you". 125 | 126 | A "library" means a collection of software functions and/or data 127 | prepared so as to be conveniently linked with application programs 128 | (which use some of those functions and data) to form executables. 129 | 130 | The "Library", below, refers to any such software library or work 131 | which has been distributed under these terms. A "work based on the 132 | Library" means either the Library or any derivative work under 133 | copyright law: that is to say, a work containing the Library or a 134 | portion of it, either verbatim or with modifications and/or translated 135 | straightforwardly into another language. (Hereinafter, translation is 136 | included without limitation in the term "modification".) 137 | 138 | "Source code" for a work means the preferred form of the work for 139 | making modifications to it. For a library, complete source code means 140 | all the source code for all modules it contains, plus any associated 141 | interface definition files, plus the scripts used to control 142 | compilation and installation of the library. 143 | 144 | Activities other than copying, distribution and modification are not 145 | covered by this License; they are outside its scope. The act of 146 | running a program using the Library is not restricted, and output from 147 | such a program is covered only if its contents constitute a work based 148 | on the Library (independent of the use of the Library in a tool for 149 | writing it). Whether that is true depends on what the Library does and 150 | what the program that uses the Library does. 151 | 152 | **1.** You may copy and distribute verbatim copies of the Library's 153 | complete source code as you receive it, in any medium, provided that 154 | you conspicuously and appropriately publish on each copy an 155 | appropriate copyright notice and disclaimer of warranty; keep intact 156 | all the notices that refer to this License and to the absence of any 157 | warranty; and distribute a copy of this License along with the 158 | Library. 159 | 160 | You may charge a fee for the physical act of transferring a copy, and 161 | you may at your option offer warranty protection in exchange for a 162 | fee. 163 | 164 | **2.** You may modify your copy or copies of the Library or any 165 | portion of it, thus forming a work based on the Library, and copy and 166 | distribute such modifications or work under the terms of Section 1 167 | above, provided that you also meet all of these conditions: 168 | 169 | - **a)** The modified work must itself be a software library. 170 | - **b)** You must cause the files modified to carry prominent 171 | notices stating that you changed the files and the date of 172 | any change. 173 | - **c)** You must cause the whole of the work to be licensed at no 174 | charge to all third parties under the terms of this License. 175 | - **d)** If a facility in the modified Library refers to a function 176 | or a table of data to be supplied by an application program that 177 | uses the facility, other than as an argument passed when the 178 | facility is invoked, then you must make a good faith effort to 179 | ensure that, in the event an application does not supply such 180 | function or table, the facility still operates, and performs 181 | whatever part of its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of 185 | the application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | **3.** You may opt to apply the terms of the ordinary GNU General 212 | Public License instead of this License to a given copy of the Library. 213 | To do this, you must alter all the notices that refer to this License, 214 | so that they refer to the ordinary GNU General Public License, version 215 | 2, instead of to this License. (If a newer version than version 2 of 216 | the ordinary GNU General Public License has appeared, then you can 217 | specify that version instead if you wish.) Do not make any other 218 | change in these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for that 221 | copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of the 225 | Library into a program that is not a library. 226 | 227 | **4.** You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy from 235 | a designated place, then offering equivalent access to copy the source 236 | code from the same place satisfies the requirement to distribute the 237 | source code, even though third parties are not compelled to copy the 238 | source along with the object code. 239 | 240 | **5.** A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a work, 243 | in isolation, is not a derivative work of the Library, and therefore 244 | falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. Section 250 | 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data structure 260 | layouts and accessors, and small macros and small inline functions 261 | (ten lines or less in length), then the use of the object file is 262 | unrestricted, regardless of whether it is legally a derivative work. 263 | (Executables containing this object code plus portions of the Library 264 | will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | **6.** As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a work 273 | containing portions of the Library, and distribute that work under 274 | terms of your choice, provided that the terms permit modification of 275 | the work for the customer's own use and reverse engineering for 276 | debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | - **a)** Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood that 294 | the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | - **b)** Use a suitable shared library mechanism for linking with 298 | the Library. A suitable mechanism is one that (1) uses at run time 299 | a copy of the library already present on the user's computer 300 | system, rather than copying library functions into the executable, 301 | and (2) will operate properly with a modified version of the 302 | library, if the user installs one, as long as the modified version 303 | is interface-compatible with the version that the work was 304 | made with. 305 | - **c)** Accompany the work with a written offer, valid for at least 306 | three years, to give the same user the materials specified in 307 | Subsection 6a, above, for a charge no more than the cost of 308 | performing this distribution. 309 | - **d)** If distribution of the work is made by offering access to 310 | copy from a designated place, offer equivalent access to copy the 311 | above specified materials from the same place. 312 | - **e)** Verify that the user has already received a copy of these 313 | materials or that you have already sent this user a copy. 314 | 315 | For an executable, the required form of the "work that uses the 316 | Library" must include any data and utility programs needed for 317 | reproducing the executable from it. However, as a special exception, 318 | the materials to be distributed need not include anything that is 319 | normally distributed (in either source or binary form) with the major 320 | components (compiler, kernel, and so on) of the operating system on 321 | which the executable runs, unless that component itself accompanies 322 | the executable. 323 | 324 | It may happen that this requirement contradicts the license 325 | restrictions of other proprietary libraries that do not normally 326 | accompany the operating system. Such a contradiction means you cannot 327 | use both them and the Library together in an executable that you 328 | distribute. 329 | 330 | **7.** You may place library facilities that are a work based on the 331 | Library side-by-side in a single library together with other library 332 | facilities not covered by this License, and distribute such a combined 333 | library, provided that the separate distribution of the work based on 334 | the Library and of the other library facilities is otherwise 335 | permitted, and provided that you do these two things: 336 | 337 | - **a)** Accompany the combined library with a copy of the same work 338 | based on the Library, uncombined with any other 339 | library facilities. This must be distributed under the terms of 340 | the Sections above. 341 | - **b)** Give prominent notice with the combined library of the fact 342 | that part of it is a work based on the Library, and explaining 343 | where to find the accompanying uncombined form of the same work. 344 | 345 | **8.** You may not copy, modify, sublicense, link with, or distribute 346 | the Library except as expressly provided under this License. Any 347 | attempt otherwise to copy, modify, sublicense, link with, or 348 | distribute the Library is void, and will automatically terminate your 349 | rights under this License. However, parties who have received copies, 350 | or rights, from you under this License will not have their licenses 351 | terminated so long as such parties remain in full compliance. 352 | 353 | **9.** You are not required to accept this License, since you have not 354 | signed it. However, nothing else grants you permission to modify or 355 | distribute the Library or its derivative works. These actions are 356 | prohibited by law if you do not accept this License. Therefore, by 357 | modifying or distributing the Library (or any work based on the 358 | Library), you indicate your acceptance of this License to do so, and 359 | all its terms and conditions for copying, distributing or modifying 360 | the Library or works based on it. 361 | 362 | **10.** Each time you redistribute the Library (or any work based on 363 | the Library), the recipient automatically receives a license from the 364 | original licensor to copy, distribute, link with or modify the Library 365 | subject to these terms and conditions. You may not impose any further 366 | restrictions on the recipients' exercise of the rights granted herein. 367 | You are not responsible for enforcing compliance by third parties with 368 | this License. 369 | 370 | **11.** If, as a consequence of a court judgment or allegation of 371 | patent infringement or for any other reason (not limited to patent 372 | issues), conditions are imposed on you (whether by court order, 373 | agreement or otherwise) that contradict the conditions of this 374 | License, they do not excuse you from the conditions of this License. 375 | If you cannot distribute so as to satisfy simultaneously your 376 | obligations under this License and any other pertinent obligations, 377 | then as a consequence you may not distribute the Library at all. For 378 | example, if a patent license would not permit royalty-free 379 | redistribution of the Library by all those who receive copies directly 380 | or indirectly through you, then the only way you could satisfy both it 381 | and this License would be to refrain entirely from distribution of the 382 | Library. 383 | 384 | If any portion of this section is held invalid or unenforceable under 385 | any particular circumstance, the balance of the section is intended to 386 | apply, and the section as a whole is intended to apply in other 387 | circumstances. 388 | 389 | It is not the purpose of this section to induce you to infringe any 390 | patents or other property right claims or to contest validity of any 391 | such claims; this section has the sole purpose of protecting the 392 | integrity of the free software distribution system which is 393 | implemented by public license practices. Many people have made 394 | generous contributions to the wide range of software distributed 395 | through that system in reliance on consistent application of that 396 | system; it is up to the author/donor to decide if he or she is willing 397 | to distribute software through any other system and a licensee cannot 398 | impose that choice. 399 | 400 | This section is intended to make thoroughly clear what is believed to 401 | be a consequence of the rest of this License. 402 | 403 | **12.** If the distribution and/or use of the Library is restricted in 404 | certain countries either by patents or by copyrighted interfaces, the 405 | original copyright holder who places the Library under this License 406 | may add an explicit geographical distribution limitation excluding 407 | those countries, so that distribution is permitted only in or among 408 | countries not thus excluded. In such case, this License incorporates 409 | the limitation as if written in the body of this License. 410 | 411 | **13.** The Free Software Foundation may publish revised and/or new 412 | versions of the Lesser General Public License from time to time. Such 413 | new versions will be similar in spirit to the present version, but may 414 | differ in detail to address new problems or concerns. 415 | 416 | Each version is given a distinguishing version number. If the Library 417 | specifies a version number of this License which applies to it and 418 | "any later version", you have the option of following the terms and 419 | conditions either of that version or of any later version published by 420 | the Free Software Foundation. If the Library does not specify a 421 | license version number, you may choose any version ever published by 422 | the Free Software Foundation. 423 | 424 | **14.** If you wish to incorporate parts of the Library into other 425 | free programs whose distribution conditions are incompatible with 426 | these, write to the author to ask for permission. For software which 427 | is copyrighted by the Free Software Foundation, write to the Free 428 | Software Foundation; we sometimes make exceptions for this. Our 429 | decision will be guided by the two goals of preserving the free status 430 | of all derivatives of our free software and of promoting the sharing 431 | and reuse of software generally. 432 | 433 | **NO WARRANTY** 434 | 435 | **15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 436 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 437 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 438 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 439 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 440 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 441 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 442 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 443 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 444 | 445 | **16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 446 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 447 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 448 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 449 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 450 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 451 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 452 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 453 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 454 | DAMAGES. 455 | 456 | ### END OF TERMS AND CONDITIONS 457 | 458 | ### How to Apply These Terms to Your New Libraries 459 | 460 | If you develop a new library, and you want it to be of the greatest 461 | possible use to the public, we recommend making it free software that 462 | everyone can redistribute and change. You can do so by permitting 463 | redistribution under these terms (or, alternatively, under the terms 464 | of the ordinary General Public License). 465 | 466 | To apply these terms, attach the following notices to the library. It 467 | is safest to attach them to the start of each source file to most 468 | effectively convey the exclusion of warranty; and each file should 469 | have at least the "copyright" line and a pointer to where the full 470 | notice is found. 471 | 472 | one line to give the library's name and an idea of what it does. 473 | Copyright (C) year name of author 474 | 475 | This library is free software; you can redistribute it and/or 476 | modify it under the terms of the GNU Lesser General Public 477 | License as published by the Free Software Foundation; either 478 | version 2.1 of the License, or (at your option) any later version. 479 | 480 | This library is distributed in the hope that it will be useful, 481 | but WITHOUT ANY WARRANTY; without even the implied warranty of 482 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 483 | Lesser General Public License for more details. 484 | 485 | You should have received a copy of the GNU Lesser General Public 486 | License along with this library; if not, write to the Free Software 487 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 488 | 489 | Also add information on how to contact you by electronic and paper 490 | mail. 491 | 492 | You should also get your employer (if you work as a programmer) or 493 | your school, if any, to sign a "copyright disclaimer" for the library, 494 | if necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in 497 | the library `Frob' (a library for tweaking knobs) written 498 | by James Random Hacker. 499 | 500 | signature of Ty Coon, 1 April 1990 501 | Ty Coon, President of Vice 502 | 503 | That's all there is to it! -------------------------------------------------------------------------------- /docs/rapport.tex: -------------------------------------------------------------------------------- 1 | %Locate this file one level lower than the folder with references: 2 | % -https://svn.oss.deltares.nl/repos/openearthtools/trunk/references 3 | %which must be checkout with in a folder named <00_references>. 4 | %E.g., locate this file renamed as in a structure: 5 | % -<#/00_references/> 6 | % -<#/01_memo/memo.tex> 7 | % 8 | %Folder <#00_reference/02_textree> must be in the MikTeX path. See <#00_reference/02_textree/readme> 9 | % 10 | % 11 | % 12 | % 13 | % 14 | % 15 | %---------------------------- 16 | %---------DOCUMENT TYPE 17 | %---------------------------- 18 | % 19 | 20 | \newcommand{\isreport}{1} %0=memo; 1=report 21 | \newcommand{\addAppendix}{1} %0=no appendix; 1=add appendix 22 | \newcommand{\addCitation}{0} %0=no; 1=yes 23 | \newcommand{\addmemosignatures}{0} %0=no; 1=yes 24 | 25 | %Add colon in \mySubtitle if desired or leave empty if no substitle is desired. 26 | \def\myTitle{Verkenning tool voor effecten ijsafvoer en dwarsstroming} 27 | \def\mySubtitle{} 28 | 29 | \def\myOrganisationi{Deltares} 30 | \def\myAuthori{Robert Groenewege} 31 | \def\myAuthorii{} 32 | \def\myPhonei{+31\,(0)64\,691\,1978} 33 | \def\myEmaili{robert.groenewege@deltares.nl} 34 | 35 | \def\myVersion{0.3} 36 | \def\myDate{\today} 37 | 38 | % 39 | % 40 | % 41 | %---------------------------- 42 | %---------DOCUMENT CLASS 43 | %---------------------------- 44 | % 45 | 46 | \ifnum \isreport=1 47 | \documentclass[dutch,signature]{deltares_report} 48 | %language: english, ducth, spanish 49 | %nosummary: writes no executive summary 50 | %signature: adds box for signatures 51 | \else 52 | \documentclass{deltares_memo} 53 | \fi 54 | 55 | % 56 | % 57 | % 58 | %---------------------------- 59 | %---------PACKAGES AND COMMANDS 60 | %---------------------------- 61 | % 62 | 63 | \input{00_references/abbreviations} 64 | \input{00_references/packages} 65 | \usepackage{pdfpages} 66 | \usepackage{tablefootnote} 67 | \usepackage{lipsum} 68 | \usepackage{mwe} 69 | \usepackage{subcaption} 70 | \usepackage{multirow} 71 | \usepackage{booktabs} 72 | \usepackage[table]{xcolor} 73 | \usepackage{siunitx} 74 | \usepackage{longtable} 75 | \usepackage{environ} 76 | 77 | \NewEnviron{requirement}%\usepackage{environ} 78 | {\emph{"\BODY"} \par\addvspace{5pt} } 79 | \NewEnviron{testmethod}{\BODY \vspace{5pt}} 80 | 81 | % 82 | % 83 | % 84 | %---------------------------- 85 | %---------DOCUMENT INFORMATION 86 | %---------------------------- 87 | % 88 | 89 | %%%%%%%% 90 | %---------REPORT INPUT 91 | %%%%%%%% 92 | % 93 | 94 | \ifnum \isreport=1 95 | 96 | \def\authorList{\myAuthori{}, \myAuthorii{}} %Where does this go in the layout? 97 | 98 | %adding a different figure on the cover 99 | % 100 | %\renewcommand{\FrontCover}{\includegraphics[height=182mm,width=182mm]{../00_figures/12_other/DJI_0017.JPG}} 101 | %\renewcommand{\FrontCircle}{\includegraphics[height=182mm,width=182mm]{../00_references/01_layouts/cover/cover_transparant_circle.pdf}} 102 | %\renewcommand{\FrontCover}{\includegraphics[height=182mm,width=182mm]{pictures/Deltares_rapport_omslag_gennepperhuis.jpg}} 103 | %prevent sentence break between pages 104 | \widowpenalties 1 10000 105 | \raggedbottom 106 | 107 | \begin{document} 108 | 109 | \title{\myTitle} 110 | \subtitle{\mySubtitle} 111 | \author{\myAuthori{} \\ 112 | \myAuthorii{}} 113 | \partner{} 114 | \coverPhoto{} 115 | \date{\today} 116 | \version{\myVersion} 117 | 118 | %\foreach \var [evaluate=\var as \myindex using {int(\var-1)}] in {1} { 119 | %\pgfmathparse{\authorList[\myindex]}\pgfmathresult 120 | %} 121 | 122 | \authori{\myAuthori{}} 123 | \organisationi{\myOrganisationi} 124 | %\authorii{\myAuthorii{}} 125 | %\organisationii{\myOrganisationi} 126 | 127 | %revision table 128 | \revieweri{Anna Kosters} 129 | \datei{\myDate} 130 | \versioni{\myVersion} 131 | \approvali{} 132 | \publisheri{} 133 | \authorii{\myAuthorii{}} 134 | 135 | \client{Rijkswaterstaat} 136 | \contact{Arjan Sieben} 137 | \keywords{rekentool, rivierkundig, beoordelingskader, ijs, dwarsstroming, ingreep} 138 | \reference{} 139 | \classification{} 140 | \status{concept} 141 | \disclaimer{} 142 | \summary{De beoordeling van de rivierkundige effecten van voorgenomen ingrepen in de grote rivieren in Nederland wordt met behulp van het Rivierkundig Beoordelingskader (RBK) \citep{RWS23} uitgevoerd. Echter blijkt in de praktijk de beoordeling van de invloed van ingrepen op ijsafvoer lastig uit te voeren; een eenduidige, efficiënte methodiek ontbreekt. Ook voor de beoordeling van effecten van ingrepen op dwarsstroming zijn (en worden) door initiatiefnemers verschillende aanpakken ontwikkeld. Met een uniforme aanpak kan de kwaliteit beter worden geborgd. 143 | 144 | Het doel van deze studie is het ontwikkelen van een eenduidige aanpak met rekentool, voor de RBK-bepaling van dwarsstroom- en ijsafvoereffecten. Hiermee moet, naar gelang de behoefte, eenduidig op uniforme wijze de grootte van dwarsstroming en dwarsstromingseffecten en de relevante variabelen voor het schatten van de invloed van maatregelen op de doorvoer van ijs kunnen worden bepaald, conform de specificaties in het RBK. Dit is een gecombineerde aanpak van twee verschillende aspecten (ijsafvoer en dwarsstroming op de vaarweg) vanwege een redelijke overlap in de voor evaluatie relevante variabelen. 145 | 146 | Er is een prototype van de rekentool ontwikkeld, dat bestaat uit eenvoudige scripts die via een Command Line Interface (CLI) kunnen worden uitgevoerd. Anticiperend op een mogelijke implementatie in de D-FAST productlijn is er al zo veel mogelijk aangesloten op de code van D-FAST-MI en -BE. De tool is enkel getest op rivierafvoer-gedomineerde takken met stationaire afvoersommen en nog niet op getij-gedomineerde takken. Uit de validatie met een hypothetische ingreep blijkt de tool geschikt voor het beoogde doel. Uit eerste karakteriseringen van het huidige functioneren van de gehele Rijn en Maas met betrekking tot de afvoer van ijs en dwarsstroming, blijken er tevens tientallen overschrijdingen of knelpunten te bestaan. 147 | 148 | Het wordt aanbevolen om de rekentool binnen de D-FAST productlijn op te nemen en richtlijnen te ontwikkelen voor toepassing van de tool op getij-gedomineerde riviertakken. Ook wordt aanbevolen de huidige uitvoer van de tool uit te breiden met tweedimensionale figuren van stroomsnelheid en -richting, ten behoeve van het automatisch bepalen van het stroomvoerend profiel. Als laatst moet de tool nog getest worden met andere schipafmetingen.} 149 | 150 | \documentid{11211565-010-ZWS-0001} 151 | \projectnumber{11211565-010} 152 | 153 | 154 | %%%%%%%% 155 | %---------MEMO INPUT 156 | %%%%%%%% 157 | % 158 | 159 | 160 | \else 161 | 162 | \begin{document} 163 | %\memoTo{Aukje Spruyt, Johan Boon} 164 | %\memoConfidentialUntil{} 165 | %\memoName{Memo} %Name in header. If commented out, default "Memo" is used. 166 | %\memoDate{\today\currenttime} 167 | %\memoVersion{\myVersion{}} %11206793-013-ZWS-0001_v0.1-groynes.docx 168 | %\memoFrom{\parbox[t]{3cm}{ 169 | \myAuthori{} 170 | \myAuthorii{} \\ 171 | }} 172 | \memoTelephone{\parbox[t]{3cm}{ 173 | \myPhonei{} 174 | %\myPhoneii{} \\ 175 | }} 176 | \memoEmail{\parbox[t]{3cm}{ 177 | \myEmaili{} 178 | %\myEmailii{} 179 | }} 180 | \memoSubject{\myTitle{}\mySubtitle{}} 181 | \memoCopy{} 182 | 183 | \fi 184 | 185 | % 186 | % 187 | % 188 | %---------------------------- 189 | %---------COVER 190 | %---------------------------- 191 | % 192 | %\svnInfo $Id: report.tex 83 2022-12-05 10:24:56Z ottevan $ 193 | \deltarestitle 194 | \ifnum \isreport=1 195 | \newpage 196 | \fi 197 | \ifnum \isreport=0 198 | \ifnum \addmemosignatures=1 199 | \begin{tabular}{p{\textwidth/8}|p{0.175\textwidth}|p{0.2\textwidth}|p{0.2\textwidth}|p{0.2\textwidth}} 200 | \rowcolor{dblue1} \textbf{Document version} & \textbf{Date} & \textbf{Author} & \textbf{Reviewer} & \textbf{Approval} \\ 201 | \topline 202 | 0.2 & & Robert Groenewege & Anna Kosters & \\ 203 | \midline 204 | & & & \\ 205 | \midline 206 | \end{tabular} 207 | \fi 208 | \fi 209 | 210 | % 211 | % 212 | % 213 | %---------------------------- 214 | %---------START DOCUMENT 215 | %---------------------------- 216 | % 217 | 218 | \def\RijnFigDir{../examples/c01 - Rijn/} 219 | \def\MaasFigDir{../examples/c02 - Maas/} 220 | \def\RMMFigDir{../examples/c03 - RMM/} 221 | \def\NVOMaasFigDir{../examples/c04 - NVO Maas/figures/} 222 | 223 | \newcommand{\insertdoublefigure}[3]{ 224 | \begin{figure}[hbt!] 225 | \centering 226 | \begin{subfigure}[b]{0.5\textwidth} 227 | \centering 228 | \includegraphics[width=\textwidth]{#1} 229 | \end{subfigure}\hfill 230 | \begin{subfigure}[b]{0.5\textwidth} 231 | \centering 232 | \includegraphics[width=\textwidth]{#2} 233 | \end{subfigure} 234 | \captionsetup{justification=centering} 235 | \caption{#3} 236 | \label{#3} 237 | \end{figure} 238 | } 239 | 240 | \newcommand{\insertfrfigure}[3]{ 241 | \begin{figure}[hbt!] 242 | \centering 243 | \begin{subfigure}[b]{\textwidth} 244 | \centering 245 | \includegraphics[width=\textwidth,height=0.8\textheight,keepaspectratio]{#1} 246 | \end{subfigure}\hfill 247 | \begin{subfigure}[b]{\textwidth} 248 | \centering 249 | \includegraphics[width=\textwidth,height=0.2\textheight,keepaspectratio]{#2} 250 | \end{subfigure} 251 | \captionsetup{justification=centering} 252 | \caption{#3} 253 | \label{#3} 254 | \end{figure} 255 | } 256 | 257 | %Second input is the level of the section. 258 | \gensection{\isreport}{1}{Projectomschrijving} 259 | \label{sec:projectplan} 260 | 261 | \gensection{\isreport}{2}{Aanleiding} 262 | De rivierkundige effecten van voorgenomen ingrepen (maatregelen) in de grote rivieren in Nederland worden bij vergunningverlening door Rijkswaterstaat (RWS) beoordeeld. Zo moet een goede geleiding van ijs gewaarborgd blijven om de kans op ijsdammen, waterstandsopstuwing en overstromingen te minimaliseren. Ook de component van de stroming dwars op de vaarweg (dwarsstroming) mag niet te groot worden omdat dit hinder of onveiligheid voor scheepvaart kan opleveren. 263 | 264 | De beoordeling van de effecten van ingrepen wordt met behulp van het Rivierkundig Beoordelingskader (RBK) \citep{RWS23} uitgevoerd. Echter blijkt in de praktijk de beoordeling van de invloed van ingrepen op ijsafvoer lastig uit te voeren; een eenduidige, efficiënte methodiek ontbreekt. Ook voor de beoordeling van effecten van ingrepen op dwarsstroming zijn (en worden) door initiatiefnemers verschillende aanpakken ontwikkeld. Met een uniforme aanpak kan de kwaliteit beter worden geborgd. 265 | 266 | In 2024 is door Kaderrichtlijn Water (KRW) projecten voor de Rijntakken een aanpak gevolgd voor het kwantitatieve onderdeel van de beoordeling van effecten op ijsafvoer. Voor SITO Rivierkunde is de invloed van onregelmatige oevers op rivierfuncties (veilige afvoer van water, sediment en ijs, dwarsstroming, vlot en veilig varen en ondersteuning laagwaterstanden) onderzocht door \citet{Groenewege25}. Beide ontwikkelingen geven perspectief op generiek gebruik dat in een rekentool geformaliseerd kan worden. 267 | 268 | \gensection{\isreport}{2}{Doel} 269 | Het doel van deze studie is het ontwikkelen van een eenduidige aanpak met rekentool, voor de RBK-bepaling van dwarsstroom- en ijsafvoereffecten, waarmee naar gelang de behoefte, eenduidig 270 | \begin{itemize} 271 | \item op uniforme wijze de grootte van dwarsstroming en dwarsstromingseffecten kan worden bepaald conform de specificaties in het RBK; 272 | \item de relevante variabelen voor het schatten van de invloed van maatregelen op de doorvoer van ijs kunnen worden bepaald, conform de specificaties in het RBK, inclusief correcties voor invloeden van benedenstrooms ijsdek en lokale bodemveranderingen. 273 | \end{itemize} 274 | 275 | Dit is een gecombineerde aanpak van twee verschillende aspecten (ijsafvoer en dwarsstroming op de vaarweg) vanwege een redelijke overlap in de voor evaluatie relevante variabelen. 276 | 277 | Bij de rekentool gaat het in 2025 om een serie van eenvoudige scripts, en nadrukkelijk nog niet om een tool die uitgeleverd kan worden voor gebruik door ingenieursbureaus. Mits de resultaten veelbelovend zijn kan in een volgende stap erover worden nagedacht hoe dit omgezet kan worden in een tool die door de markt toegepast kan worden, bijvoorbeeld een aanvulling van de set tools onder D-FAST. D-FAST is een softwareproductlijn ontwikkeld door Deltares en bestaat momenteel uit 2 applicaties: D-FAST Morphological Impact (MI) en D-FAST Bank Erosion (BE). Met D-FAST MI kan een eerste inschatting gemaakt worden van het effect van een maatregel op de bodem van de hoofdgeul. D-FAST BE is een hulpmiddel voor een snelle beoordeling van de erosie van de rivieroever. Met eventuele implementatie van deze rekentool in D-FAST moet dan ook worden bekeken hoe het beheer en onderhoud van de tool kan worden gewaarborgd. Wel is in 2025 geprobeerd om al zo veel mogelijk aan te sluiten bij de code van D-FAST-MI en -BE. 278 | 279 | Het doel van dit rapport is het toelichten van de ontwikkelde rekentool, aan de hand van een algemene beschrijving, validatie met een geselecteerde ingreep, en een eerste karakterisering van het huidige functioneren van de Rijn en Maas. 280 | 281 | \gensection{\isreport}{2}{Werkzaamheden} 282 | \label{} 283 | 284 | De volgende werkzaamheden zijn uitgevoerd: 285 | \begin{itemize} 286 | \item Definitie van de tool ten aanzien van de inhoud (toepassingsgebied, beperkingen, enz.) en het gebruik (processing D-HYDRO-rekenresultaten, format rapportage) 287 | \item Scripting en test 288 | \item Toepassing voor de Rijn en Maas: rapportage en interpretatie van rivierstukken middels eerste kwalificatie 289 | \item Rapportage met een beschrijving van de tool en de resultaten van de toepassing 290 | \item Afstemming met de RWS klankbordgroep, bestaande uit Arjan Sieben (RWS Water, Verkeer en Leefomgeving (WVL)), William de Lange (RWS WVL), Joey Ewals (RWS Zuid-Nederland (ZN)), Emiel Kater (RWS Oost-Nederland (ON)), Mirjam Flierman (RWS West-Nederland Zuid (WNZ)). Tevens heeft afstemming plaatsgevonden met Hans Veldman en Walter van Doornik (beiden RWS ON) en Mark Bos (RWS WVL). 291 | \end{itemize} 292 | 293 | 294 | Dit leidt tot de volgende opgeleverde producten: 295 | \begin{itemize} 296 | \item prototype van de tool (zie bijgeleverde broncode) 297 | \item rapport (voorliggend document) 298 | \item instructies voor installeren en gebruik (zie Bijlage \ref{app:gebruiksinstructies}) 299 | \end{itemize} 300 | 301 | 302 | 303 | \gensection{\isreport}{1}{Achtergrond} 304 | \label{} 305 | 306 | \gensection{\isreport}{2}{Afvoer van ijs} 307 | \label{achtergrond_ijs} 308 | Het RBK stelt dat een goede geleiding van ijs en water gewaarborgd moet blijven \citep[sectie 1.5]{RWS23}: 309 | 310 | \begin{quote} 311 | "De volgende ontwerpprincipes zijn relevant voor een goede afvoer van ijs: 312 | \begin{itemize} 313 | \item In het stroomvoerend profiel mag de ingreep voor afvoeren vanaf bankfull tot grofweg 75 jaar herhalingstijd (Lobith van 4000 tot 8000 $m^3/s$, Borgharen van 1500 tot 2800 $m^3/s$), ook in scenario’s met benedenstrooms ijsdek, de Froude getallen niet verlagen tot onder 0.08, om de kans op ontwikkeling van ijsdammen niet te verhogen); 314 | \item Verander lokaal de normaalbreedte van de rivier niet. Lokale versmallingen kunnen de ijsafvoer blokkeren; 315 | \item Laat de gestrekte oevers in stand, met name in het splitsingspuntengebied; 316 | \item Voorkom dat grote stukken ijs massaal vanuit de nevengeul in de hoofdgeul kunnen stromen en op die manier blokkades gaan vormen; 317 | \item Voorkom de vorming van ondieptes in het zomerbed." 318 | \end{itemize} 319 | \end{quote} 320 | 321 | De beoordeling van de doorvoer van ijs is in de meeste gevallen grotendeels kwalitatief. Echter, de volgende ontwerpprincipes uit het RBK \citep{RWS23} kunnen eenduidig kwantitatief worden toegepast met hydraulische D-HYDRO simulaties\footnote{Hoewel het RBK \citep[Deel D]{RWS23} nog WAQUA specificeert om hydraulische effecten van een ingreep te bepalen, is dit model al grotendeels uitgefaseerd en wordt in de praktijk meer gebruik gemaakt van D-HYDRO.}: 322 | \begin{enumerate} 323 | \item In het stroomvoerend profiel mag de ingreep voor afvoeren vanaf bankfull tot grofweg 75 jaar herhalingstijd (Lobith van 4000 tot 8000 $m^3/s$, Borgharen van 1500 tot 2800 $m^3/s$), ook in scenario’s met benedenstrooms ijsdek, de Froude getallen niet verlagen tot onder 0.08, om de kans op ontwikkeling van ijsdammen niet te verhogen. 324 | \item Verander lokaal de normaalbreedte van de rivier niet. Lokale versmallingen kunnen de ijsafvoer blokkeren. \item Laat de gestrekte oevers in stand, met name in het splitsingspuntengebied. 325 | \end{enumerate} 326 | 327 | Ad 1) Het Froude-getal karakteriseert de relatieve convectie van stroming met ijs. Bij stroming met ijs tegen ijsdekken kunnen volgens het criterium van Kivisild ijsdammen verwacht worden voor Froude-getallen onder 0.015 á 0.150 \citep{Termes91}. Het gemiddelde van deze range, 0.08, lijkt een goede grenswaarde voor ijsdamvorming \citep{Zagonjolli19}. 328 | 329 | Dit betreft de overgang van stroming met los ijs naar stroming onder een vast ijsdek. Een haperende doorvoer van ijs leidt tot vorming van ijsdammen. De vrije stroming (met los ijs) ondergaat opstuwing vanuit het benedenstroomse, vaste ijsdek en wordt ook beïnvloed door morfologische effecten van de ingreep. Het Froude-getal dat stroming karakteriseert met D-HYDRO resultaten zonder ijsdek en morfologische effecten heeft dus twee correcties nodig. Deze zijn beschreven in bijlage \ref{app:ijscorrecties}. 330 | 331 | Ad 2) De ijsafvoer stagneert bij een afnemend ijs-transporterend vermogen. In trajecten met getij is dit het geval gedurende doodtij en kentering van het getij. In stationaire stroming is dat ter plekke van verbredingen en splitsingen, bij obstakels, op ondiepten en in sterk gekromde stroming. Een constante normaalbreedte met vloeiend verlopende normaallijnen borgt een goede ijsafvoer, omdat dan gradiënten in stroming en daarmee de gradiënten in ijs-transporterend vermogen beperkt zijn. 332 | 333 | Vanwege de invloed op meerdere takken weegt handhaving van deze normaalbreedten (en de normaallijnen die deze lijnen definiëren) in en rondom de splitsingspuntgebieden zeer zwaar. Daarbuiten kan, om de continuïteit in ijs-transporterend vermogen te borgen, de invloed van maatregelen op de ijsafvoer van stationaire stroming (dus zonder getij) voldoende gekarakteriseerd worden met de gradiënt in grootte en richting van de stroomsnelheid. Een ontwerp mag in het rivierstuk van de ingreep, bij de genoemde rivierafvoeren niet leiden tot meer of grotere van deze gradiënten. 334 | 335 | \gensection{\isreport}{2}{Dwarsstroming} 336 | Het kwantitatieve onderdeel van de beoordeling van invloeden op de vaarweg is veelal gericht op bepaling van dwarsstroomsnelheden. Immers, van ingrepen wordt verwacht dat deze bij dwarsstroomdebieten groter dan 50 m³/s, de dwarsstroming in de vaarweg niet boven de richtlijn van 0.15 m/s verhogen, tenzij hierdoor de padbreedte\footnote{De padbreedte is de ruimte of vaarroute waar een schip ten allen tijde gebruik van kan maken om te manoeuvreren en normale uitwijkmanoeuvres te maken. Voor een normaal vaarwegprofiel geldt een padbreedte van 2 maal de toegelaten scheepsbreedte.}van passerende schepen niet meer dan een halve scheepsbreedte toeneemt. Bij debieten kleiner dan 50 m³/s is dit een richtlijn van 0.30 m/s. Dit zijn de criteria in het RBK \citep{RWS23}, toegespitst op nevengeulen e.d. In de Richtlijn Vaarwegen 2020 \citep{Koedijk20} zijn criteria opgenomen voor geconcentreerde dwarsstroming: kleinere debieten met grotere uitstroomsnelheid (bv. in/uitlaat koelwater bij een centrale gemaal/ riooloverstort/ inlaatpunt, e.d.). Binnen RWS WVL loopt er momenteel een actie om de criteria vanuit deze twee invalshoeken beter op elkaar aan te laten sluiten (pers. comm. RWS klankbordgroep). De tool moet dan ook anticiperen op aanpassing van de criteria. 337 | 338 | De toepassing voor RBK beoordeling begint veelal met het in kaart brengen van stroombeelden en stroomsnelheden voor de situatie zonder ingrepen en voor de situatie met ingrepen. De dwarsstroomsnelheid op de rand van de vaarweg (zoals gedefinieerd in het RBK) moet voor een aantal kenmerkende rivierafvoeren en getijverlopen (Rijn-Maasmonding (RMM)) worden gepresenteerd in grafieken. Deze grafieken geven de rivierbeheerder inzicht in de effecten van de ingreep op dwarsstromingen. Ook de representatieve dwarsstroomsnelheid moet kunnen worden berekend en gepresenteerd. Dit betreft de dwarsstroomsnelheid die representatief is voor de vaarweg in geval van gestrekte oevers \citep[zie][Bijlage 7]{RWS23}. 339 | 340 | \gensection{\isreport}{1}{Beschrijving van de rekentool} 341 | In dit hoofdstuk wordt beschreven wat de (beoogde) werking en functionaliteit van de rekentool is. 342 | 343 | \gensection{\isreport}{2}{Inleiding} 344 | De broncode van de tool is geschreven in de programmeertaal Python. Dit maakt het makkelijker om in een later stadium de tool binnen de D-FAST familie, ook geschreven in Python, te implementeren. Om deze reden wordt er ook zoveel mogelijk gebruik gemaakt van al bestaande functies van D-FAST. 345 | 346 | De tool is te gebruiken als "command line interface" (CLI) zonder een grafische gebruikersomgeving (GUI). De tool is meegeleverd met instructies voor installeren en gebruik (zie Bijlage \ref{app:gebruiksinstructies}). De benodigde invoer van de tool betreft het volgende, vergelijkbaar met D-FAST MI: 347 | \begin{itemize} 348 | \item map- of fourier uitvoerbestanden van D-HYDRO simulaties; 349 | \item configuratiebestanden van de riviertakken (naam, trajecten, afmetingen van schepen, enz.); 350 | \item configuratiebestand van de analyse (hiermee kunnen gebruikers verwijzen naar de relevante D-HYDRO simulaties en resultaten van D-FAST-MI). 351 | \end{itemize} 352 | 353 | De uitvoer van de tool bestaat uit figuren, Excel bestanden en netCDF bestanden. In de volgende secties is dit verder uitgewerkt. 354 | 355 | \gensection{\isreport}{2}{Algemeen ontwerp} 356 | 357 | Uit de feedback vanuit de RWS klankbordgroep zijn enkele algemene vereisten aan de tool gedefinieerd. Hieronder worden die eisen opgenoemd, samen met de huidige status van de rekentool: 358 | \begin{enumerate} 359 | \item \begin{requirement} 360 | Geef gebruikers de mogelijkheid om de code aan te passen/suggesties te doen voor verbetering. Beheerder kan ze dan al/of niet opnemen in de officiële versie. 361 | \end{requirement} 362 | \begin{testmethod} 363 | De broncode is gedeeld met de klankbordgroep. Als er wordt besloten om de rekentool in beheer en onderhoud op te nemen, zal worden gekeken naar open-source. 364 | \end{testmethod} 365 | 366 | \item \begin{requirement} 367 | Formuleringen en criteria moeten 1 op 1 overeenkomen met RBK. 368 | De tool moet anticiperen op aanpassing van criteria voor de dwarsstroming. 369 | De tool moet geschikt zijn voor het berekenen van de representatieve dwarsstroming. 370 | \end{requirement} 371 | \begin{testmethod} 372 | Dit is het geval. Echter is het RBK6 - wat betreft de afvoer van ijs en dwarsstroming - nog onduidelijk over formuleringen en criteria voor de RMM en is er meer uitzoekwerk nodig voordat dit in de rekentool beschikbaar kan worden gemaakt. 373 | \end{testmethod} 374 | 375 | \item \begin{requirement} 376 | Voor de RMM moet de tool de maxima kunnen berekenen voor eb en vloed. 377 | \end{requirement} 378 | \begin{testmethod} 379 | Dit is nog niet geïmplementeerd (zie ook de software-eis hierboven). Voor D-FAST-MI is er al wat code geschreven om de maximale stroming met getij te bepalen, maar dat is nu nog lastig aan te sluiten op deze rekentool. Het toepassingsgebied van deze rekentool-prototype is vooralsnog beperkt tot de takken zonder getij totdat er aangesloten kan worden op de code in D-FAST (zie sectie \ref{sec:toepassingsgebied}). 380 | \end{testmethod} 381 | 382 | \item \begin{requirement} 383 | De tool moet ook een verschilplot (bijvoorbeeld op de secundaire as) kunnen genereren. 384 | De tool moet geen afstand over de lijn visualiseren maar rivierkilometer (rkm) op de x-as. 385 | \end{requirement} 386 | \begin{testmethod} 387 | Dit is momenteel standaard uitvoer van de rekentool. 388 | \end{testmethod} 389 | 390 | \item \begin{requirement} 391 | De tool moet de mogelijkheid bieden om modelresultaten langs de raai-km van de rivier te plotten. 392 | \end{requirement} 393 | \begin{testmethod} 394 | Om resultaten langs de raai-km van de rivier te plotten, moet in het configuratiebestand voor het keyword \texttt{RiverKM} onder \texttt{General} een XYC bestand \citep{dfastmi_usermanual} met raai-km's worden opgegeven. 395 | \end{testmethod} 396 | 397 | \item \begin{requirement} 398 | De tool moet de mogelijkheid bieden voor een inverse rkm-as. 399 | \end{requirement} 400 | \begin{testmethod} 401 | Dit is mogelijk middels het keyword \texttt{InvertXAxis} (0=nee,1=ja) onder \texttt{General}. 402 | \end{testmethod} 403 | 404 | \item \begin{requirement} 405 | De gebruiker moet zelf lijnen kunnen definiëren, waarlangs de modelresultaten worden geplot. 406 | \end{requirement} 407 | \begin{testmethod} 408 | Dit is mogelijk middels het keyword \texttt{ProfileLines} (pad naar GIS bestand, relatief aan configuratiebestand) onder \texttt{General}. 409 | \end{testmethod} 410 | 411 | \end{enumerate} 412 | 413 | \gensection{\isreport}{2}{Beoordeling van de afvoer van ijs} 414 | Om de afvoer van ijs te beoordelen, kunnen de amplitude en richting van stroomsnelheden ten eerste gevisualiseerd worden langs drie lijnen (ééndimensionaal): 415 | \begin{itemize} 416 | \item de twee lijnen die al gebruikt worden voor het in beeld brengen van dwarsstroming (of, als deze ontbreken, de waterlijn bij bankvullende afvoer). 417 | \item de lijn over de rivieras die wordt gebruikt voor de waterstandseffecten op de as. 418 | \end{itemize} 419 | Stroomsnelheden langs deze lijnen worden door de tool over voldoende lengte gevisualiseerd om een goede vergelijking van de waarden ter plekke van de ingreep met waarden op ongestoorde oeverdelen mogelijk te maken. 420 | 421 | Ten tweede produceert de tool figuren van Froude getallen voor de situatie zonder ingreep en voor de situatie met ingreep, met een classificatie van de toe- en afname in verschillende klassen (Figuur \ref{fig:Froude}). Dit is inclusief de twee benodigde correcties (zie Bijlage \ref{app:ijscorrecties}). 422 | 423 | \begin{figure*}[hbt!] 424 | \centering 425 | \includegraphics[width=\textwidth]{01_figures/HKV_Froude.png} 426 | \captionsetup{justification=centering} 427 | \caption{Beoogde visualisatie van Froude getallen. Bron: HKV} 428 | \label{fig:Froude} 429 | \end{figure*} 430 | 431 | \gensection{\isreport}{2}{Beoordeling van dwarsstroming} 432 | \label{sec:beschrijving_dwarsstroming} 433 | De beoordeling van dwarsstroming is uitvoerig beschreven in het RBK \citep[bijlage 7]{RWS23}. De tool automatiseert de stappen voor de bepaling en presentatie van (representatieve) dwarsstroming. 434 | 435 | De geproduceerde figuren volgen de opzet van Figuur \ref{fig:dwarsstroming}, maar visualiseren óók het variërende criterium (0.15 of 0.3 $m/s$) dat hoort bij het lokale dwarsstroomdebiet. De dwarsstroming wordt gepresenteerd langs de lijn(en) die de gebruiker zelf opgeeft (zie volgende alinea). Overschrijdingen van het (lokale) criterium worden ook apart weggeschreven in een Excel bestand. Het is mogelijk om in één figuur zowel de lijn voor een situatie zonder ingreep als de lijn voor een situatie met ingreep te visualiseren, zodat het effect van de ingreep inzichtelijk is. Op de secundaire as wordt het verschil tussen de situatie zonder en met ingreep toegevoegd. Dit leidt tot drie lijnen: 436 | \begin{enumerate} 437 | \item Referentie (zwarte lijn) 438 | \item Met ingreep (blauwe lijn) 439 | \item Verschil = ingreep - referentie (rode lijn) 440 | \end{enumerate} 441 | 442 | Bij RWS-ZN/Maas is het normprofiel bepalend voor de RBK-toets op dwarsstroming. Bij RWS-ON/Rijntakken is de rand van de vaarweg de norm. Deze wordt gemarkeerd door de bakenlijn langs de kribbakens. Bij de Waal en Nederrijn/Lek komt de bakenlijn vrijwel overeen met de normaallijn. Bij de IJssel ligt de normaallijn enkele meters uit de bakenlijn en zijn er ook trajecten waar de bakenlijn en/of de normaallijn is gewijzigd. Vanwege de diversiteit aan mogelijke profiellijnen, is ervoor gekozen om in de tool de gebruiker de mogelijkheid te bieden zelf één of meerdere lijnen aan te leveren. 443 | 444 | \begin{figure*}[hbt!] 445 | \centering 446 | \includegraphics[width=\textwidth]{01_figures/RBK_dwarsstroming.png} 447 | \captionsetup{justification=centering} 448 | \caption{Voorbeeld van de visualisatie van dwarsstroming. "Links wordt het dwarsstroomdebiet bepaald in geval van een brede in-/uitstroming. Het grijs gearceerde gebied betreft het maximale debiet over de lengte van een maatgevend schip ($L_{schip}$) binnen de breedte van de in-/uitstroming ($W_u$). Rechts wordt het dwarsstroomdebiet bepaald in geval van een geconcentreerde in-/uitstroming". Bron: \citet[Bijlage 7, Figuur 1]{RWS23}} 449 | \label{fig:dwarsstroming} 450 | \end{figure*} 451 | 452 | \gensection{\isreport}{2}{Toepassingsgebied} 453 | \label{sec:toepassingsgebied} 454 | In principe kan de tool uitgevoerd worden voor alle riviertakken waarvoor maatgevende schipafmetingen zijn gedefinieerd (in het RBK \citep[Bijlage 7, Tabel 1]{RWS23}). Dit betreft: 455 | 456 | \begin{itemize} 457 | \item Bovenrijn (Rkm 859-867) 458 | \item Waal (Rkm 868-951) 459 | \item Pannerdensch-Kanaal (Rkm 868-879) 460 | \item Nederrijn-Lek (Rkm 880-989) 461 | \item IJssel (Rkm 880-1000) 462 | \item Zwarte Water 463 | \item Merwedes (Rkm 951-980) 464 | \item Maas (Rkm 16-227) 465 | \item Amer 466 | \item Haringvliet 467 | \item Hollands Diep 468 | \end{itemize} 469 | 470 | De tool is ook nog alleen getest op riviertakken waar de invloed van getij verwaarloosbaar klein is ten opzichte van rivierafvoer. Anticiperend op implementatie van de tool in D-FAST wordt er in deze studie nog weinig aandacht besteed aan toepassing van de tool op takken met getij. Voor D-FAST-MI wordt hier namelijk al aan gewerkt en het is de verwachting dat de benodigde invoer, definities en criteria hier deels uit zullen kristalliseren. Op termijn is het de bedoeling om hierop aan te sluiten. 471 | 472 | \gensection{\isreport}{2}{Beperkingen} 473 | \label{beperkingen} 474 | De volgende beperkingen aan de rekentool zijn momenteel bekend (zie ook hoofdstuk \ref{conclusies}): 475 | \begin{itemize} 476 | \item Als de analyse wordt uitgevoerd op een gebied met meerdere rivierassen, zoals in de RMM, mogen er in de invoer geen dubbelingen in de rivierkilometers voorkomen. Er moet dus gekozen worden voor één as(lijn). 477 | \item Voorlopig is het nog niet mogelijk om met de tool maximale stroming tijdens eb en vloed uit te rekenen. Als alternatief kan de gebruiker zelf een fourier of map-bestand als invoer opgeven met maximale stroming tijdens eb en/of vloed ter hoogte van de ingreep. 478 | \item De berekening van dwarsstroomdebiet is momenteel afhankelijk van een correcte rivierkilometrering, omdat dit als de parameter voor afstand wordt gekozen. Daar waar verspringingen plaatsvinden is het berekende dwarsstroomdebiet onnauwkeurig. Bijvoorbeeld op de IJssel tussen rkm 905 en 910 wordt de afstand overschat en dus ook het berekende dwarsstroomdebiet. In een volgende versie kan dit worden verholpen. 479 | \item Daar waar de stroomsnelheid zeer klein is, kan in de figuren de stromingsrichting ten opzichte van de profiellijn grote uitslagen tonen. 480 | \item De tool produceert nog geen 2D figuren van stroomsnelheid en -richting. Dit zou handig zijn om het stroomvoerend profiel te bepalen. 481 | \item Voor andere schipafmetingen, bv. de recreatievaart, is de tool nog niet getest. Er kan al wel gesteld worden dat een correcte uitvoer van de tool onder andere berust op correcte invoer, waaronder een nauwkeurige representatie van de stroming met D-HYDRO. Hiervoor geldt de richtlijn dat de resolutie van het rekenrooster rond de ingreep in langsrichting fijner moet zijn dan de lengte van het schip. 482 | \end{itemize} 483 | 484 | \gensection{\isreport}{1}{Validatie} 485 | \label{validatie} 486 | In dit hoofdstuk worden voorbeelden gegeven waarmee de visualisatie van resultaten wordt toegelicht en de werking van de tool is gevalideerd. Ten eerste wordt de hypothetische natuurvriendelijke oever van \citet{Groenewege25} in de Maas bij rivierkilometer 188 gepresenteerd. Ten tweede is er een eerste karakterisering gemaakt van dwarsstroming en Froude-getallen van de gehele Rijn en Maas. De exacte visualisatie van resultaten door de tool kan mogelijk nog wijzigen in het eindproduct ten opzichte van het hier gepresenteerde tussenproduct. 487 | 488 | \gensection{\isreport}{2}{Natuurvriendelijke oever Maas} 489 | De natuurvriendelijke oever (NVO) betreft een symmetrische erosiekom van 50 m breed en 100 m lang. De resultaten worden ter illustratie voor één afvoersom gepresenteerd (S2100); overige afvoersommen kunnen in \citet{Groenewege25} worden gevonden. 490 | 491 | \gensection{\isreport}{3}{Afvoer van ijs} 492 | In deze sectie worden de effecten van de NVO op de afvoer van ijs getoond. 493 | 494 | \gensection{\isreport}{4}{1D profielen} 495 | 496 | In onderstaand figuur worden de randen van de vaarweg getoond, waarlangs stroomsnelheid en -richting worden gepresenteerd. De bodemligging in de plansituatie fungeert als achtergrond. 497 | 498 | \insertdoublefigure{\NVOMaasFigDir/profile0_location.png}{\NVOMaasFigDir/profile1_location.png}{Locatie van profiel 0 (links) en profiel 1 (rechts). De NVO bevindt zich ter hoogte van rkm 188.} 499 | 500 | Figuur \ref{fig:NVO_stroming_p0} en Figuur \ref{fig:NVO_stroming_p1} tonen stroomsnelheid en -richting langs de profielen (zwarte en blauwe lijnen). De x-as is hier omgekeerd en de rivierkilometers zijn aflopend in positieve richting. Het bereik van de x-as is standaard gelijk aan het bereik van de profiellijn. De y-as van absolute stroomsnelheid magnitude begint standaard bij 0, en de y-as van absolute stroomrichting varieert van -90 tot 90 graden (loodrecht) ten opzichte van de profiellijn\footnote{Er is voor dit bereik gekozen vanwege eb en vloed; in de broncode zit (nog) geen afhankelijkheid van de oriëntatie van de profiellijn (stroomopwaarts of -afwaarts). Om te beoordelen of de stroming omkeert door de ingreep (richtingsverandering van meer dan 90 graden), moeten aanvullend de 2D modelresultaten worden bekeken.}. De situatie zonder ingreep wordt aangeduid met 'Referentie' en de situatie met ingreep met 'Plansituatie'. Het verschil hiertussen wordt in rood rechts op een secundaire y-as weergegeven, die gecentreerd is rond 0. Met deze figuren wordt duidelijk dat snelheidsgradiënten worden versterkt ter plekke van de NVO op rkm 188. Aan de kant van de NVO zien we tevens verandering van stromingsrichting van meer dan 10 graden. 501 | 502 | \begin{figure*}[hbt!] 503 | \centering 504 | \includegraphics[width=\textwidth]{\NVOMaasFigDir/C2_profile0_velocity_angle.png} 505 | \captionsetup{justification=centering} 506 | \caption{Stroomsnelheid en -richting langs profiel 0 (S2100)} 507 | \label{fig:NVO_stroming_p0} 508 | \end{figure*} 509 | 510 | \begin{figure*}[hbt!] 511 | \centering 512 | \includegraphics[width=\textwidth]{\NVOMaasFigDir/C2_profile1_velocity_angle.png} 513 | \captionsetup{justification=centering} 514 | \caption{Stroomsnelheid en -richting langs profiel 1 (S2100)} 515 | \label{fig:NVO_stroming_p1} 516 | \end{figure*} 517 | 518 | \FloatBarrier 519 | \gensection{\isreport}{4}{Froude getallen} 520 | Figuur \ref{fig:NVO_Froude} laat de Froude getallen zien rondom de NVO, zonder correcties voor waterstandsopstuwing door een ijsdek of voor lokale bodemveranderingen. Het verschil tussen de plansituatie en de referentie wordt getoond in Figuur \ref{fig:NVO_Froude_diff1}. Binnen de erosiekom zien we een toename van Froude getallen van < 0.08 naar >= 0.08. Hier direct benedenstrooms van worden Froude getallen verlaagd van > 0.08 naar <= 0.08. Het toevoegen van de correctie voor opstuwing als gevolg van de aanwezigheid van een ijsdek (Figuur \ref{fig:NVO_Froude_diff2}) verkleint de verschillen met 30\% (zie Appendix \ref{app:ijscorrecties}). De lokale bodemverandering is eerder door \citet{Groenewege25} met D-FAST-MI bepaald. Het toevoegen van de correctie voor deze bodemverandering (Figuur \ref{fig:NVO_Froude_diff3}) levert in dit geval relatief weinig verschil op. Volgens de beoordeling in het RBK heeft deze NVO een onacceptabele impact op de afvoer van ijs, omdat Froude-getallen worden verlaagd tot onder 0.08 in het stroomvoerend profiel. Vanuit deze figuren is het niet meteen duidelijk wat het stroomvoerend profiel is, dus het wordt aanbevolen om in een latere versie van de rekentool ook uitvoer van 2D figuren van stroomsnelheid en -richting mogelijk te maken. 521 | 522 | \begin{figure*}[!h] 523 | \centering 524 | \includegraphics[width=0.5\textwidth]{\NVOMaasFigDir/C2_intervention_Froude.png} 525 | \captionsetup{justification=centering} 526 | \caption{Froude getallen in plansituatie, zonder correcties (S2100)} 527 | \label{fig:NVO_Froude} 528 | \end{figure*} 529 | 530 | \begin{figure*}[hbt!] 531 | \centering 532 | \includegraphics[width=0.5\textwidth]{\NVOMaasFigDir/C2_difference_Froude.png} 533 | \captionsetup{justification=centering} 534 | \caption{Verschil in Froude getallen tussen plansituatie en referentie, zonder correcties (S2100)} 535 | \label{fig:NVO_Froude_diff1} 536 | \end{figure*} 537 | 538 | \begin{figure*}[hbt!] 539 | \centering 540 | \includegraphics[width=0.5\textwidth]{\NVOMaasFigDir/C2_difference_Froude_wateruplift.png} 541 | \captionsetup{justification=centering} 542 | \caption{Verschil in Froude getallen tussen plansituatie en referentie, met correctie voor wateropstuwing door benedenstrooms ijsdek (S2100)} 543 | \label{fig:NVO_Froude_diff2} 544 | \end{figure*} 545 | 546 | \begin{figure*}[hbt!] 547 | \centering 548 | \includegraphics[width=0.5\textwidth]{\NVOMaasFigDir/C2_difference_Froude_wateruplift_bedchange.png} 549 | \captionsetup{justification=centering} 550 | \caption{Verschil in Froude getallen tussen plansituatie en referentie, met alle correcties (S2100)} 551 | \label{fig:NVO_Froude_diff3} 552 | \end{figure*} 553 | 554 | \FloatBarrier 555 | \gensection{\isreport}{3}{Dwarsstroming} 556 | In deze sectie worden de effecten van de NVO op de dwarsstroming getoond. In Figuur \ref{fig:NVO_dwarsstroming_p0} en Figuur \ref{fig:NVO_dwarsstroming_p1} wordt de dwarsstroming op de profielen getoond. De trajecten waar het criterium geldt (zie \ref{sec:beschrijving_dwarsstroming}) zijn grijs gearceerd en begrensd door grijze, verticale lijnen. De criteria die daarbij horen, op basis van het berekende dwarsstroomdebiet (oppervlakte van het grijs gearceerde gebied), zijn met rode lijnen weergegeven. In dit geval geldt nergens het strengere criterium van dwarsstroomsnelheid < 0.15 $m/s$, omdat nergens het dwarsstroomdebiet groter dan 50 $m^3/s$ is. Over het algemeen is de impact van de NVO erg klein. Tussen rkm 188.1 en 188.4 is in Figuur \ref{fig:NVO_dwarsstroming_p0} te zien dat in de referentiesituatie de dwarsstroomsnelheid het criterium van 0.3 $m/s$ al overschrijdt. Eén van de gegenereerde Excel bestanden is in Figuur \ref{xlsx:NVO_dwarsstroming_p0} getoond, waarin op te merken is dat de NVO de overschrijding zelfs doet afnemen. Volgens de beoordeling in het RBK is er dus geen sprake van een onacceptabele toename van de dwarsstroming. 557 | 558 | \begin{figure*}[hbt!] 559 | \centering 560 | \includegraphics[width=\textwidth]{\NVOMaasFigDir/C2_profile0_transverse_discharge.png} 561 | \captionsetup{justification=centering} 562 | \caption{Dwarsstroming langs profiel 0 (S2100)} 563 | \label{fig:NVO_dwarsstroming_p0} 564 | \end{figure*} 565 | 566 | \begin{figure*}[hbt!] 567 | \centering 568 | \includegraphics[width=\textwidth]{\NVOMaasFigDir/C2_profile1_transverse_discharge.png} 569 | \captionsetup{justification=centering} 570 | \caption{Dwarsstroming langs profiel 1 (S2100)} 571 | \label{fig:NVO_dwarsstroming_p1} 572 | \end{figure*} 573 | 574 | \begin{figure*}[hbt!] 575 | \centering 576 | \includegraphics[width=\textwidth]{\NVOMaasFigDir/C2_profile0_transverse_discharge_xlsx.png} 577 | \captionsetup{justification=centering} 578 | \caption{Dwarsstroming langs profiel 0, Excel output (S2100). Links: referentie, rechts: plansituatie} 579 | \label{xlsx:NVO_dwarsstroming_p0} 580 | \end{figure*} 581 | 582 | \FloatBarrier 583 | \gensection{\isreport}{1}{Karakterisering van de Rijntakken} 584 | \label{Rijn} 585 | In dit hoofdstuk wordt een eerste karakterisering gemaakt van stroming in de gehele Rijn met betrekking tot de afvoer van ijs en dwarsstroming. Dit is zowel gedaan om de tool te testen als om inzicht te krijgen in het huidige functioneren van de rivier. 586 | 587 | \gensection{\isreport}{2}{Opzet} 588 | Voor de berekeningen is gebruik gemaakt van het \texttt{dflowfm2d-rijn-j24\_6-v1a} model. De stationaire afvoersommen S4000, S6000 en S8000 zijn doorgerekend om het bereik "Lobith van 4000 tot 8000 $m^3/s$" (zie sectie \ref{achtergrond_ijs}) te representeren. De randen van de vaarwegen zijn dezelfde als in \citep{Groenewege25}: de normaallijnen zoals aangeleverd door RWS-ON. Deze lijnen lopen benedenstrooms door tot rkm 955 (Gorinchem) op de Waal, rkm 970 (Schoonhoven) op de Lek, en rkm 1006 (Ketelhaven) op de IJssel. 589 | 590 | \gensection{\isreport}{2}{Resultaten} 591 | 592 | \gensection{\isreport}{3}{Afvoer van ijs} 593 | In deze sectie worden enkele opmerkelijke knelpunten met betrekking tot de afvoer van ijs gepresenteerd. De analyse hiervoor is gedaan aan de hand van de figuren die de rekentool kan produceren, door te kijken waar binnen de normaallijnen Froude getallen onder 0.08 liggen (gecorrigeerd voor een benedenstrooms ijsdek). Dit is ter versimpeling enkel gedaan voor een afvoer van 4000 $m^3/s$, omdat hier - binnen het gegeven bereik van het RBK - de grootste kans bestaat op verandering in Froude getallen van > 0.08 naar < 0.08. De focus ligt hier op abrupte verlagingen van Froude getallen; relatief lage Froude getallen nabij kribben en als onderdeel van geleidelijke transities worden niet als knelpunten beschouwd. 594 | 595 | In onderstaande kaarten zijn de normaallijnen aangegeven met groene lijnen. Ook wordt de stroming langs profiellijnen getoond om de werking van de tool verder te illustreren. 596 | 597 | \gensection{\isreport}{4}{Boven-Rijn en Waal} 598 | Over het algemeen worden de Boven-Rijn en Waal gedomineerd door Froude getallen tussen 0.10 en 0.15 (bij een afvoer van 4000 $m^3/s$ en gecorrigeerd voor een benedenstrooms ijsdek). Desalniettemin bestaan er enkele knelpunten, die in onderstaande figuren zijn uitgelicht. 599 | 600 | \insertfrfigure{\RijnFigDir//BovenrijnWaal/figures/C1_reference_profile2_Froude_wateruplift.png}{\RijnFigDir//BovenrijnWaal/figures/C1_profile2_velocity_angle.png}{Knelpunt in de buitenbocht, rkm 882.5-883.} 601 | \insertfrfigure{\RijnFigDir//BovenrijnWaal/figures/C1_reference_profile6_Froude_wateruplift.png}{\RijnFigDir//BovenrijnWaal/figures/C1_profile6_velocity_angle.png}{Knelpunt in de binnenbocht, rkm 928-929.} 602 | \insertfrfigure{\RijnFigDir//BovenrijnWaal/figures/C1_reference_profile7_Froude_wateruplift.png}{\RijnFigDir//BovenrijnWaal/figures/C1_profile7_velocity_angle.png}{Knelpunt in de buitenbocht, rkm 953-955.} 603 | 604 | \FloatBarrier 605 | \gensection{\isreport}{4}{Nederrijn-Lek en Pannerdens Kanaal} 606 | Over het algemeen is het Pannerdens Kanaal gedomineerd door Froude getallen tussen 0.10 en 0.15 en vindt er een geleidelijke vermindering van Froude getallen plaats in stroomafwaartse richting op de Nederrijn-Lek. De Nederrijn en de Lek tot rkm 940 zijn gedomineerd door Froude getallen tussen 0.10 en 0.15. Vervolgens is het traject van rkm 940 tot rkm 950 gedomineerd door lagere Froude getallen tussen 0.08 en 0.10. Als laatst is de Lek vanaf rkm 950 gedomineerd door Froude getallen onder 0.08. Anders dan de andere Rijntakken komen hier geen opmerkelijke knelpunten naar voren. De kans op een ijsblokkade neemt in stroomafwaartse richting geleidelijk toe. 607 | 608 | Ter illustratie zijn hieronder de Froude getallen in figuren gepresenteerd. 609 | 610 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile6_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile6_velocity_angle.png}{Froude getallen op het Pannerdens Kanaal.} 611 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile0_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile0_velocity_angle.png}{Froude getallen op de Nederrijn, rkm 878-893.} 612 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile1_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile1_velocity_angle.png}{Froude getallen op de Nederrijn, rkm 894-909.} 613 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile2_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile2_velocity_angle.png}{Froude getallen op de Nederrijn, rkm 909-924.} 614 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile3_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile3_velocity_angle.png}{Froude getallen op de Nederrijn en Lek, rkm 924-939.} 615 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile4_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile4_velocity_angle.png}{Froude getallen op de Lek, rkm 939-955.} 616 | \insertfrfigure{\RijnFigDir//NederrijnLek/figures/C1_reference_profile5_Froude_wateruplift.png}{\RijnFigDir//NederrijnLek/figures/C1_profile5_velocity_angle.png}{Froude getallen op de Lek, rkm 955-970.} 617 | 618 | \FloatBarrier 619 | \gensection{\isreport}{4}{IJssel} 620 | Over het algemeen zijn Froude getallen in de IJssel niet hoog (bij een afvoer van 4000 $m^3/s$ en gecorrigeerd voor een benedenstrooms ijsdek). De Boven-IJssel tot ongeveer rkm 900 is gedomineerd door Froude getallen tussen 0,10 en 0,15. Van rkm 900 tot rkm 977 is het vooral gedomineerd door lagere Froude getallen tussen 0,08 en 0,10. Het traject tussen rkm 977 en rkm 994 is weer gedomineerd door hogere Froude getallen, tussen 0,10 en 0,15. Tussen rkm 994 en de monding van de IJssel bestaat de hoogste kans op een ijsblokkade; dit hele traject is gekenmerkt door Froude getallen onder 0,08. In onderstaande figuren zijn enkele knelpunten ter illustratie uitgelicht. 621 | 622 | \insertfrfigure{\RijnFigDir//IJssel/figures/C1_reference_profile1_Froude_wateruplift.png}{\RijnFigDir//IJssel/figures/C1_profile0_velocity_angle.png}{Knelpunt in de buitenbochten, rkm 891-899.75.} 623 | \insertfrfigure{\RijnFigDir//IJssel/figures/C1_reference_profile6_Froude_wateruplift.png}{\RijnFigDir//IJssel/figures/C1_profile2_velocity_angle.png}{Knelpunt in de buitenbocht, rkm 919.} 624 | \insertfrfigure{\RijnFigDir//IJssel/figures/C1_reference_profile8_Froude_wateruplift.png}{\RijnFigDir//IJssel/figures/C1_profile3_velocity_angle.png}{Knelpunt in de buitenbochten, rkm 937-938.5.} 625 | \insertfrfigure{\RijnFigDir//IJssel/figures/C1_reference_profile9_Froude_wateruplift.png}{\RijnFigDir//IJssel/figures/C1_profile7_velocity_angle.png}{Knelpunt in het traject rkm 942-943.5.} 626 | \insertfrfigure{\RijnFigDir//IJssel/figures/C1_reference_profile27_Froude_wateruplift.png}{\RijnFigDir//IJssel/figures/C1_profile11_velocity_angle.png}{Knelpunt in het traject rkm 994-1006.} 627 | 628 | \FloatBarrier 629 | \gensection{\isreport}{3}{Dwarsstroming} 630 | In deze sectie wordt gefocust op overschrijding van de gestelde normen. Er is gekeken naar alle overschrijdingen in totaal. Daarbij is er niet bepaald of het kan gaan om locaties waar de dwarsstroming op zowel de linker- als rechteroever te hoog is. 631 | 632 | \FloatBarrier 633 | \gensection{\isreport}{4}{Boven-Rijn en Waal} 634 | 635 | Op de Boven-Rijn en Waal wordt de dwarsstromingsnorm op honderden locaties overschreden, verdeeld over een afstand van ongeveer 100 km. Voor S4000 zijn er in totaal 106 overschrijdingen, voor S6000 zijn het er 120, en voor S8000 zijn het er 145. Een overzicht van de grootste overschrijdingen (> 2 keer het criterium) is gegeven in Tabel \ref{tab:BovenrijnWaal_dwarsstroming}. Het grootste dwarsstroomdebiet is 430 $m^3/s$ voor S4000 (rkm 918,277 - 918,554), 388 $m^3/s$ voor S6000 (rkm 907,9 - 908,167) en 461 $m^3/s$ voor S8000 (rkm 907,904 - 908,172). Op deze laatste locatie ligt de samenkomst van de Kaliwaal met de Waal (zie profiel 10). 636 | 637 | \input{\RijnFigDir//BovenrijnWaal/output/analysis.tex} 638 | 639 | Ter illustratie worden deze overschrijdingen hieronder voor een afvoer van 8000 $m^3/s$ weergegeven. 640 | 641 | \input{\RijnFigDir//BovenrijnWaal/output/figures_S8000.tex} 642 | 643 | \FloatBarrier 644 | \gensection{\isreport}{4}{Nederrijn-Lek en Pannerdens Kanaal} 645 | Op de Nederrijn-Lek en het Pannerdens Kanaal wordt de dwarsstromingsnorm op tientallen locaties overschreden, verdeeld over een afstand van ongeveer 100 km. Voor S4000 zijn er 13 overschrijdingen, voor S6000 zijn het er 60, en voor S8000 zijn het er 85. Een overzicht van de grootste overschrijdingen (> 2 keer het criterium) is gegeven in Tabel \ref{tab:NederrijnLek_dwarsstroming}. Het grootste dwarsstroomdebiet is 91 $m^3/s$ voor S4000 (rkm 922.203-922.31), 218 $m^3/s$ voor S6000 (rkm 899.77-899.955) en 210 $m^3/s$ voor S8000 (rkm 924.671-924.864). Op deze laatste locatie ligt het uiterwaardengebied de Waarden van Gravenbol (zie profiel 11). 646 | 647 | \input{\RijnFigDir//NederrijnLek/output/analysis.tex} 648 | 649 | Ter illustratie worden deze overschrijdingen hieronder voor een afvoer van 8000 $m^3/s$ weergegeven. 650 | 651 | \input{\RijnFigDir//NederrijnLek/output/figures_S8000.tex} 652 | 653 | \FloatBarrier 654 | \gensection{\isreport}{4}{IJssel} 655 | 656 | Op de IJssel wordt de dwarsstromingsnorm op tientallen locaties overschreden, verdeeld over een afstand van ongeveer 128 km. Voor 4000 zijn er in totaal 7 overschrijdingen, voor S6000 zijn het er 37, en voor S8000 zijn het er 76. Een overzicht van de grootste overschrijdingen (> 2 keer het criterium) is gegeven in Tabel \ref{tab:IJssel_dwarsstroming}. Het grootste dwarsstroomdebiet is 146 $m^3/s$ voor S4000 (rkm 1001.7-1001.822), 217 $m^3/s$ voor S6000 (rkm 1001.7-1001.822) en 274 $m^3/s$ voor S8000 (rkm 1001.7-1001.822). Op deze laatste locatie ligt de vertakking van de IJssel in het Kattendiep en Keteldiep (zie profiel 11). 657 | 658 | \input{\RijnFigDir//IJssel/output/analysis.tex} 659 | 660 | Ter illustratie worden deze overschrijdingen hieronder voor een afvoer van 8000 $m^3/s$ weergegeven. 661 | 662 | \input{\RijnFigDir//IJssel/output/figures_S8000.tex} 663 | 664 | \gensection{\isreport}{1}{Karakterisering van de Maas} 665 | \label{Maas} 666 | In dit hoofdstuk wordt een eerste karakterisering gemaakt van stroming in de gehele Maas met betrekking tot de afvoer van ijs en dwarsstroming. Dit is zowel gedaan om de tool te testen als om inzicht te krijgen in het huidige functioneren van de rivier. 667 | 668 | \gensection{\isreport}{2}{Opzet} 669 | Voor de berekeningen is gebruik gemaakt van het \texttt{dflowfm2d-maas-beno22\_6-v2a} model. De afvoersommen S1700, S2100 en S2500 zijn doorgerekend om het bereik "Borgharen van 1500 tot 2800 $m^3/s$" (zie sectie \ref{achtergrond_ijs}) te representeren. De Grensmaas, het Julianakanaal, Lateraalkanaal, Maas-Waalkanaal, andere aangetakte kanalen, havens, bruggen en sluizen zijn buiten beschouwing gelaten in de analyse. 670 | 671 | De randen van de vaarwegen zijn verkregen uit een nabewerking van het normprofiel dat door RWS-ZN is aangeleverd (bestand \texttt{normprofiel.gdb\textbar layername=normprofiel\_breeklijn}) \citep{Groenewege25}. Dit normprofiel loopt ongeveer tot Well benedenstrooms door. Om het aantal profielen te minimaliseren, zijn eerst aparte segmenten zoveel mogelijk met elkaar verbonden. In een eerste test kwamen honderden overschrijdingen van de dwarsstromingsnorm naar voren, wat deels te wijten was aan de grilligheid van het normprofiel. Daarom is het normprofiel tevens versimpeld (Douglas-Peucker algoritme met een tolerantie van 2 m), waarmee de grilligheid sterk verminderd is. Het resulterende normprofiel heeft dus geen officiële status, maar is wel geoptimaliseerd om stroming langs de profiellijnen zo goed mogelijk te representeren. Desalniettemin blijven er nog veel locaties over waar het normprofiel de oever te dicht lijkt te volgen. Daarom is ook gekeken naar stroming langs de rivieras. 672 | 673 | \gensection{\isreport}{2}{Resultaten} 674 | 675 | \FloatBarrier 676 | \gensection{\isreport}{3}{Afvoer van ijs} 677 | In deze sectie worden enkele opmerkelijke knelpunten met betrekking tot de afvoer van ijs gepresenteerd. De analyse hiervoor is gedaan aan de hand van de figuren die de rekentool kan produceren, door te kijken waar binnen het (bewerkte) normprofiel Froude getallen onder 0.08 liggen (gecorrigeerd voor een benedenstrooms ijsdek). Dit is ter versimpeling enkel gedaan voor een afvoer van 1700 $m^3/s$, omdat hier - binnen het gegeven bereik van het RBK - de grootste kans bestaat op verandering in Froude getallen van > 0.08 naar < 0.08. De focus ligt hier op abrupte verlagingen van Froude getallen; relatief lage Froude getallen nabij kribben en als onderdeel van geleidelijke transities worden niet als knelpunten beschouwd. In onderstaande kaarten is het normprofiel aangegeven met groene lijnen. Ook wordt de stroming langs profiellijnen getoond om de werking van de tool verder te illustreren. 678 | 679 | De Bovenmaas wordt gekenmerkt door Froude getallen rond 0.15 en daarboven (voor een afvoer van 1700 $m^3/s$ en met een benedenstrooms ijsdek). De Zandmaas wordt tussen rkm 67 en rkm 87 gekenmerkt door Froude getallen tussen 0.10 en 0.15, en tussen rkm 87 en rkm 147 door Froude getallen rond 0.08. Het traject tussen rkm 109.5 en 120.5 is een knelpunt waar Froude getallen veelal onder 0.08 liggen. Ook ter hoogte van havens en (neven)geulen of plassen ligt het Froude getal vaak onder 0.08. Zie onderstaande figuren ter illustratie. Vanaf stuw Sambeek (rkm 147) zijn in stroomafwaartse richting de laagste Froude getallen te vinden: tussen 0.04 en 0.08. 680 | 681 | \insertfrfigure{\MaasFigDir//figures/C1_reference_profile26_Froude_wateruplift.png}{\MaasFigDir//figures/C1_profile26_velocity_angle.png}{Knelpunt voor de afvoer van ijs, rkm 109.5-120.5.} 682 | \insertfrfigure{\MaasFigDir//figures/C1_reference_profile25_Froude_wateruplift.png}{\MaasFigDir///figures/C1_profile25_velocity_angle.png}{Knelpunt voor de afvoer van ijs, rkm 135-138.} 683 | 684 | \FloatBarrier 685 | \gensection{\isreport}{3}{Dwarsstroming} 686 | Op de Maas wordt de dwarsstromingsnorm op honderden locaties langs de randen van de vaarweg overschreden, verdeeld over een afstand van ongeveer 250 km. Voor S1700 zijn er in totaal 66 overschrijdingen, voor S2100 zijn het er 326, en voor S2500 zijn het er 128. Een overzicht van de grootste overschrijdingen (> 4 keer het criterium) is gegeven in Tabel \ref{tab:Maas_dwarsstroming}. Het grootste dwarsstroomdebiet is 240 $m^3/s$ voor S1700 (rkm 70,705 - 70,909), 554 $m^3/s$ voor S2100 (rkm 73,302 - 73,498) en 401 $m^3/s$ voor S2500 (rkm 73,289 - 73,484). Op deze laatste locatie ligt de Maas tussen de Gerelingsplas en de Oolderplas. Onderstaand figuur toont de dwarsstroomsnelheid op deze locatie. 687 | 688 | 689 | \input{\MaasFigDir/output/analysis.tex} 690 | 691 | \insertdoublefigure{\MaasFigDir//figures/C2_profile24_transverse_discharge.png}{\MaasFigDir//figures/profile24_location.png}{Dwarsstroming op de Maas voor S2100 profiel 24} 692 | 693 | %\gensection{\isreport}{2}{Rijn-Maasmonding} 694 | % 695 | %\gensection{\isreport}{3}{Opzet} 696 | %Voor de berekeningen is gebruik gemaakt van het \texttt{dflowfm2d-rmm\_vzm-j17\_6-v2a} model. Er is gerekend met gemiddeld getij, en Rijnafvoer bij Tiel van 4062.8, 5417.3 en 6616.9 m3/s en Maasafvoer bij Lith van 1235, 1742 en 2248 m3/s, om het bereik "Lobith van 4000 tot 8000 m3/s" en "Borgharen van 1500 tot 2800 m3/s" (zie sectie \ref{achtergrond_ijs}) te representeren. Het Fourier bestand is enigszins aangepast om de juiste uitvoer te krijgen, en 1 verouderd keyword ("transportmethod") is in de MDU aangepast. In het RBK staan alleen voor de takken Haringvliet, Hollands Diep, Amer en de Merwedes maatgevende schipafmetingen genoemd; zodoende is de analyse alleen voor deze takken uitgevoerd. De analyse beperkt zich hier nog tot Froude getallen in 2D. 697 | % 698 | %\gensection{\isreport}{3}{Resultaten} 699 | % 700 | %\gensection{\isreport}{4}{Afvoer van ijs} 701 | % 702 | %\gensection{\isreport}{5}{Froude getallen} 703 | 704 | \gensection{\isreport}{1}{Conclusies en aanbevelingen} 705 | \label{conclusies} 706 | Het doel van deze studie was om een eenduidige aanpak met rekentool te ontwikkelen voor de RBK-bepaling van dwarsstroom- en ijsafvoereffecten. Het prototype dat is ontwikkeld is een "Command Line Interface" (CLI) dat in voorliggend rapport is beschreven. Uit de validatie met een hypothetische ingreep (hoofdstuk \ref{validatie}) blijkt de tool geschikt voor het beoogde doel. Uit eerste karakteriseringen van het huidige functioneren van de Rijn (hoofdstuk \ref{Rijn}) en de Maas (hoofdstuk \ref{Maas}) met betrekking tot de afvoer van ijs en dwarsstroming, blijken er tevens tientallen overschrijdingen of knelpunten te bestaan. Wat betreft dwarsstroming zijn van alle Rijntakken de meeste overschrijdingen op de Boven-Rijn en Waal te vinden. Omdat voor de Rijn en Maas verschillende profiellijnen worden gebruikt, is een één-op-één vergelijking van dwars- en langsstroming (en gradiënten hierin) lastig te maken. Wel kan gesteld worden dat op beide rivieren Froude getallen in stroomafwaartse richting afnemen (vanwege vermindering in stroomsnelheid); echter is in deze studie geen rekening gehouden met getij. 707 | 708 | Gezien de huidige beperkingen van de rekentool (zie sectie \ref{beperkingen}), worden de volgende verbeteringen aanbevolen: 709 | \begin{enumerate} 710 | \item De rekentool binnen de D-FAST productlijn als module opnemen, waarbij de code deels herschreven wordt en uitgebreid met D-FAST-MI functionaliteit. Hierbij heeft berekening van maximale stroming tijdens eb en vloed hoge prioriteit. 711 | \item Automatisch bepalen en tonen van het stroomvoerend profiel, zodat data hierbuiten weggefilterd kan worden en ook de beoordeling van het effect van ingrepen op de afvoer van ijs wordt vergemakkelijkt. 712 | \item Testen van de tool met andere schipafmetingen en herziening van de huidige maatgevende waarden, die niet per se representatief zijn voor een "klein schip" \citep{Koedijk20}. 713 | \item Opstellen van richtlijnen voor gebruik van deze tool in de RMM (gefocust op selectie van gemiddeld/springtij, rivierafvoer, moment van getij, profiellijn(en), enz.). 714 | \item Ten behoeve van beoordeling van dwarsstroom- en ijsafvoereffecten van ingrepen in de Maas wordt aangeraden vloeiendere lijnen te gebruiken dan het normprofiel (bestand \texttt{normprofiel.gdb\textbar layername=normprofiel\_breeklijn}, zoals aangeleverd door RWS-ZN voor \citet{Groenewege25}). 715 | \end{enumerate} 716 | 717 | Tevens wordt aangeraden om voor de resterende beoordelingsaspecten uit het RBK \citep{RWS23} ook gestandaardiseerde en uniforme rekentools te ontwikkelen die op elkaar aansluiten, zodat steeds dezelfde aanpakken worden gevolgd voor de beoordeling van rivierkundige effecten van ingrepen. 718 | 719 | % 720 | % 721 | % 722 | %---------------------------- 723 | %---------GLOSSARY 724 | %---------------------------- 725 | % 726 | %\section{List of Mathematical Symbols} 727 | %\printglossary[title=] 728 | 729 | %define 730 | % \newglossaryentry{H}{ 731 | % name={$H$}, 732 | % description={weir height. Distance between the crest and the bed level on the downstream side.}, 733 | % sort={H} 734 | % } 735 | 736 | %use 737 | % \gls{H} 738 | 739 | % 740 | % 741 | % 742 | %---------------------------- 743 | %---------BIBLIOGRAPHY 744 | %---------------------------- 745 | % 746 | \FloatBarrier 747 | \gensection{\isreport}{1}{Referenties} 748 | \DeclareRobustCommand{\van}[3]{#3} 749 | \bibliography{00_references/references} 750 | 751 | 752 | \vfill 753 | \ifnum \addCitation=1 754 | Please cite this work using: \\ 755 | \\ 756 | \texttt{@TechReport\{[Add\_bibtex\_reference],\\ 757 | ~author~~~= \{\myAuthori{} and \myAuthorii{}\},\\ 758 | ~title~~~= \{\myTitle{}: \mySubtitle{}\},\\ 759 | ~number~~~= \{\myVersion{}\},\\ 760 | ~year~~~~= \{\myDate{}\},\\ 761 | ~institution= \{{Deltares, Delft, the Netherlands}\}\\ 762 | \ifnum \isreport=1 763 | ~type~~~~= \{\{T\}ech. \{R\}ep.\},\\ 764 | \else 765 | ~type~~~~= \{\{M\}emo\}, \\ 766 | \fi 767 | ~\} 768 | } 769 | \fi 770 | 771 | % 772 | % 773 | % 774 | %---------------------------- 775 | %---------APPENDIX 776 | %---------------------------- 777 | % 778 | \ifnum \addAppendix=1 779 | 780 | \newpage 781 | \appendix 782 | \renewcommand\thesection{\AlphAlph{\value{section}}} 783 | 784 | \gensection{\isreport}{1}{Correcties van modelresultaten voor de afvoer van ijs} 785 | \label{app:ijscorrecties} 786 | 787 | \textbf{Correctie 1}\\ 788 | De verandering in waterdiepte $h$ [$m$] door opstuwing van een benedenstrooms ijsdek wordt naar \citet{Zagonjolli19} pragmatisch geschat met 789 | \begin{equation} 790 | {\frac{h_{\text{zonder ijsdek}}}{h_{\text{met ijsdek}}}} \approx \left(\frac{C_{\text{met ijsdek}}}{C_{\text{zonder ijsdek}}}\right)^{\frac{2}{3}}\approx \left(\frac{1}{\sqrt{2}}\right)^{\frac{2}{3}}, 791 | \end{equation} 792 | waarbij $C$ de Chézy ruwheidscoëfficiënt is [$m^{\frac{1}{2}}/s$]. 793 | 794 | Het Froude getal $Fr$ is gedefinieerd als 795 | \begin{equation} 796 | Fr = {\frac{u}{\sqrt{gh}}} = {\frac{q}{h\sqrt{gh}}}, 797 | \end{equation} 798 | waarbij $u$ de stroomsnelheid is [$m/s$], $q$ het specifieke debiet [$m^2/s$] en $g$ de gravitatie-constante [$m/s^2$]. Het effect van de opstuwing op $Fr$ wordt dan geschat met 799 | \begin{equation} 800 | {\frac{Fr_{\text{met ijsdek}}}{Fr_{\text{zonder ijsdek}}}} \approx \left({\frac{h_{\text{zonder ijsdek}}}{h_{\text{met ijsdek}}}}\right)^{\frac{3}{2}} 801 | \end{equation} 802 | \begin{equation} 803 | {\frac{Fr_{\text{met ijsdek}}}{Fr_{\text{zonder ijsdek}}}} = {\frac{C_{\text{met ijsdek}}}{C_{\text{zonder ijsdek}}}} 804 | \end{equation} 805 | \begin{equation} \label{eq:5} 806 | Fr_{\text{met ijsdek}} = {\frac{Fr_{\text{zonder ijsdek}}}{\sqrt{2}}} 807 | \end{equation} 808 | De correctie voor opstuwing door een benedenstrooms ijsdek volgt dus eenvoudig uit $0.71Fr$, berekend in D-HYDRO. Hier is verondersteld dat stroombanen in de hoofdgeul onder invloed van het benedenstrooms ijsdek niet verschuiven. Door deze vereenvoudiging kan $Fr$ wat worden overschat omdat bij opstuwing in de hoofdgeul de uiterwaarden wat gemakkelijker meestromen. 809 | 810 | \textbf{Correctie 2}\\ 811 | De invloed van lokale bodemveranderingen op het Froude getal wordt vergelijkbaar geschat met 812 | \begin{equation} \label{eq:6} 813 | \frac{Fr_{z1}}{Fr_{z0}} \approx \left({\frac{h_{z0}}{h_{z1}}}\right)^{\frac{3}{2}} = \left(1-{\frac{\Delta{z}}{h_{z0}}}\right)^{-\frac{3}{2}}, 814 | \end{equation} 815 | waarbij $z$ de bodemligging is. $z1$ geeft bodemverandering aan en $z0$ géén bodemverandering. 816 | 817 | \textbf{Gecombineerde correctie}\\ 818 | De uiteindelijke, gecombineerde correctie van het met D-HYDRO berekende Froude getal is dan (\ref{eq:5}, \ref{eq:6}) 819 | \begin{equation} 820 | \frac{Fr_{gecorrigeerd}}{Fr_{berekend}} = \frac{1}{\sqrt{2}}\left(1-{\frac{\Delta{z}}{h_{z0}}}\right)^{-\frac{3}{2}}. 821 | \end{equation} 822 | 823 | \gensection{\isreport}{1}{Gebruiksinstructies} 824 | \label{app:gebruiksinstructies} 825 | 826 | \gensection{\isreport}{2}{Installatie} 827 | Om de rekentool te installeren moeten onderstaande stappen gevolgd worden: 828 | \begin{enumerate} 829 | \item Download de bijgesloten broncode (ZIP-bestand). 830 | \item Installeer de meest recente versie van Python (https://www.python.org/downloads/). 831 | \item Installeer Miniforge (https://github.com/conda-forge/miniforge). 832 | \item Navigeer naar de \texttt{BuildScripts} folder en voer \texttt{DevelopDfastmi.bat} uit. 833 | \item Nadat dit commando correct is uitgevoerd, zou u een map met de naam “venv.” moeten hebben. Dit is uw virtuele omgeving. Om deze omgeving te activeren, voert u het volgende commando in Miniforge Prompt uit: \texttt{conda activate py\_3\_10-dfastmi}. 834 | \end{enumerate} 835 | 836 | \gensection{\isreport}{2}{Executie} 837 | De broncode van de rekentool bevindt zich in de \texttt{dfasttf} folder. De analyse kan uitgevoerd worden middels het commando \texttt{python -m src}. Hierbij moeten twee additionele argumenten worden opgegeven: 838 | \begin{itemize} 839 | \item \texttt{-{}-config}: pad van het configuratiebestand voor de analyse. 840 | \item \texttt{-{}-ships}: pad van het bestand met schipafmetingen per riviertak. Voor \texttt{PlotType = 1D} moet in dit bestand een sectie aanwezig zijn met dezelfde naam als \texttt{Reach} in het configuratiebestand. 841 | \end{itemize} 842 | 843 | Het configuratiebestand heeft dezelfde structuur als van D-FAST-MI \citep{dfastmi_usermanual}, maar met de volgende toevoegingen: 844 | \begin{itemize} 845 | \item \texttt{PlotType}: \texttt{1D} of \texttt{2D} 846 | \item \texttt{InvertXAxis}: boolean die aangeeft of de x-as in 1D plots omgekeerd moet worden of niet. 847 | \item \texttt{ProfileLines}: pad naar bestand met profiellijnen. 848 | \item \texttt{WaterUpliftCorrection}: boolean die aangeeft of de correctie van Froude getallen voor een benedenstrooms ijsdek moet worden toegepast. 849 | \item \texttt{BedChangeCorrection}: boolean die aangeeft of de correctie van Froude getallen voor morfologische impact van een ingreep moet worden toegepast. 850 | \item \texttt{BedChangeFile}: pad naar netCDF-bestand van morfologische verandering, wordt enkel gelezen als \texttt{BedChangeCorrection = True}\footnote{Dit is nog enkel getest met een D-FAST-MI uitvoerbestand.}. 851 | \item \texttt{[BoundingBox]}: sectie voor kader dat de D-HYDRO uitvoer begrenst, gedefinieerd door coördinaten (keywords) \texttt{xmin, xmax, ymin, ymax}. 852 | \end{itemize} 853 | 854 | 855 | 856 | \fi 857 | 858 | \LastPage 859 | \end{document} 860 | --------------------------------------------------------------------------------