Deltares / imod-python

🐍🧰 Make massive MODFLOW models
https://deltares.github.io/imod-python/
MIT License
17 stars 3 forks source link

Create ``Modflow6Simulation.from_imod5_data`` method #1041

Closed JoerivanEngelen closed 4 months ago

JoerivanEngelen commented 4 months ago

We can use this to have a test if the methods created now for the first MODFLOW6 packages result in a working model. Packages which do not have a from_imod5_data implementation can be skipped, easiest probably to make the mf6.Package object temporarily have a mf6.Package.from_imod5_data method to just pass for packages without an implementation.

UPDATE

We currently have different settings we want to be configurable:

Following the discussion here https://github.com/Deltares/imod-python/issues/960:

When regridding a simulation loaded with from_file, this can have the following API to regrid with a custom setting:

 mf6sim_coarse = Modflow6Simulation.from_file("coarse_sim.toml")
rch_coarse = mf6sim_coarse["gwf"].pop("rch")
mf6sim = mf6sim_coarse.regrid_like(...)
rch_regrid_method = RechargeRegridMethod(rate=(RegridderType.OVERLAP, "maximum"))
rch = rch_coarse.regrid_like(..., method=rch_regrid_method )
mf6sim["gwf"]["rch"] = rch

Important note here is that that the different variables which make up a package are on a consistent grid, otherwise they wouldn't be assigned properly to a dataset. This is not necessarily the case with iMOD5 data.

API proposal

We got comments from users that they wouldn't like default options, to force conscious decisions. I think for regridding we can still set default methods. But for planar boundary conditions where allocating and distributing conductances over model layers is required, we need to force users to make a decision. If I'm not mistaken, the iMOD5 settings for allocating cells and distributing conductances are global, and thus used for all packages, given that there is only one setting for each in the RUNFILE function of iMOD Batch.

imod5_data = imod.formats.prj.open_projectfile_data("imod5_model.prj")
# This does the regridding to get the consistent datasets, 
# allocating and distributing settings are required arguments
mf6sim = Modflow6Simulation.from_imod5_data(imod5_data, sim_allocation_settings, sim_distribution_settings)
# Get required parameters
target_grid = mf6sim["gwf"]["dis"].dataset["idomain"]
dis_pkg = mf6sim["gwf"]["dis"]
npf_pkg = mf6sim["gwf"]["npf"]
# Say we want a different regridding method
riv_regrid_method = RiverRegridMethod(stage=(RegridderType.OVERLAP, "maximum"))
mf6sim["gwf"]["riv-sys1"] = River.from_imod5_data(imod5_data, "riv-sys1", target_grid, dis_pkg, npf_pkg, sim_allocation_settings, sim_distribution_settings, regrid_method=riv_regrid_method)
# Say we want to allocate and distribute conductances of riv-sys2 with a different method.
mf6sim["gwf"]["riv-sys2"] = River.from_imod5_data(imod5_data, "riv-sys2", target_grid, dis_pkg, npf_pkg, riv_allocation_settings, riv_distribution_settings)

+ Users have to explicitly set allocation/distribution options, forced to choose and read about its effects + Allows the flexibility to choose different methods - Not easy to pop iMOD5 data for certain packages (e.g. "DIS), as iMOD5 deems certain variables as separate packages (TOP, BOT, IBOUND), therefore packages will be converted twice.

The method can look something like:

class GroundwaterFlowModel():
...
    def from_imod5_data(self, imod5_data, allocation_settings, distribution_settings):
        target_grid = self.get_smallest_target_grid(imod5_data)

        gwf = {}
        for key, pkg_class in PACKAGE_MAPPING:
            gwf[key] = pkg_class.from_imod5_data(imod5_data, target_grid)

        package_keys = set(gwf.keys())
        boundary_condition_keys = set(imod5_data.keys()) - package_keys

        dis_pkg = gwf["dis"]
        npf_pkg = gwf["npf"]

        for key in boundary_condition_keys:
            bc_class = self._get_bc_class_from_key(key)
            gwf[key] = bc_class.from_imod5_data(imod5_data, key, target_grid, dis_pkg, npf_pkg, allocation_settings, distribution_settings)

        self.mask_all_packages()
...

We can create default allocation options for simulations as follows, with a pydantic Dataclass, similar to this PR:

@dataclass
class DefaultSimulationAllocationOptions:
      drn: ALLOCATION_OPTION = ALLOCATION_OPTION.first_active_to_elevation
      riv: ALLOCATION_OPTION = ALLOCATION_OPTION.stage_to_elevation

We could expose this as public API so that users can change default settings for all package of a certain type. If they want to allocate a specific package, they have to manually load this.

sim_allocation_settings = DefaultSimulationAllocationOptions(riv=ALLOCATION_OPTION.at_elevation)
mf6sim = Modflow6Simulation.from_imod5_data(imod5_data, sim_allocation_settings, sim_distribution_settings)
mf6sim["gwf"]["riv-sys1"] = River.from_imod5_data(
    imod5_data, "riv-sys1", target_grid, dis_pkg, npf_pkg, allocation_option = ALLOCATION_OPTION.stage_to_elevation
)
luitjansl commented 4 months ago

merged into feature branch