Model time discretization#

iMOD Python provides nice functionality to discretize your models into stress periods, depending on the timesteps you assigned your boundary conditions. This functionality is activated with the create_time_discretization() method.

Basics#

To demonstrate the create_time_discretization() method, we first have to create a Model object. In this case weโ€™ll use a MODFLOW 6 simulation, but the imod.wq.SeawatModel also supports this.

Note

The simulation created in this example misses some mandatory packes, so is not able to write a functional simulation. It is purely intended to describe the workings of the time_discretization method. For a fully functional MODFLOW 6 model, see the examples in MODFLOW 6.

Welโ€™ll start off with the usual imports:

import numpy as np
import pandas as pd
import xarray as xr

import imod

We can discretize a simulation as follows, this creates a imod.mf6.TimeDiscretization object under the key "time_discretization".

simulation = imod.mf6.Modflow6Simulation("example")
simulation.create_time_discretization(
    additional_times=["2000-01-01", "2000-01-02", "2000-01-04"]
)

simulation["time_discretization"]
TimeDiscretization
<xarray.Dataset> Size: 48B
Dimensions:              (time: 2)
Coordinates:
  * time                 (time) datetime64[ns] 16B 2000-01-01 2000-01-02
Data variables:
    timestep_duration    (time) float64 16B 1.0 2.0
    n_timesteps          int64 8B 1
    timestep_multiplier  float64 8B 1.0


To view the data inside imod.mf6.TimeDiscretization object:

simulation["time_discretization"].dataset
<xarray.Dataset> Size: 48B
Dimensions:              (time: 2)
Coordinates:
  * time                 (time) datetime64[ns] 16B 2000-01-01 2000-01-02
Data variables:
    timestep_duration    (time) float64 16B 1.0 2.0
    n_timesteps          int64 8B 1
    timestep_multiplier  float64 8B 1.0


Notice that even though we specified three points in time, only two timesteps are included in the time coordinate, this is because Modflow requires a start time and a duration of each stress period. iMOD Python therefore uses three points in time to compute two stress periods with a duration.

simulation["time_discretization"].dataset["timestep_duration"]
<xarray.DataArray 'timestep_duration' (time: 2)> Size: 16B
array([1., 2.])
Coordinates:
  * time     (time) datetime64[ns] 16B 2000-01-01 2000-01-02


These two stress periods use their respective start time in their time coordinate.

Boundary Conditions#

The create_time_discretization method becomes especially useful if we add boundary conditions to our groundwater model. Weโ€™ll first still have to initialize a groundwater flow model though:

gwf_model = imod.mf6.GroundwaterFlowModel()

Weโ€™ll create a dummy grid, which is used to provide an appropriate grid to the boundary condition packages.

template_data = np.ones((2, 1, 2, 2))

layer = [1]
y = [1.5, 0.5]
x = [0.5, 1.5]

dims = ("time", "layer", "y", "x")

Next, we can assign a Constant Head boundary with two timesteps:

chd_times = pd.to_datetime(["2000-01-01", "2000-01-04"])
chd_data = xr.DataArray(
    data=template_data,
    dims=dims,
    coords={"time": chd_times, "layer": layer, "y": y, "x": x},
)

gwf_model["chd"] = imod.mf6.ConstantHead(head=chd_data)

gwf_model["chd"].dataset
<xarray.Dataset> Size: 139B
Dimensions:        (time: 2, layer: 1, y: 2, x: 2)
Coordinates:
  * time           (time) datetime64[us] 16B 2000-01-01 2000-01-04
  * layer          (layer) int64 8B 1
  * y              (y) float64 16B 1.5 0.5
  * x              (x) float64 16B 0.5 1.5
Data variables:
    head           (time, layer, y, x) float64 64B 1.0 1.0 1.0 ... 1.0 1.0 1.0
    print_input    bool 1B False
    print_flows    bool 1B False
    save_flows     bool 1B False
    observations   object 8B None
    repeat_stress  object 8B None


Weโ€™ll also assign a Recharge boundary with two timesteps, which differ from the ConstantHead boundary:

rch_times = pd.to_datetime(["2000-01-01", "2000-01-02"])
rch_data = xr.DataArray(
    data=template_data,
    dims=dims,
    coords={"time": rch_times, "layer": layer, "y": y, "x": x},
)

gwf_model["rch"] = imod.mf6.Recharge(rate=rch_data)

gwf_model["rch"].dataset
<xarray.Dataset> Size: 140B
Dimensions:        (time: 2, layer: 1, y: 2, x: 2)
Coordinates:
  * time           (time) datetime64[us] 16B 2000-01-01 2000-01-02
  * layer          (layer) int64 8B 1
  * y              (y) float64 16B 1.5 0.5
  * x              (x) float64 16B 0.5 1.5
