Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
785d889
Merge pull request #225 from ec-jrc/development
doc78 Jul 1, 2026
025cff0
Update in setup test instruction
doc78 Jul 1, 2026
c7f0416
Added zenodo badge. Small update to reference xml
doc78 Jul 2, 2026
657726f
Updated maintainer list in docker file
doc78 Jul 2, 2026
9936a0f
introducing packing with dummy values
fuchsiger Jul 8, 2026
a0fc652
Def Options Change
fuchsiger Jul 13, 2026
c34f0bd
Def Options Change
fuchsiger Jul 13, 2026
d2d6066
Def Options Change
fuchsiger Jul 13, 2026
757061b
Def Options Change
fuchsiger Jul 13, 2026
081ec5e
Def Options Change
fuchsiger Jul 13, 2026
33e8d55
Updates to default options and fill values
fuchsiger Jul 14, 2026
964a24a
Updates to default options and fill values
fuchsiger Jul 14, 2026
12e4b1b
Update fill value
fuchsiger Jul 17, 2026
9d71096
Update fill values
fuchsiger Jul 17, 2026
ac75a1a
adding Aggregation Functions
fuchsiger Jul 27, 2026
4f0751d
Merge branch 'feature/offset_scale_outputs' of https://github.com/ec-…
fuchsiger Jul 27, 2026
f88b4ce
Update settings.xml
fuchsiger Jul 28, 2026
8d9566d
Update settings.xml
fuchsiger Jul 28, 2026
5882f24
Update
fuchsiger Jul 28, 2026
9f916c4
Debug Statement
fuchsiger Jul 28, 2026
9dc1da9
Debug Statement
fuchsiger Jul 28, 2026
3580718
Debug Statement
fuchsiger Jul 28, 2026
0af2e47
Debug Statement
fuchsiger Jul 28, 2026
6a608ee
Debug Statement
fuchsiger Jul 28, 2026
b8d835b
Debug Statement
fuchsiger Jul 28, 2026
4969862
Debug Statement
fuchsiger Jul 28, 2026
ac359c3
Update Debug
fuchsiger Jul 28, 2026
f0e6971
Update Debug
fuchsiger Jul 28, 2026
3098d63
Remove discharge output packing due to precision
fuchsiger Jul 29, 2026
f31f67f
Fix non nan masking outside the basin mask when aggregating outputs
fuchsiger Jul 29, 2026
c22201e
Implement Carlos Comments
fuchsiger Aug 4, 2026
cd2c9f2
Rounding some scale factors
fuchsiger Aug 4, 2026
cf98283
Added test for scale,offset and aggregation. Changed some scale value…
doc78 Aug 4, 2026
434d421
Modified Ta Maps scale and offset to fit the correct range. Rounded u…
doc78 Aug 5, 2026
82e2e7c
Removed temporary tracking files
doc78 Aug 5, 2026
2cbbbe9
transpiration scale offset update
fuchsiger Aug 5, 2026
20ed76d
Update ta ranges
fuchsiger Aug 6, 2026
cc5e212
Update neg. range tact
fuchsiger Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.gem
*.sublime-project
*.sublime-workspace
.kiro
.bundle
.DS_Store
.jekyll-metadata
Expand All @@ -27,3 +28,6 @@ lisflood_model.egg-info
.vscode/
*.ipynb
.ipynb_checkpoints/
implementation_guide_scale_offset_packing.md
lisflood_optimization_report.md
lisflood_optimization_report_v2.md
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# docker push jrce1/lisflood

FROM continuumio/miniconda3
LABEL maintainer="Stefania Grimaldi, Cinzia Mazzetti, Carlo Russo, Valerio Lorini, Ad de Roo"
LABEL maintainer="Stefania Grimaldi, Timo Schaffhauser, Carlo Russo, Cinzia Mazzetti, Corentin Carton De Wiart"

