-
Notifications
You must be signed in to change notification settings - Fork 65
Feature/offset scale outputs #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
785d889
025cff0
c7f0416
657726f
9936a0f
a0fc652
c34f0bd
d2d6066
757061b
081ec5e
33e8d55
964a24a
12e4b1b
9d71096
ac75a1a
4f0751d
f88b4ce
8d9566d
5882f24
9f916c4
9dc1da9
3580718
0af2e47
6a608ee
b8d835b
4969862
ac359c3
f0e6971
3098d63
f31f67f
c22201e
cd2c9f2
cf98283
434d421
82e2e7c
2cbbbe9
20ed76d
cc5e212
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| # ------------------------------------------------------------------------ | ||
|
|
@@ -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) | ||
|
|
||
|
|
@@ -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) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
@@ -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 | ||
| # ------------------------------------------------------------------------ | ||
|
|
@@ -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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.