Data variables:
    rate           (time, layer, y, x) float64 64B 1.0 1.0 1.0 ... 1.0 1.0 1.0
    print_input    bool 1B False
    print_flows    bool 1B False
    save_flows     bool 1B False
    observations   object 8B None
    repeat_stress  object 8B None
    fixed_cell     bool 1B False


We can now let iMOD Python figure out how the simulationโ€™s time should be discretized. It is important that we provide an endtime, otherwise the duration of the last stress period cannot be determined:

endtime = pd.to_datetime(["2000-01-06"])

simulation_bc = imod.mf6.Modflow6Simulation("example_bc")
simulation_bc["gwf_1"] = gwf_model

simulation_bc.create_time_discretization(additional_times=endtime)

simulation_bc["time_discretization"].dataset
<xarray.Dataset> Size: 64B
Dimensions:              (time: 3)
Coordinates:
  * time                 (time) datetime64[ns] 24B 2000-01-01 ... 2000-01-04
Data variables:
    timestep_duration    (time) float64 24B 1.0 2.0 2.0
    n_timesteps          int64 8B 1
    timestep_multiplier  float64 8B 1.0


Notice that iMOD Python figured out that the two boundary conditions, both with two timesteps, should lead to three stress periods!

Extra settings#

The imod.mf6.TimeDiscretization package also supports other settings, like the amount of timesteps which Modflow should use within a stress period, as well as a timestep multiplier, to gradually increase the timesteps modflow uses within a stress period. This can be useful when boundary conditions change very abruptly between stress periods. These settings are set by accessing their respective variables in the dataset.

times = simulation_bc["time_discretization"].dataset.coords["time"]

simulation_bc["time_discretization"].dataset["timestep_multiplier"] = 1.5
simulation_bc["time_discretization"].dataset["n_timesteps"] = xr.DataArray(
    data=[2, 4, 4], dims=("time",), coords={"time": times}
)

simulation_bc["time_discretization"].dataset
<xarray.Dataset> Size: 80B
Dimensions:              (time: 3)
Coordinates:
  * time                 (time) datetime64[ns] 24B 2000-01-01 ... 2000-01-04
Data variables:
    timestep_duration    (time) float64 24B 1.0 2.0 2.0
    n_timesteps          (time) int64 24B 2 4 4
    timestep_multiplier  float64 8B 1.5


Adaptive Time Stepping#

The imod.mf6.AdaptiveTimeStepping package can be used to let Modflow decide the length of each timestep within a stress period. This can be useful when boundary conditions change abruptly, or when a model is highly dynamic. The imod.mf6.AdaptiveTimeStepping package can be added to a imod.mf6.Modflow6Simulation as follows:

simulation_bc["ats"] = imod.mf6.AdaptiveTimeStepping(
    dt_init=1e-4, dt_min=1e-4, dt_max=2.0, dt_multiplier=2.0, dt_fail_divisor=5.0
)

simulation_bc["ats"]
AdaptiveTimeStepping
<xarray.Dataset> Size: 40B
Dimensions:          ()
Data variables:
    dt_init          float64 8B 0.0001
    dt_min           float64 8B 0.0001
    dt_max           float64 8B 2.0
    dt_multiplier    float64 8B 2.0
    dt_fail_divisor  float64 8B 5.0


The dt_init parameter sets the initial timestep length, dt_min and dt_max set the minimum and maximum timestep length allowed, dt_multiplier multiplies the timestep length for each consecutive timestep if the model converges well, and dt_fail_divisor decreases the timestep length if the model has convergence problems.

There is one further way to influence the timestep length, which is to set the ats_percel parameter in the imod.mf6.AdvectionTVD package of a transport model. This parameter sets the maximum fraction of a cell that a parcel of solute is allowed to travel in a single timestep. This can be used to ensure that the timestep is small enough to get accurate transport results. Once this is set, MODFLOW 6 will choose the smallest necessary timestep based on the convergence criteria and the ats_percel criteria.

transport_model = imod.mf6.GroundwaterTransportModel()
transport_model["adv"] = imod.mf6.AdvectionTVD(ats_percel=0.95)
simulation_bc["gwt_1"] = transport_model

transport_model["adv"]
AdvectionTVD
<xarray.Dataset> Size: 20B
Dimensions:     ()
Data variables:
    scheme      <U3 12B 'TVD'
    ats_percel  float64 8B 0.95


Conclusion#

The imod.mf6.Modflow6Simulation.create_time_discretization() method can be used to easily generate stress periods for a simulation, based on the timesteps of the boundary conditions assigned to the model. The imod.mf6.AdaptiveTimeStepping package can be used to let MODFLOW 6 decide the length of each timestep within a stress period, based on convergence criteria and, in case of using the ats_percel options, the velocity field.

Total running time of the script: (0 minutes 0.083 seconds)

Gallery generated by Sphinx-Gallery