ENV DEBIAN_FRONTEND=noninteractive

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21107672-blue.svg)](https://doi.org/10.5281/zenodo.21107672)

# Lisflood OS

This repository hosts source code of LISFLOOD model.
Expand Down
2 changes: 1 addition & 1 deletion docs/5_annex_output-files/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ To speed up the pre-run and to prevent that results are taken from the pre-run,
| actual transpiration | repTaMaps | $\frac{mm}{timestep}$ | TaMaps | tact |
| rainfall interception | repInterceptionMaps | $\frac{mm}{timestep}$ | InterceptionMaps <br> InterceptionForestMaps | int <br> intF |
| evaporation of intercepted water | repEWIntMaps | $\frac{mm}{timestep}$ | EWIntMaps | ewint |
| leaf drainage | repLeafDrainageMaps | $\frac{mm}{timestep}$ | LeafDrainageMaps <br> LeafDrainageForestMaps | ldra <br> draF |
| leaf drainage | repLeafDrainageMaps | $\frac{mm}{timestep}$ | LeafDrainageMaps <br> LeafDrainageForestMaps no | ldra <br> draF |
| infiltration | repInfiltrationMaps | $\frac{mm}{timestep}$ | InfiltrationMaps <br> InfiltrationForestMaps | inf <br> infF |
| preferential (bypass) flow | repPrefFlowMaps | $\frac{mm}{timestep}$ | PrefFlowMaps <br> PrefFlowtherMaps <br> PrefFlowForestMaps <br> PrefFlowIrrigationMaps | pflowpixel <br> pflow <br> pflowF <br> pflowi |
| percolation upper to lower soil layer | repPercolationMaps | $\frac{mm}{timestep}$ | Percolation1ato1bOtherMaps <br> Percolation1to1bForestMaps <br> Percolation1ato1bIrrigationMaps <br> Percolation1bto2OtherMaps <br> Percolation1bto2ForestMaps <br> Percolation1bto2IrrigationMaps | Percolation1ato1bOther <br> Percolation1ato1bForest <br> Percolation1to2Irrigation <br> Percolation1bto2Other <br> Percolation1bto2Forest <br> Percolation1bto2Irrigation |
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
IMPORTANT Note:
To test pip installation:
python setup.py testpypi
pip install --index-url https://test.pypi.org/simple/ lisflood-model==5.0.0
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple lisflood-model==5.0.0

To publish on PyPi:

Expand Down
183 changes: 98 additions & 85 deletions src/lisflood/global_modules/default_options.py

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions src/lisflood/global_modules/netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def write_netcdf_header(settings,
start_date,
rep_steps,
frequency,
):
map_value=None):

""" Writes a netcdf header without the data inside

Expand All @@ -465,7 +465,11 @@ def write_netcdf_header(settings,
list of reporting steps
frequency:
output frequency (all, monthly or yearly)

map_value: ReportedMap namedtuple or None, optional
Output variable metadata from default_options.py. When provided and OutputPacking is
enabled, its scale_factor and add_offset attributes are used for int16 packing.
If None (default), no packing is applied.

Returns
-------
object
Expand Down Expand Up @@ -569,7 +573,25 @@ def write_netcdf_header(settings,
time.units = 'minutes since %s' % start_date.strftime("%Y-%m-%d %H:%M:%S.0")
nf1.variables["time"][:] = date2num(time_stamps, time.units, time.calendar)

value = nf1.createVariable(var_name, dtype, ('time', dim_lat_y, dim_lon_x), zlib=True, fill_value=-9999, chunksizes=(1, nrow, ncol))
# value = nf1.createVariable(var_name, dtype, ('time', dim_lat_y, dim_lon_x), zlib=True, fill_value=-9999, chunksizes=(1, nrow, ncol))
# Packing: use int16 with CF scale/offset if enabled and variable has packing metadata
packing_enabled = binding.get('OutputPacking', 'False') == 'True'
has_packing = (map_value is not None
and getattr(map_value, 'scale_factor', None) is not None
and getattr(map_value, 'add_offset', None) is not None)
if packing_enabled and has_packing:
Comment thread
fuchsiger marked this conversation as resolved.
var_dtype = 'i2'
var_fill = default_fillvals['i2']
else:
var_dtype = dtype
var_fill = -9999

value = nf1.createVariable(var_name, var_dtype, ('time', dim_lat_y, dim_lon_x),
zlib=True, fill_value=var_fill, chunksizes=(1, nrow, ncol))
if packing_enabled and has_packing:
value.scale_factor = np.float64(map_value.scale_factor)
value.add_offset = np.float64(map_value.add_offset)

else:
value = nf1.createVariable(var_name, dtype, (dim_lat_y, dim_lon_x), zlib=True, fill_value=-9999)

Expand Down
154 changes: 147 additions & 7 deletions src/lisflood/global_modules/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@

"""
import os
import datetime
import numpy as np
from pcraster import ifthen, catchmenttotal, mapmaximum
import sys
import warnings

