Skip to content
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

Change API for PCRGlobWB model class #85

Merged
merged 26 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0cf2e38
Add PCRGlobWBParameterSet
Peter9192 May 17, 2021
e72b44d
Change constructor and setup methods
Peter9192 May 17, 2021
f29353a
Select container based on version argument
Peter9192 May 17, 2021
b9a329b
Update notebook and make it work
Peter9192 May 17, 2021
b013766
Fix absolute paths for additional input data
Peter9192 May 17, 2021
8fb414a
update notebook text cells
Peter9192 May 17, 2021
cbdb7c5
Remove container locations from config
Peter9192 May 17, 2021
6aeb2f6
Formatting etc.
Peter9192 May 17, 2021
5fb0053
Add (optional) forcing class to constructor
Peter9192 May 19, 2021
a9cc9d3
Add warnings to conda env
Peter9192 May 19, 2021
9b5082e
Revert "Add warnings to conda env"
Peter9192 May 20, 2021
2e68eaa
Merge remote-tracking branch 'origin/master' into pcrg-new-constructor
Peter9192 Jun 9, 2021
bff06a9
Switch to using new forcing module
Peter9192 Jun 9, 2021
614f4d0
Docstring for forcing
Peter9192 Jun 9, 2021
a18d604
Make versions a tuple
Peter9192 Jun 9, 2021
01cdfa5
Use docker:// for singularity image path
Peter9192 Jun 9, 2021
8024125
Merge remote-tracking branch 'origin/master' into pcrg-new-constructor
Peter9192 Jun 10, 2021
7f5fc18
Forcing files relative to forcing dir
Peter9192 Jun 10, 2021
fd99905
Update forcing paths in setup of default config and document
Peter9192 Jun 10, 2021
87f978d
Reduce number of configurable parameters
Peter9192 Jun 10, 2021
4db744e
Add __str__ methods on the forcing and parametersets
Peter9192 Jun 10, 2021
95a6a87
Catch timeout on singularity spawner
Peter9192 Jun 10, 2021
b5333a3
Merge remote-tracking branch 'origin/master' into pcrg-new-constructor
Peter9192 Jun 14, 2021
80f4a29
Remove additional_input_dirs option (and add black formattting)
Peter9192 Jun 18, 2021
93cafbf
Catch all futuretimeouts for starting containers
Peter9192 Jun 18, 2021
1dc36c4
Merge remote-tracking branch 'origin/master' into pcrg-new-constructor
Peter9192 Jun 18, 2021
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
2 changes: 0 additions & 2 deletions ewatercycle/config/ewatercycle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@ marrmot.singularity_image: null
marrmot.docker_image: null
lisflood.singularity_image: null
lisflood.docker_image: null
pcrglobwb.singularity_image: null
pcrglobwb.docker_image: null
196 changes: 134 additions & 62 deletions ewatercycle/models/pcrglobwb.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import shutil
import time
import warnings
from dataclasses import dataclass
from os import PathLike
from pathlib import Path
from typing import Any, Iterable, Optional, Tuple
from typing import Any, Iterable, Optional, Tuple, Union

import numpy as np
import xarray as xr
Expand All @@ -15,94 +17,175 @@
from ewatercycle.parametersetdb.config import CaseConfigParser


@dataclass
class PCRGlobWBParameterSet:
"""Parameter set for the PCRGlobWB model class.

A valid pcrglobwb parameter set consists of a folder with input data files
and should always include a default configuration file.
"""
input_dir: Union[str, PathLike]
"""Input folder path."""
default_config: Union[str, PathLike]
"""Path to (default) model configuration file consistent with `input_data`."""
def __setattr__(self, name: str, value: Union[str, PathLike]):
self.__dict__[name] = Path(value).expanduser().resolve()


