Skip to content

Commit

Permalink
Started making forcing optional in listflood
Browse files Browse the repository at this point in the history
Refs #148
  • Loading branch information
sverhoeven committed Sep 24, 2021
1 parent ee43e4c commit 6da76a7
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 39 deletions.
109 changes: 70 additions & 39 deletions src/ewatercycle/models/lisflood.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import logging
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any, Iterable, Tuple, cast
from typing import Any, Iterable, Optional, Tuple, cast

import numpy as np
import xarray as xr
from cftime import num2date
from dateutil.parser import parse
from grpc4bmi.bmi_client_docker import BmiClientDocker
from grpc4bmi.bmi_client_singularity import BmiClientSingularity

Expand Down Expand Up @@ -42,11 +43,11 @@ def __init__( # noqa: D107
self,
version: str,
parameter_set: ParameterSet,
forcing: LisfloodForcing,
forcing: Optional[LisfloodForcing] = None,
):
super().__init__(version, parameter_set, forcing)
self._check_forcing(forcing)
self.cfg = XmlConfig(parameter_set.config)
self._check_forcing(forcing)

def _set_docker_image(self):
images = {"20.10": "ewatercycle/lisflood-grpc4bmi:20.10"}
Expand Down Expand Up @@ -83,10 +84,12 @@ def setup( # type: ignore
max 1, ~0.90 drip irrigation, ~0.75 sprinkling
start_time: Start time of model in UTC and ISO format string
e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
If not given then forcing start time is used.
If not given then forcing start time is used
or if forcing not given then uses start time from parameter set config file.
end_time: End time of model in UTC and ISO format string
e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
If not given then forcing end time is used.
If not given then forcing end time is used
or if forcing not given then uses computed end time from parameter set config file.
MaskMap: Mask map to use instead of one supplied in parameter set.
Path to a NetCDF or pcraster file with
same dimensions as parameter set map files and a boolean variable.
Expand All @@ -95,7 +98,6 @@ def setup( # type: ignore
Returns:
Path to config file and path to config directory
"""
# TODO forcing can be a part of parameter_set
cfg_dir_as_path = None
if cfg_dir:
cfg_dir_as_path = to_absolute_path(cfg_dir)
Expand All @@ -110,7 +112,9 @@ def setup( # type: ignore
)

assert self.parameter_set is not None
input_dirs = [str(self.parameter_set.directory), str(self.forcing_dir)]
input_dirs = [str(self.parameter_set.directory)]
if self.forcing is not None:
input_dirs.append(str(self.forcing_dir))
if MaskMap is not None:
mask_map = to_absolute_path(MaskMap)
try:
Expand Down Expand Up @@ -142,10 +146,35 @@ def setup( # type: ignore
)
return str(config_file), str(cfg_dir_as_path)

def _start_from_config(self):
# mimic date parsing of lisflood model,
# see https://github.com/ec-jrc/lisflood-code/blob/331304ad6b23377149caf99bcc7081180348fe57/src/lisflood/global_modules/settings.py#L558-L583
return get_time(
parse(
self._get_textvar_value("CalendarDayStart"), dayfirst=True
).isoformat()
+ "Z"
)

def _end_from_config(self):
_start = self._start_from_config()
step_size = int(self._get_textvar_value("DtSec"))
step_start = int(self._get_textvar_value("StepStart"))
step_end = int(self._get_textvar_value("StepEnd"))
end_offset = datetime.timedelta(seconds=(step_end - step_start) * step_size)
end_date = _start + end_offset
return get_time(end_date.isoformat() + "Z")

def _check_forcing(self, forcing):
"""Check forcing argument and get path, start/end time of forcing data."""
# TODO check if mask has same grid as forcing files,
# if not warn users to run reindex_forcings
if forcing is None:
# Expect that forcings are inside parameter set
self.forcing = None
# TODO StepStart, StepEnd can also be date string instead of integer
self._start = self._start_from_config()
self._end = self._end_from_config()
if isinstance(forcing, LisfloodForcing):
self.forcing = forcing
self.forcing_dir = to_absolute_path(forcing.directory)
Expand All @@ -168,29 +197,31 @@ def _create_lisflood_config(
) -> Path:
"""Create lisflood config file."""
assert self.parameter_set is not None
assert self.forcing is not None
# overwrite dates if given
if start_time_iso is not None:
start_time = get_time(start_time_iso)
if self._start <= start_time <= self._end:
self._start = start_time
else:
raise ValueError("start_time outside forcing time range")
raise ValueError(
"start_time outside forcing or parameter set time range"
)
if end_time_iso is not None:
end_time = get_time(end_time_iso)
if self._start <= end_time <= self._end:
self._end = end_time
else:
raise ValueError("end_time outside forcing time range")
raise ValueError("end_time outside forcing or parameter set time range")

settings = {
"CalendarDayStart": self._start.strftime("%d/%m/%Y 00:00"),
"StepStart": "1",
"StepEnd": str((self._end - self._start).days),
"PathRoot": str(self.parameter_set.directory),
"PathMeteo": str(self.forcing_dir),
"PathOut": str(cfg_dir),
}
if self.forcing is not None:
settings["PathMeteo"] = str(self.forcing_dir)

if IrrigationEfficiency is not None:
settings["IrrigationEfficiency"] = IrrigationEfficiency
Expand All @@ -206,33 +237,34 @@ def _create_lisflood_config(
if key in textvar_name:
textvar.set("value", value)

# input for lisflood
if "PrefixPrecipitation" in textvar_name:
textvar.set("value", Path(self.forcing.PrefixPrecipitation).stem)
if "PrefixTavg" in textvar_name:
textvar.set("value", Path(self.forcing.PrefixTavg).stem)

# maps_prefixes dictionary contains lisvap filenames in lisflood config
maps_prefixes = {
"E0Maps": {
"name": "PrefixE0",
"value": Path(self.forcing.PrefixE0).stem,
},
"ES0Maps": {
"name": "PrefixES0",
"value": Path(self.forcing.PrefixES0).stem,
},
"ET0Maps": {
"name": "PrefixET0",
"value": Path(self.forcing.PrefixET0).stem,
},
}
# output of lisvap
for map_var, prefix in maps_prefixes.items():
if prefix["name"] in textvar_name:
textvar.set("value", prefix["value"])
if map_var in textvar_name:
textvar.set("value", f"$(PathMeteo)/$({prefix['name']})")
if self.forcing is not None:
# input for lisflood
if "PrefixPrecipitation" in textvar_name:
textvar.set("value", Path(self.forcing.PrefixPrecipitation).stem)
if "PrefixTavg" in textvar_name:
textvar.set("value", Path(self.forcing.PrefixTavg).stem)

# maps_prefixes dictionary contains lisvap filenames in lisflood config
maps_prefixes = {
"E0Maps": {
"name": "PrefixE0",
"value": Path(self.forcing.PrefixE0).stem,
},
"ES0Maps": {
"name": "PrefixES0",
"value": Path(self.forcing.PrefixES0).stem,
},
"ET0Maps": {
"name": "PrefixET0",
"value": Path(self.forcing.PrefixET0).stem,
},
}
# output of lisvap
for map_var, prefix in maps_prefixes.items():
if prefix["name"] in textvar_name:
textvar.set("value", prefix["value"])
if map_var in textvar_name:
textvar.set("value", f"$(PathMeteo)/$({prefix['name']})")

# Write to new setting file
lisflood_file = cfg_dir / "lisflood_setting.xml"
Expand Down Expand Up @@ -294,7 +326,6 @@ def _coords_to_indices(
def parameters(self) -> Iterable[Tuple[str, Any]]:
"""List the parameters for this model."""
assert self.parameter_set is not None
assert self.forcing is not None
# TODO fix issue #60
return [
(
Expand Down
26 changes: 26 additions & 0 deletions tests/models/test_lisflood.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,32 @@ def test_parameters_after_setup(
assert model.parameters == expected_parameters


class TestOnlyParameterSet:
@pytest.fixture
def parameterset(self, mocked_config):
example_parameter_set = example_parameter_sets()["lisflood_fraser"]
example_parameter_set.download()
example_parameter_set.to_config()
return example_parameter_set

@pytest.fixture
def model(self, parameterset):
m = Lisflood(version="20.10", parameter_set=parameterset)
yield m
if m.bmi:
# Clean up container
del m.bmi

def test_default_parameters(self, model: Lisflood):
expected_parameters = [
("IrrigationEfficiency", "0.75"),
("MaskMap", "$(PathMaps)/masksmall.map"),
("start_time", "1986-01-02T00:00:00Z"),
("end_time", "2018-01-02T00:00:00Z"),
]
assert model.parameters == expected_parameters


class MockedBmi(Bmi):
"""Mimic a real use case with realistic shape and abitrary high precision."""

Expand Down

0 comments on commit 6da76a7

Please sign in to comment.