Background
To generate accurate forecasts, it's crucial to represent the current system state as accurately as possible. In operational systems, the system state is often prepared by running a hindcast simulation using past data and generating a state file at the end. This “warm state” is then used as the initial condition for the forecast simulation.
However, when the quality and availability of the model input data are insufficient, it is possible that the uncertainty propagates into the forecast. In this cases, it is beneficial to correct the state with data assimilation techniques, or may be more effective to use direct real-time water level observations as the initial condition for your forecast simulation.
In this How-to, we demonstrate how to setup a 1D D-HYDRO(Delft3DFM) model to run with observed water level as initial conditions, using a python script.
When is this approach suitable?
This method is particularly helpful when:
The model domain is relatively flat and controlled (e.g., canal systems).
There are sufficient water level observations.
Water level gradients are minimal, so velocity initialization can be approximated or ignored.
⚠️ Caution: If the system is highly dynamic (e.g., during or right after a flood event), using only observed water levels may cause instabilities at the model start and increase the required warm-up time.
Step-by-Step Guide
1. Configure the D-HYDRO (Delft3DFM) model to use initial water level file
Set the model to read an initial water level file (InitialWaterLevel.ini) in the Initial Fields file (initialFields.ini). Example:
[General]
fileVersion = 2.00
fileType = iniField[Initial]
quantity = waterlevel
dataFile = InitialWaterLevel.ini
dataFileType = 1dField- Setup the InitialWaterLevel.ini file by associating the water level gauges to the branch and chainage in the model. Example:
[General]
fileVersion = 2.00
fileType = 1dField[Global]
quantity = WaterLevel
unit = m
value = 3.000[Branch]
branchId = Canal 01
values = $Canal_01_upstream$
[Branch]
branchId = Canal 02
numLocations = 2
chainage = 0.000 75.000
values = $Canal_02_upstream$ $Canal_02_downstream$
2. In Delft-FEWS, export the observed water level data as a netcdf file
Export a NetCDF file containing recent observations of the water level gauges that are needed by the model.
3. Replace placeholders using a Python script
Since FEWS currently doesn’t support direct value replacement in NetCDF initial condition files, you can use a python script to:
Extract the latest observed water levels.
Replace the corresponding values in the initialWatrerlevel.ini file.
- Example:Generate_initialcondition.py
# Configure logging log_file = 'Generate_initialcondition.log' pi_output_file = "Generate_initialcondition.xml" logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(log_file), logging.StreamHandler() ]) def convert_to_pi_diagnostics(log_file, pi_output_file): if not os.path.exists(log_file): logging.error(f"Log file not found: {log_file}") return namespaces = { '': 'http://www.wldelft.nl/fews/PI', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance' } root = ET.Element("Diag", version="1.2", xmlns="http://www.wldelft.nl/fews/PI", xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance", xsi_schemaLocation="http://www.wldelft.nl/fews/PI https://fewsdocs.deltares.nl/schemas/version1.0/pi-schemas/pi_diag.xsd") with open(log_file, "r") as file: for line in file: match = re.match(r"(?P<timestamp>.*?) - (?P<level>\w+) - (?P<message>.*)", line) if match: timestamp = match.group("timestamp") level = match.group("level") message = match.group("message") level_mapping = { "ERROR": 0, "WARNING": 2, "INFO": 3, "DEBUG": 4 } level_num = level_mapping.get(level, 4) event_code = f"EventCode.{level.upper()}" ET.SubElement(root, "line", level=str(level_num), description=message, eventCode=event_code) tree = ET.ElementTree(root) pi_output_file = Path(pi_output_file).with_suffix('.xml') # Ensure proper extension tree.write(pi_output_file, encoding="utf-8", xml_declaration=True) logging.info(f"Pi Diagnostics file created: {pi_output_file}") def fill_ini_with_nc_data(nc_file_path, ini_file_path, output_file_path="output.ini"): # Step 1: Read netCDF file ds = nc.Dataset(nc_file_path) # Extract station IDs and waterlevel data station_ids = ds.variables['station_id'][:] waterlevels = ds.variables['waterlevel'][:] # Decode bytes if needed if station_ids.dtype.kind in {'S', 'O'}: station_ids = [b''.join(station_ids_i).decode('utf-8').strip('\x00') for station_ids_i in station_ids] # Decode masked array waterlevels = np.ma.filled(waterlevels, fill_value=np.nan) # Take the last time step last_values = waterlevels[-1, :] # Build dictionary station_value_dict = dict(zip(station_ids, last_values)) logging.debug("NC file parsed. ") # Step 2: Read the ini file with open(ini_file_path, 'r', encoding='utf-8') as f: ini_text = f.read() logging.debug("INI file parsed. ") # Step 3: Replace placeholders def replacement(match): key = match.group(1) value = station_value_dict.get(key) if value is None or np.ma.is_masked(value) or np.isnan(value): logging.warning(f"Warning: No value found for {key}") return match.group(0) # Keep original placeholder if missing or invalid return f"{value:.3f}" filled_ini_text = re.sub(r'\$(.*?)\$', replacement, ini_text) logging.debug("INI file filled. ") # Step 4: Save to output file with open(output_file_path, 'w', encoding='utf-8') as f: f.write(filled_ini_text) logging.info(f"Filled INI file written to {output_file_path}") if __name__ == "__main__": parser = argparse.ArgumentParser(description='Generate initial condition required by Delft3DFM based on water level in the netcdf file.') parser.add_argument('nc_file_path', type=Path, help='Path to the waterlevel.nc. Must contain station_id that will be used by InitialWaterLevel.ini. ') parser.add_argument('ini_file_path', type=Path, help='Path to the InitialWaterLevel.ini. Must contain station_id in the netcdf file as property keywords in between ##.') args = parser.parse_args() nc_file_path = args.nc_file_path ini_file_path = args.ini_file_path output_file_path = ini_file_path try: fill_ini_with_nc_data(nc_file_path, ini_file_path, output_file_path) except Exception as e: logging.error("An error occurred: %s", e) convert_to_pi_diagnostics(log_file, pi_output_file)
4. Integrate in the general adaptor
Add the Python script to the executeActivities in the General Adapter configuration.
Example:
<executeActivity> <command> <executable>%REGION_HOME%/Modules/Python/Generate_initialcondition.exe</executable> </command> <arguments> <argument>%ROOT_DIR%/Input/waterlevel.nc</argument> <argument>%WORK_DIR%/dflowfm/InitialWaterLevel.ini</argument> </arguments> <timeOut>300000</timeOut> <overrulingDiagnosticFile>Generate_initialcondition.xml</overrulingDiagnosticFile> </executeActivity>