from .zusatz import TimeoutputTimeseries
from .add1 import decompress, valuecell, loadmap, compressArray
from .netcdf import write_netcdf_header, iterOpenNetcdf, nanCheckMap, uncompress_array
from .errors import LisfloodFileError, LisfloodWarning
from .settings import inttodate, CDFFlags, LisSettings
from .settings import inttodate, CDFFlags, LisSettings, MaskInfo
from netCDF4 import default_fillvals


# ------------------------------------------------------------------------
# Packing constants for int16 CF scale/offset encoding
# ------------------------------------------------------------------------
PACK_FILL = np.int16(default_fillvals['i2']) # -32767
PACK_MIN = PACK_FILL + 1 # -32766
PACK_MAX = np.iinfo(np.int16).max # 32767

# ------------------------------------------------------------------------
# Writer classes
# ------------------------------------------------------------------------
Expand Down Expand Up @@ -95,7 +105,7 @@ def write(self, start_date, rep_steps):
if self.data is not None:
nf1 = write_netcdf_header(self.settings, self.map_name, self.map_path, self.var.DtDay,
self.map_key, self.map_value.output_var, self.map_value.unit,
start_date, rep_steps, self.frequency)
start_date, rep_steps, self.frequency, map_value=self.map_value)

map_np = uncompress_array(self.data)

Expand Down Expand Up @@ -151,16 +161,38 @@ def write(self, start_date, rep_steps):
if self.step_range[0] == 0:
nf1 = write_netcdf_header(self.settings, self.map_name, self.map_path, self.var.DtDay,
self.map_key, self.map_value.output_var, self.map_value.unit,
start_date, rep_steps, self.frequency)
start_date, rep_steps, self.frequency, map_value=self.map_value)
else:
nf1 = iterOpenNetcdf(self.map_path, "", 'a', format='NETCDF4')

nc_var = nf1.variables[self.map_name]
is_packed = nc_var.dtype == np.int16
if is_packed:
nc_var.set_auto_maskandscale(False)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should double check the behavior of this line with the missing value. Maybe unit tests can help here to avoid issues

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I fully agree, in our teams conversation I think you said you will take care right, but in any case if your schedule doesnt allow just let me know and I will start :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already started it, don’t worry

scale = nc_var.scale_factor
offset = nc_var.add_offset
nodata_mask = MaskInfo.instance().info.mask

for step, data in zip(self.step_range, self.data_steps):
nf1.variables[self.map_name][step, :, :] = uncompress_array(data)
map_np = uncompress_array(data)
if is_packed:
packed = np.round((map_np - offset) / scale).astype(np.float64)
clipped = ((packed < -32767) | (packed > 32767)) & (map_np != -9999)
if clipped.any():
vmin = offset + scale * (-32767)
vmax = offset + scale * 32767
warnings.warn(LisfloodWarning(
f"OutputPacking: {clipped.sum()} values in '{self.map_name}' outside "
f"packing range [{vmin:.4g}, {vmax:.4g}] and will be clipped."
))
packed = np.clip(packed, PACK_MIN, PACK_MAX)
packed[nodata_mask] = PACK_FILL
nc_var[step, :, :] = packed.astype(np.int16)
else:
nc_var[step, :, :] = map_np

nf1.close()

# clear lists for next chunk
self.step_range.clear()
self.data_steps.clear()
else:
Expand Down Expand Up @@ -381,6 +413,87 @@ def _start_date(self):
def _rep_steps(self):
return self._rep_steps_val


class MapOutputAggregated(MapOutput):
"""Handles temporal aggregation (monthly/yearly mean/sum) for a variable."""

def __init__(self, var, map_key, map_value, frequency, operation):
out_type = 'all' # accumulates every timestep
settings = LisSettings.instance()
binding = settings.binding
self._start_date_val = var.CalendarDayStart
self._rep_steps_val = range(binding['StepStartInt'], binding['StepEndInt'] + 1)

self._operation = operation # 'mean' or 'sum'
self._accum_buffer = None
self._accum_count = 0
self._write_step = 0 # own step counter for NetCDF time dimension

# Disable int16 packing for sum aggregates — monthly/yearly sums can exceed
# the int16 range calibrated for daily values. Mean aggregates stay within
# the same value range as daily output, so packing remains valid.
if operation == 'sum':
map_value_no_pack = map_value._replace(scale_factor=None, add_offset=None)
else:
map_value_no_pack = map_value

