예제 #1
0
 def _start_container(self):
     if CFG["container_engine"] == "docker":
         self.bmi = BmiClientDocker(
             image=self.docker_image,
             image_port=55555,
             work_dir=str(self.work_dir),
             timeout=300,
         )
     elif CFG["container_engine"] == "singularity":
         self.bmi = BmiClientSingularity(
             image=self._singularity_image(CFG["singularity_dir"]),
             work_dir=str(self.work_dir),
             timeout=300,
         )
     else:
         raise ValueError(
             f"Unknown container technology: {CFG['container_engine']}")
예제 #2
0
    def _start_container(self):
        additional_input_dirs = [str(self.parameter_set.directory)]
        if self.forcing:
            additional_input_dirs.append(self.forcing.directory)

        if CFG["container_engine"] == "docker":
            self.bmi = BmiClientDocker(
                image=self.docker_image,
                image_port=55555,
                work_dir=str(self.work_dir),
                input_dirs=additional_input_dirs,
                timeout=300,
            )
        elif CFG["container_engine"] == "singularity":
            self.bmi = BmiClientSingularity(
                image=self._singularity_image(CFG["singularity_dir"]),
                work_dir=str(self.work_dir),
                input_dirs=additional_input_dirs,
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology in CFG: {CFG['container_engine']}"
            )