@dataclass
class PCRGlobWBForcing:
"""Forcing data for the PCRGlobWB model class."""
precipitationNC: Optional[Union[str, PathLike]] = None
"""Input file for precipitation data."""
temperatureNC: Optional[Union[str, PathLike]] = None
"""Input file for temperature data."""
refETPotFileNC: Optional[Union[str, PathLike]] = None
"""Input file for reference potential evapotranspiration data."""
def __setattr__(self, name: str, value: Union[str, PathLike, None]):
if value is not None and Path(value).exists():
self.__dict__[name] = Path(value).expanduser().resolve()
else:
warnings.warn(
f"No valid input path received for {name}, defaulting to ''.")
self.__dict__[name] = ''


class PCRGlobWB(AbstractModel):
"""eWaterCycle implementation of PCRGlobWB hydrological model.

Attributes
Args:

version: pick a version from :py:attr:`~available_versions`
parameter_set: instance of :py:class:`~PCRGlobWBParameterSet`.
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved

Attributes:

bmi (Bmi): GRPC4BMI Basic Modeling Interface object
"""
def setup( # type: ignore
self,
input_dir: PathLike,
cfg_file: PathLike,
additional_input_dirs: Iterable[PathLike] = [],
**kwargs) -> Tuple[PathLike, PathLike]:
"""Start model inside container and return config file and work dir.
available_versions = ('setters')
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved

Args:
def __init__(self,
version: str,
parameter_set: PCRGlobWBParameterSet,
forcing: Optional[PCRGlobWBForcing] = None):
super().__init__()

- input_dir: main input directory. Relative paths in the cfg_file
should start from this directory.
self.version = version
self.parameter_set = parameter_set
self._additional_input_dirs: Iterable[PathLike] = []

- cfg_file: path to a valid pcrglobwb configuration file,
typically somethig like `setup.ini`.
self._set_docker_image()
self._set_singularity_image()

- additional_input_dirs: one or more additional data directories
that the model will have access to.
self._setup_work_dir()
self._setup_default_config()

- **kwargs (optional, dict): any settings in the cfg_file that you
want to overwrite programmatically. Should be passed as a dict,
e.g. `meteoOptions = {"temperatureNC": "era5_tas_1990_2000.nc"}`
where meteoOptions is the section in which the temperatureNC option
may be found.
if forcing is not None:
self._update_config(meteoOptions=forcing.__dict__)

Returns: Path to config file and work dir
"""
self._setup_work_dir()
self._setup_config(cfg_file, input_dir, **kwargs)
self._start_container(input_dir, additional_input_dirs)
def _set_docker_image(self):
images = {
"setters": "ewatercycle/pcrg-grpc4bmi:setters",
}
self.docker_image = images[self.version]

return self.cfg_file, self.work_dir,
def _set_singularity_image(self):
# TODO auto detect sif file based on docker image and singularity dir.
images = {
"setters": "ewatercycle/pcrg-grpc4bmi:setters",
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved
}
self.singularity_image = CFG['singularity_dir'] / images[self.version]

def _setup_work_dir(self):
# Must exist before setting up default config
timestamp = time.strftime("%Y%m%d_%H%M%S")
work_dir = Path(CFG["output_dir"]) / f'pcrglobwb_{timestamp}'
work_dir.mkdir()
self.work_dir = work_dir.resolve()
print(f"Created working directory: {work_dir}")
self.work_dir = work_dir.expanduser().resolve()

def _setup_config(self, cfg_file: PathLike, input_dir: PathLike, **kwargs):
cfg = CaseConfigParser()
cfg.read(cfg_file)
self.cfg = cfg
def _setup_default_config(self):
config_file = self.parameter_set.default_config
input_dir = self.parameter_set.input_dir

full_input_path = Path(input_dir).resolve()
cfg.set('globalOptions', 'inputDir', str(full_input_path))
cfg = CaseConfigParser()
cfg.read(config_file)
cfg.set('globalOptions', 'inputDir', str(input_dir))
cfg.set('globalOptions', 'outputDir', str(self.work_dir))

self.config = cfg