super().__init__(var, out_type, frequency, map_key, map_value_no_pack)

# Force immediate write for aggregated outputs (one slice per period)
if hasattr(self, 'writer') and hasattr(self.writer, 'chunks'):
self.writer.chunks = 1

def _output_checkpoint(self):
"""Always True — we accumulate every timestep."""
return True

@property
def _start_date(self):
return self._start_date_val

@property
def _rep_steps(self):
return self._rep_steps_val

def stage(self):
"""Accumulate instead of storing instantaneous values."""
self.step = self.var.currentTimeStep()
map_np = self.writer._extract_map()

if self._accum_buffer is None:
self._accum_buffer = np.zeros_like(map_np)

self._accum_buffer += map_np
self._accum_count += 1

def write(self):
"""Write only at period boundary (month-end or year-end)."""
current_date = self.var.CalendarDate
next_date = current_date + datetime.timedelta(days=self.var.DtDay)
if self.frequency == 'monthly':
is_boundary = current_date.month != next_date.month
elif self.frequency == 'yearly':
is_boundary = current_date.year != next_date.year
else:
is_boundary = True

if is_boundary and self._accum_buffer is not None:
# Finalize
if self._operation == 'mean':
result = self._accum_buffer / self._accum_count
else: # sum
result = self._accum_buffer

# Stage the aggregated result into the writer
self.writer.data_steps.append(result)
self.writer.step_range.append(self._write_step)
self.writer.write(self._start_date, self._rep_steps)

# Increment own step counter and reset accumulator
self._write_step += 1
self._accum_buffer = None
self._accum_count = 0

# ------------------------------------------------------------------------
# Output factory
# ------------------------------------------------------------------------
Expand Down Expand Up @@ -428,11 +541,38 @@ def __init__(self, var):
if out.is_valid():
outputs.append(out)

check_duplicates = []
# --- Temporal aggregation outputs ---
binding = settings.binding
aggregation_configs = {
'OutputMonthlyMean': ('monthly', 'mean'),
'OutputMonthlySum': ('monthly', 'sum'),
'OutputYearlyMean': ('yearly', 'mean'),
'OutputYearlySum': ('yearly', 'sum'),
}
aggregated_vars = set() # track which vars are aggregated

reportedmaps = settings.options['reportedmaps']
for setting_key, (frequency, operation) in aggregation_configs.items():
var_list = binding.get(setting_key, '').split(';')
for var_name in var_list:
var_name = var_name.strip()
if var_name and var_name in reportedmaps:
map_value = reportedmaps[var_name]
out = MapOutputAggregated(var, var_name, map_value, frequency, operation)
if out.is_valid():
outputs.append(out)
aggregated_vars.add(var_name)

# Remove normal outputs for variables that are now aggregated
outputs_clean = []
check_duplicates = []
for out in outputs:
# Skip normal Maps/All output if variable is aggregated
if hasattr(out, 'map_key') and out.map_key in aggregated_vars:
if not isinstance(out, (MapOutputEnd, MapOutputAggregated)):
continue
if out.map_path in check_duplicates:
print(f'Warning! Output map {out.map_path} is duplicated, check list of outputs')
print(f'Warning! Output map {out.map_path} is duplicated')
else:
check_duplicates.append(out.map_path)
outputs_clean.append(out)
Expand Down
8 changes: 8 additions & 0 deletions src/lisflood/global_modules/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,14 @@ def __init__(self, model):
msg += "\t[X] The simulation output as specified in the settings file can be found in {}\n".format(out_dir)
msg += "\t[X] Activated modules: {}\n".format(activated_options)
msg += "\t[X] Report options: {}\n".format(activated_rep)
# Packing and aggregation info
binding = settings.binding
if binding.get('OutputPacking', 'False') == 'True':
msg += "\t[X] Output Packing: int16 scale/offset enabled\n"
for agg_key in ['OutputMonthlyMean', 'OutputMonthlySum', 'OutputYearlyMean', 'OutputYearlySum']:
agg_val = binding.get(agg_key, '').strip()
if agg_val:
msg += "\t[X] {}: {}\n".format(agg_key, agg_val)
self._msg = '{}{}'.format(header, msg)

def __str__(self):
Expand Down
Loading