예제 #3
0
class Wflow(AbstractModel[WflowForcing]):
    """Create an instance of the Wflow model class.

    Args:
        version: pick a version from :py:attr:`~available_versions`
        parameter_set: instance of
            :py:class:`~ewatercycle.parameter_sets.default.ParameterSet`.
        forcing: instance of :py:class:`~WflowForcing` or None.
            If None, it is assumed that forcing is included with the parameter_set.
    """

    available_versions = ("2020.1.1", "2020.1.2", "2020.1.3")
    """Show supported WFlow versions in eWaterCycle"""
    def __init__(  # noqa: D107
        self,
        version: str,
        parameter_set: ParameterSet,
        forcing: Optional[WflowForcing] = None,
    ):
        super().__init__(version, parameter_set, forcing)
        self._set_docker_image()
        self._setup_default_config()

    def _set_docker_image(self):
        images = {
            # "2019.1": "ewatercycle/wflow-grpc4bmi:2019.1", # no good ini file
            "2020.1.1": "ewatercycle/wflow-grpc4bmi:2020.1.1",
            "2020.1.2": "ewatercycle/wflow-grpc4bmi:2020.1.2",
            "2020.1.3": "ewatercycle/wflow-grpc4bmi:2020.1.3",
        }
        self.docker_image = images[self.version]

    def _singularity_image(self, singularity_dir):
        images = {
            "2020.1.1": "ewatercycle-wflow-grpc4bmi_2020.1.1.sif",
            "2020.1.2": "ewatercycle-wflow-grpc4bmi_2020.1.2.sif",
            "2020.1.3": "ewatercycle-wflow-grpc4bmi_2020.1.3.sif",
        }
        image = singularity_dir / images[self.version]
        return str(image)

    def _setup_default_config(self):
        config_file = self.parameter_set.config
        forcing = self.forcing

        cfg = CaseConfigParser()
        cfg.read(config_file)

        if forcing:
            cfg.set("framework", "netcdfinput", Path(forcing.netcdfinput).name)
            cfg.set("inputmapstacks", "Precipitation", forcing.Precipitation)
            cfg.set(
                "inputmapstacks",
                "EvapoTranspiration",
                forcing.EvapoTranspiration,
            )
            cfg.set("inputmapstacks", "Temperature", forcing.Temperature)
            cfg.set("run", "starttime", _iso_to_wflow(forcing.start_time))
            cfg.set("run", "endtime", _iso_to_wflow(forcing.end_time))
        if self.version in self.available_versions:
            if not cfg.has_section("API"):
                logger.warning(
                    "Config file from parameter set is missing API section, "
                    "adding section")
                cfg.add_section("API")
            if not cfg.has_option("API", "RiverRunoff"):
                logger.warning(
                    "Config file from parameter set is missing RiverRunoff "
                    "option in API section, added it with value '2, m/s option'"
                )
                cfg.set("API", "RiverRunoff", "2, m/s")

        self.config = cfg

    def setup(self,
              cfg_dir: str = None,
              **kwargs) -> Tuple[str, str]:  # type: ignore
        """Start the model inside a container and return a valid config file.

        Args:
            cfg_dir: a run directory given by user or created for user.
            **kwargs (optional, dict): see :py:attr:`~parameters` for all
                configurable model parameters.

        Returns:
            Path to config file and working directory
        """
        self._setup_working_directory(cfg_dir)
        cfg = self.config

        if "start_time" in kwargs:
            cfg.set("run", "starttime", _iso_to_wflow(kwargs["start_time"]))
        if "end_time" in kwargs:
            cfg.set("run", "endtime", _iso_to_wflow(kwargs["end_time"]))

        updated_cfg_file = to_absolute_path("wflow_ewatercycle.ini",
                                            parent=self.work_dir)
        with updated_cfg_file.open("w") as filename:
            cfg.write(filename)

        try:
            self._start_container()
        except FutureTimeoutError as exc:
            # https://github.com/eWaterCycle/grpc4bmi/issues/95
            # https://github.com/eWaterCycle/grpc4bmi/issues/100
            raise ValueError(
                "Couldn't spawn container within allocated time limit "
                "(300 seconds). You may try pulling the docker image with"
                f" `docker pull {self.docker_image}` or call `singularity "
                f"build {self._singularity_image(CFG['singularity_dir'])} "
                f"docker://{self.docker_image}` if you're using singularity,"
                " and then try again.") from exc

        return (
            str(updated_cfg_file),
            str(self.work_dir),
        )

    def _setup_working_directory(self, cfg_dir: str = None):
        if cfg_dir:
            self.work_dir = to_absolute_path(cfg_dir)
        else:
            timestamp = datetime.datetime.now(
                datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
            self.work_dir = to_absolute_path(f"wflow_{timestamp}",
                                             parent=CFG["output_dir"])
        # Make sure parents exist
        self.work_dir.parent.mkdir(parents=True, exist_ok=True)

        assert self.parameter_set
        shutil.copytree(src=self.parameter_set.directory, dst=self.work_dir)
        if self.forcing:
            forcing_path = to_absolute_path(self.forcing.netcdfinput,
                                            parent=self.forcing.directory)
            shutil.copy(src=forcing_path, dst=self.work_dir)

    def _start_container(self):
        if CFG["container_engine"] == "docker":
            self.bmi = BmiClientDocker(
                image=self.docker_image,
                image_port=55555,
                work_dir=str(self.work_dir),
                timeout=300,
            )
        elif CFG["container_engine"] == "singularity":
            self.bmi = BmiClientSingularity(
                image=self._singularity_image(CFG["singularity_dir"]),
                work_dir=str(self.work_dir),
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology: {CFG['container_engine']}")

    def _coords_to_indices(self, name: str, lat: Iterable[float],
                           lon: Iterable[float]) -> Iterable[int]:
        """Convert lat/lon values to index.

        Args:
            lat: Latitudinal value
            lon: Longitudinal value

        """
        grid_id = self.bmi.get_var_grid(name)
        shape = self.bmi.get_grid_shape(grid_id)  # (len(x), len(y))
        grid_lat = self.bmi.get_grid_x(grid_id)  # x is latitude
        grid_lon = self.bmi.get_grid_y(grid_id)  # y is longitude

        indices = []
        for point_lon, point_lat in zip(lon, lat):
            idx_lon, idx_lat = find_closest_point(grid_lon, grid_lat,
                                                  point_lon, point_lat)
            idx_flat = cast(int, np.ravel_multi_index((idx_lat, idx_lon),
                                                      shape))
            indices.append(idx_flat)

            logger.debug(
                f"Requested point was lon: {point_lon}, lat: {point_lat}; "
                "closest grid point is "
                f"{grid_lon[idx_lon]:.2f}, {grid_lat[idx_lat]:.2f}.")

        return indices

    def get_value_as_xarray(self, name: str) -> xr.DataArray:
        """Return the value as xarray object."""
        # Get time information
        time_units = self.bmi.get_time_units()
        grid = self.bmi.get_var_grid(name)
        shape = self.bmi.get_grid_shape(grid)

        # Extract the data and store it in an xarray DataArray
        da = xr.DataArray(
            data=np.reshape(self.bmi.get_value(name), shape),
            coords={
                "longitude": self.bmi.get_grid_y(grid),
                "latitude": self.bmi.get_grid_x(grid),
                "time": num2date(self.bmi.get_current_time(), time_units),
            },
            dims=["latitude", "longitude"],
            name=name,
            attrs={"units": self.bmi.get_var_units(name)},
        )

        return da.where(da != -999)

    @property
    def parameters(self) -> Iterable[Tuple[str, Any]]:
        """List the configurable parameters for this model."""
        # An opiniated list of configurable parameters.
        cfg = self.config
        return [
            ("start_time", _wflow_to_iso(cfg.get("run", "starttime"))),
            ("end_time", _wflow_to_iso(cfg.get("run", "endtime"))),
        ]
예제 #4
0
def walrus_model_with_extra_volume(walrus_input_on_extra_volume):
    (input_dir, docker_extra_volumes) = walrus_input_on_extra_volume
    extra_volumes = {str(k): str(v['bind']) for k, v in docker_extra_volumes.items()}
    model = BmiClientSingularity(image=IMAGE_NAME, input_dir=str(input_dir), extra_volumes=extra_volumes)
    yield model
    del model
예제 #5
0
def walrus_model(tmp_path, walrus_input):
    model = BmiClientSingularity(image=IMAGE_NAME, input_dir=str(tmp_path))
    yield model
    del model
예제 #6
0
    def setup(  # type: ignore
        self,
        maximum_soil_moisture_storage: float = None,
        initial_soil_moisture_storage: float = None,
        start_time: str = None,
        end_time: str = None,
        solver: Solver = None,
        cfg_dir: str = None,
    ) -> Tuple[str, str]:
        """Configure model run.

        1. Creates config file and config directory based on the forcing
           variables and time range
        2. Start bmi container and store as :py:attr:`bmi`

        Args:
            maximum_soil_moisture_storage: in mm. Range is specfied in `model
                parameter range file
                <https://github.com/wknoben/MARRMoT/blob/master/MARRMoT/Models/Parameter%20range%20files/m_01_collie1_1p_1s_parameter_ranges.m>`_.
            initial_soil_moisture_storage: in mm.
            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.
            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.
            solver: Solver settings
            cfg_dir: a run directory given by user or created for user.

        Returns:
            Path to config file and path to config directory
        """
        if maximum_soil_moisture_storage:
            self._parameters = [maximum_soil_moisture_storage]
        if initial_soil_moisture_storage:
            self.store_ini = [initial_soil_moisture_storage]
        if solver:
            self.solver = solver

        cfg_dir_as_path = None
        if cfg_dir:
            cfg_dir_as_path = to_absolute_path(cfg_dir)

        cfg_dir_as_path = _generate_cfg_dir(cfg_dir_as_path)
        config_file = self._create_marrmot_config(cfg_dir_as_path, start_time,
                                                  end_time)

        if CFG["container_engine"].lower() == "singularity":
            message = f"The singularity image {self.singularity_image} does not exist."
            assert self.singularity_image.exists(), message
            self.bmi = BmiClientSingularity(
                image=str(self.singularity_image),
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        elif CFG["container_engine"].lower() == "docker":
            self.bmi = BmiClientDocker(
                image=self.docker_image,
                image_port=55555,
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology in CFG: {CFG['container_engine']}"
            )
        return str(config_file), str(cfg_dir_as_path)
예제 #7
0
    def setup(  # type: ignore
        self,
        maximum_soil_moisture_storage: float = None,
        threshold_flow_generation_evap_change: float = None,
        leakage_saturated_zone_flow_coefficient: float = None,
        zero_deficit_base_flow_speed: float = None,
        baseflow_coefficient: float = None,
        gamma_distribution_chi_parameter: float = None,
        gamma_distribution_phi_parameter: float = None,
        initial_upper_zone_storage: float = None,
        initial_saturated_zone_storage: float = None,
        start_time: str = None,
        end_time: str = None,
        solver: Solver = None,
        cfg_dir: str = None,
    ) -> Tuple[str, str]:
        """Configure model run.

        1. Creates config file and config directory based on the forcing
           variables and time range
        2. Start bmi container and store as :py:attr:`bmi`

        Args:
            maximum_soil_moisture_storage: in mm. Range is specfied in `model
                parameter range file
                <https://github.com/wknoben/MARRMoT/blob/master/MARRMoT/Models/Parameter%20range%20files/m_01_collie1_1p_1s_parameter_ranges.m>`_.
                threshold_flow_generation_evap_change.
            leakage_saturated_zone_flow_coefficient: in mm/d.
            zero_deficit_base_flow_speed: in mm/d.
            baseflow_coefficient: in mm-1.
            gamma_distribution_chi_parameter.
            gamma_distribution_phi_parameter.
            initial_upper_zone_storage: in mm.
            initial_saturated_zone_storage: in mm.
            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.
            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.
                solver: Solver settings
            cfg_dir: a run directory given by user or created for user.

        Returns:
            Path to config file and path to config directory
        """
        arguments = vars()
        arguments_subset = {key: arguments[key] for key in M14_PARAMS}
        for index, key in enumerate(M14_PARAMS):
            if arguments_subset[key] is not None:
                self._parameters[index] = arguments_subset[key]
        if initial_upper_zone_storage:
            self.store_ini[0] = initial_upper_zone_storage
        if initial_saturated_zone_storage:
            self.store_ini[1] = initial_saturated_zone_storage
        if solver:
            self.solver = solver

        cfg_dir_as_path = None
        if cfg_dir:
            cfg_dir_as_path = to_absolute_path(cfg_dir)

        cfg_dir_as_path = _generate_cfg_dir(cfg_dir_as_path)
        config_file = self._create_marrmot_config(cfg_dir_as_path, start_time,
                                                  end_time)

        if CFG["container_engine"].lower() == "singularity":
            message = f"The singularity image {self.singularity_image} does not exist."
            assert self.singularity_image.exists(), message
            self.bmi = BmiClientSingularity(
                image=str(self.singularity_image),
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        elif CFG["container_engine"].lower() == "docker":
            self.bmi = BmiClientDocker(
                image=self.docker_image,
                image_port=55555,
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology in CFG: {CFG['container_engine']}"
            )
        return str(config_file), str(cfg_dir_as_path)
예제 #8
0
class PCRGlobWB(AbstractModel[PCRGlobWBForcing]):
    """eWaterCycle implementation of PCRGlobWB hydrological model.

    Args:
        version: pick a version from :py:attr:`~available_versions`
        parameter_set: instance of
            :py:class:`~ewatercycle.parameter_sets.default.ParameterSet`.
        forcing: ewatercycle forcing container;
            see :py:mod:`ewatercycle.forcing`.

    """

    available_versions = ("setters", )

    def __init__(  # noqa: D107
        self,
        version: str,
        parameter_set: ParameterSet,
        forcing: Optional[PCRGlobWBForcing] = None,
    ):
        super().__init__(version, parameter_set, forcing)
        self._set_docker_image()
        self._setup_default_config()

    def _set_docker_image(self):
        images = {
            "setters": "ewatercycle/pcrg-grpc4bmi:setters",
        }
        self.docker_image = images[self.version]

    def _singularity_image(self, singularity_dir):
        images = {
            "setters": "ewatercycle-pcrg-grpc4bmi_setters.sif",
        }
        image = singularity_dir / images[self.version]
        return str(image)

    def _setup_work_dir(self, cfg_dir: str = None):
        if cfg_dir:
            self.work_dir = to_absolute_path(cfg_dir)
        else:
            # Must exist before setting up default config
            timestamp = datetime.datetime.now(
                datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
            self.work_dir = to_absolute_path(f"pcrglobwb_{timestamp}",
                                             parent=CFG["output_dir"])
        self.work_dir.mkdir(parents=True, exist_ok=True)

    def _setup_default_config(self):
        config_file = self.parameter_set.config
        input_dir = self.parameter_set.directory

        cfg = CaseConfigParser()
        cfg.read(config_file)
        cfg.set("globalOptions", "inputDir", str(input_dir))
        if self.forcing:
            cfg.set(
                "globalOptions",
                "startTime",
                get_time(self.forcing.start_time).strftime("%Y-%m-%d"),
            )
            cfg.set(
                "globalOptions",
                "endTime",
                get_time(self.forcing.start_time).strftime("%Y-%m-%d"),
            )
            cfg.set(
                "meteoOptions",
                "temperatureNC",
                str(
                    to_absolute_path(
                        self.forcing.temperatureNC,
                        parent=self.forcing.directory,
                    )),
            )
            cfg.set(
                "meteoOptions",
                "precipitationNC",
                str(
                    to_absolute_path(
                        self.forcing.precipitationNC,
                        parent=self.forcing.directory,
                    )),
            )

        self.config = cfg

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

        Args:
            cfg_dir: a run directory given by user or created for user.
            **kwargs: Use :py:meth:`parameters` to see the current values
                configurable options for this model,

        Returns: Path to config file and work dir
        """
        self._setup_work_dir(cfg_dir)

        self._update_config(**kwargs)

        cfg_file = self._export_config()
        work_dir = self.work_dir

        try:
            self._start_container()
        except FutureTimeoutError as exc:
            # https://github.com/eWaterCycle/grpc4bmi/issues/95
            # https://github.com/eWaterCycle/grpc4bmi/issues/100
            raise ValueError(
                "Couldn't spawn container within allocated time limit "
                "(300 seconds). You may try pulling the docker image with"
                f" `docker pull {self.docker_image}` or call `singularity "
                f"build {self._singularity_image(CFG['singularity_dir'])} "
                f"docker://{self.docker_image}` if you're using singularity,"
                " and then try again.") from exc

        return str(cfg_file), str(work_dir)

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

        if "start_time" in kwargs:
            cfg.set(
                "globalOptions",
                "startTime",
                get_time(kwargs["start_time"]).strftime("%Y-%m-%d"),
            )

        if "end_time" in kwargs:
            cfg.set(
                "globalOptions",
                "endTime",
                get_time(kwargs["end_time"]).strftime("%Y-%m-%d"),
            )

        if "routing_method" in kwargs:
            cfg.set("routingOptions", "routingMethod",
                    kwargs["routing_method"])

        if "dynamic_flood_plain" in kwargs:
            cfg.set(
                "routingOptions",
                "dynamicFloodPlain",
                kwargs["dynamic_flood_plain"],
            )

        if "max_spinups_in_years" in kwargs:
            cfg.set(
                "globalOptions",
                "maxSpinUpsInYears",
                str(kwargs["max_spinups_in_years"]),
            )

    def _export_config(self) -> PathLike:
        self.config.set("globalOptions", "outputDir", str(self.work_dir))
        new_cfg_file = to_absolute_path("pcrglobwb_ewatercycle.ini",
                                        parent=self.work_dir)
        with new_cfg_file.open("w") as filename:
            self.config.write(filename)

        self.cfg_file = new_cfg_file
        return self.cfg_file

    def _start_container(self):
        additional_input_dirs = [str(self.parameter_set.directory)]
        if self.forcing:
            additional_input_dirs.append(self.forcing.directory)

        if CFG["container_engine"] == "docker":
            self.bmi = BmiClientDocker(
                image=self.docker_image,
                image_port=55555,
                work_dir=str(self.work_dir),
                input_dirs=additional_input_dirs,
                timeout=300,
            )
        elif CFG["container_engine"] == "singularity":
            self.bmi = BmiClientSingularity(
                image=self._singularity_image(CFG["singularity_dir"]),
                work_dir=str(self.work_dir),
                input_dirs=additional_input_dirs,
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology in CFG: {CFG['container_engine']}"
            )

    def _coords_to_indices(self, name: str, lat: Iterable[float],
                           lon: Iterable[float]) -> Iterable[int]:
        """Convert lat/lon values to index.

        Args:
            lat: Latitudinal value
            lon: Longitudinal value

        """
        grid_id = self.bmi.get_var_grid(name)
        shape = self.bmi.get_grid_shape(grid_id)  # (len(x), len(y))
        grid_lat = self.bmi.get_grid_x(grid_id)  # x is latitude
        grid_lon = self.bmi.get_grid_y(grid_id)  # y is longitude

        indices = []
        for point_lon, point_lat in zip(lon, lat):
            idx_lon, idx_lat = find_closest_point(grid_lon, grid_lat,
                                                  point_lon, point_lat)
            idx_flat = cast(int, np.ravel_multi_index((idx_lat, idx_lon),
                                                      shape))
            indices.append(idx_flat)

            logger.debug(
                f"Requested point was lon: {point_lon}, lat: {point_lat}; "
                "closest grid point is "
                f"{grid_lon[idx_lon]:.2f}, {grid_lat[idx_lat]:.2f}.")

        return indices

    def get_value_as_xarray(self, name: str) -> xr.DataArray:
        """Return the value as xarray object."""
        # Get time information
        time_units = self.bmi.get_time_units()
        grid = self.bmi.get_var_grid(name)
        shape = self.bmi.get_grid_shape(grid)

        # Extract the data and store it in an xarray DataArray
        da = xr.DataArray(
            data=np.reshape(self.bmi.get_value(name), shape),
            coords={
                "longitude": self.bmi.get_grid_y(grid),
                "latitude": self.bmi.get_grid_x(grid),
                "time": num2date(self.bmi.get_current_time(), time_units),
            },
            dims=["latitude", "longitude"],
            name=name,
            attrs={"units": self.bmi.get_var_units(name)},
        )

        return da.where(da != -999)

    @property
    def parameters(self) -> Iterable[Tuple[str, Any]]:
        """List the configurable parameters for this model."""
        # An opiniated list of configurable parameters.
        cfg = self.config
        return [
            (
                "start_time",
                f"{cfg.get('globalOptions', 'startTime')}T00:00:00Z",
            ),
            ("end_time", f"{cfg.get('globalOptions', 'endTime')}T00:00:00Z"),
            ("routing_method", cfg.get("routingOptions", "routingMethod")),
            (
                "max_spinups_in_years",
                cfg.get("globalOptions", "maxSpinUpsInYears"),
            ),
        ]
예제 #9
0
    def setup(  # type: ignore
        self,
        IrrigationEfficiency: str = None,  # noqa: N803
        start_time: str = None,
        end_time: str = None,
        MaskMap: str = None,
        cfg_dir: str = None,
    ) -> Tuple[str, str]:
        """Configure model run.

        1. Creates config file and config directory
           based on the forcing variables and time range.
        2. Start bmi container and store as :py:attr:`bmi`

        Args:
            IrrigationEfficiency: Field application irrigation efficiency.
                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.
            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.
            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.
            cfg_dir: a run directory given by user or created for user.

        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)

        cfg_dir_as_path = _generate_workdir(cfg_dir_as_path)
        config_file = self._create_lisflood_config(
            cfg_dir_as_path,
            start_time,
            end_time,
            IrrigationEfficiency,
            MaskMap,
        )

        assert self.parameter_set is not None
        input_dirs = [str(self.parameter_set.directory), str(self.forcing_dir)]
        if MaskMap is not None:
            mask_map = to_absolute_path(MaskMap)
            try:
                mask_map.relative_to(self.parameter_set.directory)
            except ValueError:
                # If not relative add dir
                input_dirs.append(str(mask_map.parent))

        if CFG["container_engine"].lower() == "singularity":
            image = get_singularity_image(self.version, CFG["singularity_dir"])
            self.bmi = BmiClientSingularity(
                image=str(image),
                input_dirs=input_dirs,
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        elif CFG["container_engine"].lower() == "docker":
            image = get_docker_image(self.version)
            self.bmi = BmiClientDocker(
                image=image,
                image_port=55555,
                input_dirs=input_dirs,
                work_dir=str(cfg_dir_as_path),
                timeout=300,
            )
        else:
            raise ValueError(
                f"Unknown container technology in CFG: {CFG['container_engine']}"
            )
        return str(config_file), str(cfg_dir_as_path)
def walrus_model(tmp_path, walrus_input):
    model = BmiClientSingularity(
        image="docker://ewatercycle/walrus-grpc4bmi:v0.2.0",
        input_dir=str(tmp_path))
    yield model
    del model