def setup( # type: ignore
self, **kwargs) -> Tuple[PathLike, PathLike]:
"""Start model inside container and return config file and work dir.

Args:

- **kwargs (optional, dict): Should be passed as a dict,
e.g. `meteoOptions = {"temperatureNC": "era5_tas_1990_2000.nc"}`
where meteoOptions is the section in which the temperatureNC option
may be found. See :py:attr:`~parameters` for all available settings.

Returns: Path to config file and work dir
"""
self._update_config(**kwargs)
cfg_file = self._export_config()
work_dir = self.work_dir

self._start_container()

return cfg_file, work_dir

def _update_config(self, **kwargs):
cfg = self.config

default_input_dir = self.parameter_set.input_dir

for section, options in kwargs.items():
if not cfg.has_section(section):
cfg.add_section(section)

for option, value in options.items():
cfg.set(section, option, value)

if Path(value).exists():
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved
# New data paths must be mounted on the container
inputpath = Path(value).expanduser().resolve()
if default_input_dir in inputpath.parents:
pass
elif inputpath.is_dir():
self._additional_input_dirs.append(str(inputpath))
else:
self._additional_input_dirs.append(
str(inputpath.parent))
cfg.set(section, option, str(inputpath))
else:
cfg.set(section, option, value)

def _export_config(self) -> PathLike:
new_cfg_file = Path(self.work_dir) / "pcrglobwb_ewatercycle.ini"
with new_cfg_file.open("w") as filename:
cfg.write(filename)
self.config.write(filename)

self.cfg_file = new_cfg_file.resolve()
print(f"Created config file {self.cfg_file} with inputDir "
f"{full_input_path} and outputDir {self.work_dir}.")
self.cfg_file = new_cfg_file.expanduser().resolve()
return self.cfg_file

def _start_container(self,
input_dir: PathLike,
additional_input_dirs: Iterable[PathLike] = []):
input_dirs = [input_dir] + list(additional_input_dirs)
def _start_container(self):
input_dirs = [self.parameter_set.input_dir
] + self._additional_input_dirs

if CFG["container_engine"] == "docker":
self.bmi = BmiClientDocker(
image=CFG["pcrglobwb.docker_image"],
image=self.docker_image,
image_port=55555,
work_dir=str(self.work_dir),
input_dirs=[str(input_dir) for input_dir in input_dirs],
timeout=10,
sverhoeven marked this conversation as resolved.
Show resolved Hide resolved
)
elif CFG["container_engine"] == "singularity":
image = CFG["pcrglobwb.singularity_image"]

message = f"No singularity image found at {image}"
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved
assert Path(image).exists(), message
assert self.singularity_image.exists(), message
Peter9192 marked this conversation as resolved.
Show resolved Hide resolved

self.bmi = BmiClientSingularity(
image=image,
image=str(self.singularity_image),
work_dir=str(self.work_dir),
input_dirs=[str(input_path) for input_path in input_dirs],
timeout=10,
Expand All @@ -112,11 +195,6 @@ def _start_container(self,
f"Unknown container technology in CFG: {CFG['container_engine']}"
)

inputs = "\n".join([str(Path(p).resolve()) for p in input_dirs])
print(
f"Started model container with working directory {self.work_dir} "
f"and access to the following input directories:\n{inputs}.")

def get_value_as_xarray(self, name: str) -> xr.DataArray:
"""Return the value as xarray object."""
# Get time information
Expand All @@ -142,11 +220,5 @@ def get_value_as_xarray(self, name: str) -> xr.DataArray:
@property
def parameters(self) -> Iterable[Tuple[str, Any]]:
"""List the configurable parameters for this model."""
if not hasattr(self, "cfg"):
raise NotImplementedError(
"No default parameters available for pcrglobwb. To see the "
"parameters, first run setup with a valid .ini file.")

return [(f"{section}.{option}", f"{self.cfg.get(section, option)}")
for section in self.cfg.sections()
for option in self.cfg.options(section)]
return [(section, dict(self.config[section]))
for section in self.config.sections()]
Loading