Hupselbrook Model Setup¶
SWAP plain-text configuration files (.swp, .crp, .dra, .bbc) consist of various switches (parameters with "SW" prefix), key-value pairs, and tables. These elements are grouped into sections corresponding to specific parts of the model. For example, the meteorological section includes parameters for climatological calculations, while the crop section handles parameters related to, e.g., crop water uptake. You can see the structure of the classic template of SWAP file inputs in the wiki section.
pyswap is built on Object Oriented Programming principle, similar to flopy package for MODFLOW model. Different parts of the model, or model components, are represented by separate classes (GeneralSettings
, Meteorology
, etc.). These components are then stacked together like lego blocks to build a complete model. This approach allows to easily build variants of a model by modifying or replacing individual components without changing the overall structure.
Tip
pySWAP is designed with type hinting in mind. When using a code editor like Visual Studio Code, typing `pyswap.` will show hints for available classes and functions. Similarly, hovering over a class signature will display available parameters, value ranges, and documentation.
from datetime import datetime as dt
import pyswap as psp
General settings¶
In pySWAP, each distinct section is represented as a class, similarily to flopy, a popular Python package running MODFLOW models. Therefore, in a sense, defining a model with pySWAP feels similar to defining it in a classic ASCII template, but gives you more options to work with your models further. Finally, when all necessary objects are defined, you create a Model instance. You can also define an empty Model instance, and add each section to that instance after it's created. Let's set up Metadata
and GeneralSettings
for the model. We will start with an empty model instance.
# starting with an empty model instance
ml = psp.Model()
meta = psp.components.Metadata(
author="John Doe",
institution="University of Somewhere",
email="john.doe@somewhere.com",
project="pySWAP test - hupselbrook",
swap_ver="4.2",
)
simset = psp.components.simsettings.GeneralSettings(
tstart="2002-01-01",
tend="2004-12-31",
extensions=["vap", "blc", "sba", "inc", "csv"],
nprintday=1,
swerror=1,
swscre=0,
swmonth=1,
swyrvar=0,
datefix="31 12",
inlist_csv=[
"rain",
"irrig",
"interc",
"runoff",
"drainage",
"dstor",
"epot",
"eact",
"tpot",
"tact",
"qbottom",
"gwl",
],
)
# attaching model components to the model instance
ml.metadata = meta
ml.generalsettings = simset
Note
At this stage there is one important difference to explain; you do not need to (and actually cannot) adjust the paths at the beginning. This is because pySWAP runs SWAP in a temporary directory and handles paths automatically. The same goes for file names; the default and frozen file names are "swap" for inputs (e.g., drainage file) and "result" for output.
After adding the two sections, you can view how the section would look like as a SWAP-compatible string, or see the current shape of the .swp file by calling ml.swp
property. You may notice more parameters that you wanted to set. It's because by default, heat flow (HeatFlow
), solute transport (SoluteTransport
) and fixed irrigation (FixedIrrigation
) modules are turned off in the model. There are also default settings for some parameters of Richard's equation (RichardsSettings
). To change it, it's enough to define your own objects with desired settings and reassign them in the ml
instance.
print(ml.swp)
PROJECT = 'pySWAP test - hupselbrook' SWWBA = 0 SWEND = 0 SWVAP = 1 SWBAL = 0 SWBLC = 1 SWSBA = 1 SWATE = 0 SWBMA = 0 SWDRF = 0 SWSWB = 0 SWINI = 0 SWINC = 1 SWCRP = 0 SWSTR = 0 SWIRG = 0 SWCSV = 1 SWCSV_TZ = 0 PATHWORK = './' PATHATM = './' PATHCROP = './' PATHDRAIN = './' SWSCRE = 0 SWERROR = 1 TSTART = 2002-01-01 TEND = 2004-12-31 NPRINTDAY = 1 SWMONTH = 1 SWYRVAR = 0 DATEFIX = 31 12 OUTFIL = 'result' SWHEADER = 0 INLIST_CSV = 'rain,irrig,interc,runoff,drainage,dstor,epot,eact,tpot,tact,qbottom,gwl' SWAFO = 0 SWAUN = 0 SWDISCRVERT = 0 SWIRFIX = 0 IRGFIL = 'swap' SWSNOW = 0 SWFROST = 0 SWKMEAN = 1 SWKIMPL = 0 DTMIN = 1e-06 DTMAX = 0.04 GWLCONV = 100.0 CRITDEVH1CP = 0.01 CRITDEVH2CP = 0.1 CRITDEVPONDDT = 0.0001 MAXIT = 30 MAXBACKTR = 3 SWHEA = 0 SWSOLU = 0
Meteorology¶
Setting up meteorological section additionally requires providing some climatological data in a specific CSV format. Those data are enclosed in a File
type object. For meteorological data it's MetFile
class. Let's create the Meteorology
object with a MetFile
attached to it.
Currently the other built-in method of getting the MetFile is using KNMI service or loading it from a CSV file. Read documentation of pyswap.components.meteorology
module.
# here we additionally need to load the meteo data from testcase library
from pyswap import testcase
meteo_data = psp.components.meteorology.metfile_from_csv(
metfil="283.met", csv_path=testcase.get_path("hupselbrook", "met")
)
meteo = psp.components.meteorology.Meteorology(
lat=52.0,
alt=21.0,
swetr=0,
metfile=meteo_data,
swdivide=1,
swmetdetail=0,
altw=10.0,
angstroma=0.25,
angstromb=0.5,
)
ml.meteorology = meteo
Crop¶
Crop settings can be defined either fully in your Python script, or can be partly imported from the WOFOST crop database (depending on which crop file type you are going to use). In the Hupselbrook example, the crop rotation settings includes all three: simple setup for maize, detailed WOFOST model for potato and dynamic grass growth.
Each crop file is set up as Crop
objects which in turn consists of subsections (ScheduledIrrigation
, CropDevelopmentSettingsFixed
, etc). This is identical to how the main Model
is composed.
This section is somewhat long, so buckle up...
Simple (fixed) crop settings for maize¶
maize_prep = psp.components.crop.Preparation(
swprep=0, swsow=0, swgerm=0, dvsend=3.0, swharv=0
)
scheduled_irrigation = psp.components.irrigation.ScheduledIrrigation(schedule=0)
DVS = [0.0, 0.3, 0.5, 0.7, 1.0, 1.4, 2.0]
# This is one way to create and validate tables in pySWAP.
maize_gctb = psp.components.crop.GCTB.create({
"DVS": DVS,
"LAI": [0.05, 0.14, 0.61, 4.10, 5.00, 5.80, 5.20],
})
maize_cftb = psp.components.crop.CFTB.create({
"DVS": DVS,
"CH": [1.0, 15.0, 40.0, 140.0, 170.0, 180.0, 175.0],
})
maize_rdtb = psp.components.crop.RDTB.create({
"DVS": [0.0, 0.3, 0.5, 0.7, 1.0, 2.0],
"RD": [5.0, 20.0, 50.0, 80.0, 90.0, 100.0],
})
maize_rdctb = psp.components.crop.RDCTB.create({
"RRD": [0.0, 1.0],
"RDENS": [1.0, 0.0],
})
maize_cropdev_settings = psp.components.crop.CropDevelopmentSettingsFixed(
idev=1,
lcc=168,
kdif=0.6,
kdir=0.75,
swgc=1,
gctb=maize_gctb,
swcf=2,
cftb=maize_cftb,
albedo=0.23,
rsc=61.0,
rsw=0.0,
swrd=1,
rdtb=maize_rdtb,
rdctb=maize_rdctb,
)
maize_ox_stress = psp.components.crop.OxygenStress(
swoxygen=1,
swwrtnonox=0,
aeratecrit=0.5,
hlim1=-15.0,
hlim2u=-30.0,
hlim2l=-30.0,
)
maize_dr_stress = psp.components.crop.DroughtStress(
swdrought=1,
hlim3h=-325.0,
hlim3l=-600.0,
hlim4=-8000.0,
adcrh=0.5,
adcrl=0.1,
)
# shared with the fixed crop settings
maize_interception = psp.components.crop.Interception(swinter=1, cofab=0.25)
crpmaize = psp.components.crop.CropFile(
name="maizes",
prep=maize_prep,
scheduledirrigation=scheduled_irrigation,
cropdev_settings=maize_cropdev_settings,
oxygenstress=maize_ox_stress,
droughtstress=maize_dr_stress,
interception=maize_interception,
)
WOFOST settings for potato¶
For the WOFOST model, many more parameters are necessary. Therefore we can make use of calibrated parameters for crops from existing databases. pySWAP uses the de Wit's crop database to automatically load some available parameters. The remaining parameters still need to be supplied by the user. We will make use of that database below.
from pyswap import db
# Load the crop database
db = db.WOFOSTCropDB()
potato = db.load_crop_file("potato")
potato_params = potato.get_variety("Potato_701")
potato_prep = psp.components.crop.Preparation(
swprep=0,
swsow=0,
swgerm=2,
tsumemeopt=170.0,
tbasem=3.0,
teffmx=18.0,
hdrygerm=-500.0,
hwetgerm=-100.0,
zgerm=-10.0,
agerm=203.0,
dvsend=2.0,
swharv=0,
)
potato_chtb = psp.components.crop.CFTB.create({
"DVS": [0.0, 1.0, 2.0],
"CH": [
1.0,
40.0,
50.0,
],
})
potato_rdctb = psp.components.crop.RDCTB.create({
"RRD": [0.0, 1.0],
"RDENS": [1.0, 0.0],
})
potato_cropdev_settings = psp.components.crop.CropDevelopmentSettingsWOFOST(
wofost_variety=potato_params,
swcf=2,
cftb=potato_chtb,
albedo=0.19,
laiem=0.0589,
ssa=0.0,
kdif=1.0,
rsc=207.0,
rsw=0.0,
kdir=0.75,
eff=0.45,
swrd=2,
rdc=50.0,
swdmi2rd=1,
rdctb=potato_rdctb,
)
potato_cropdev_settings.update_from_wofost()
potato_ox_stress = psp.components.crop.OxygenStress(
swoxygen=1,
swwrtnonox=1,
aeratecrit=0.5,
hlim1=-10.0,
hlim2u=-25.0,
hlim2l=-25.0,
swrootradius=2,
root_radiuso2=0.00015,
)
potato_dr_stress = psp.components.crop.DroughtStress(
swdrought=1,
hlim3h=-300.0,
hlim3l=-500.0,
hlim4=-10000.0,
adcrh=0.5,
adcrl=0.1,
)
crppotato = psp.components.crop.CropFile(
name="potatod",
prep=potato_prep,
cropdev_settings=potato_cropdev_settings,
oxygenstress=potato_ox_stress,
droughtstress=potato_dr_stress,
# shared with the fixed crop settings
interception=maize_interception,
scheduledirrigation=scheduled_irrigation,
)
Dynamic grass model¶
This one requires many parameters that are unavailable in the crop database pySWAP uses. Therefore, all tables and parameters have to be defined manually.
grass_chtb = psp.components.crop.CFTB.create({
"DNR": [0.0, 180.0, 366.0],
"CH": [12.0, 12.0, 12.0],
})
grass_slatb = psp.components.crop.SLATB.create({
"DNR": [1.00, 80.00, 300.00, 366.00],
"SLA": [0.0015, 0.0015, 0.0020, 0.0020],
})
amaxtb = psp.components.crop.AMAXTB.create({
"DNR": [1.00, 95.00, 200.00, 275.00, 366.00],
"AMAX": [40.00, 40.00, 35.00, 25.00, 25.00],
})
grass_tmpftb = psp.components.crop.TMPFTB.create({
"TAVD": [0.00, 5.00, 15.00, 25.00, 40.00],
"TMPF": [0.00, 0.70, 1.00, 1.00, 0.00],
})
grass_tmnftb = psp.components.crop.TMNFTB.create({
"TMNR": [0.0, 4.0],
"TMNF": [0.0, 1.0],
})
grass_rfsetb = psp.components.crop.RFSETB.create({
"DNR": [1.00, 366.00],
"RFSE": [1.0000, 1.0000],
})
grass_frtb = psp.components.crop.FRTB.create({
"DNR": [1.00, 366.00],
"FR": [0.3000, 0.3000],
})
grass_fltb = psp.components.crop.FLTB.create({
"DNR": [1.00, 366.00],
"FL": [0.6000, 0.6000],
})
grass_fstb = psp.components.crop.FSTB.create({
"DNR": [1.00, 366.00],
"FS": [0.4000, 0.4000],
})
grass_rdrrtb = psp.components.crop.RDRRTB.create({
"DNR": [1.0, 180.0, 366.0],
"RDRR": [0.0, 0.02, 0.02],
})
grass_rdrstb = psp.components.crop.RDRSTB.create({
"DNR": [1.0, 180.0, 366.0],
"RDRS": [0.0, 0.02, 0.02],
})
grass_rlwtb = psp.components.crop.RLWTB.create({
"RW": [300.00, 2500.00],
"RL": [20.0, 40.0],
})
grass_rdctb = psp.components.crop.RDCTB.create({
"RRD": [0.0, 1.0],
"RDENS": [1.0, 0.0],
})
grass_settings = psp.components.crop.CropDevelopmentSettingsGrass(
swcf=2,
cftb=grass_chtb,
albedo=0.23,
rsc=100.0,
rsw=0.0,
tdwi=1000.00,
laiem=0.63000,
rgrlai=0.00700,
swtsum=1,
ssa=0.0004,
span=30.00,
tbase=0.00,
slatb=grass_slatb,
kdif=0.60,
kdir=0.75,
eff=0.50,
amaxtb=amaxtb,
tmpftb=grass_tmpftb,
tmnftb=grass_tmnftb,
cvl=0.6850,
cvr=0.6940,
cvs=0.6620,
q10=2.0000,
rml=0.0300,
rmr=0.0150,
rms=0.0150,
rfsetb=grass_rfsetb,
frtb=grass_frtb,
fltb=grass_fltb,
fstb=grass_fstb,
perdl=0.050,
rdrrtb=grass_rdrrtb,
rdrstb=grass_rdrstb,
swrd=3,
swdmi2rd=1,
rlwtb=grass_rlwtb,
wrtmax=3000.0,
swrdc=0,
rdctb=grass_rdctb,
)
grass_ox_stress = psp.components.crop.OxygenStress(
swoxygen=1, hlim1=0.0, hlim2u=1.0, hlim2l=-1.0, swwrtnonox=0
)
grass_drought_stress = psp.components.crop.DroughtStress(
swdrought=1,
swjarvis=4,
alphcrit=0.7,
hlim3h=-200.0,
hlim3l=-800.0,
hlim4=-8000.0,
adcrh=0.5,
adcrl=0.1,
)
grass_salt_stress = psp.components.crop.SaltStress(swsalinity=0)
grass_interception = psp.components.crop.Interception(swinter=1, cofab=0.25)
grass_co2 = psp.components.crop.CO2Correction(swco2=0)
grass_dmmowtb = psp.components.crop.DMMOWTB.create({
"DNR": [120.0, 152.0, 182.0, 213.0, 366.0],
"DMMOW": [4700.0, 3700.0, 3200.0, 2700.0, 2700.0],
})
grass_dmmowdelay = psp.components.crop.DMMOWDELAY.create({
"DMMOWDELAY": [0.0, 2000.0, 4000.0],
"DAYDELAY": [2, 3, 4],
})
grass_management = psp.components.crop.GrasslandManagement(
seqgrazmow=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
swharvest=1,
swdmmow=2,
dmmowtb=grass_dmmowtb,
maxdaymow=42,
swlossmow=0,
mowrest=700.0,
dmmowdelay=grass_dmmowdelay,
swpotrelmf=1,
relmf=0.90,
)
grass_irrigation = psp.components.irrigation.ScheduledIrrigation(schedule=0)
crpgrass = psp.components.crop.CropFile(
name="grassd",
cropdev_settings=grass_settings,
oxygenstress=grass_ox_stress,
droughtstress=grass_drought_stress,
saltstress=grass_salt_stress,
interception=grass_interception,
co2correction=grass_co2,
grasslandmanagement=grass_management,
scheduledirrigation=grass_irrigation,
)
Main Crop object¶
the three CropFile objects defined above will be converted by pySWAP to .crp files and used in the simulation. They need to be added to the main Crop object defining the basic settings for crop simulation inside .swp model.
croprotation = psp.components.crop.CROPROTATION.create({
"CROPSTART": [dt(2002, 5, 1), dt(2003, 5, 10), dt(2004, 1, 1)],
"CROPEND": [dt(2002, 10, 15), dt(2003, 9, 29), dt(2004, 12, 31)],
"CROPFIL": ["'maizes'", "'potatod'", "'grassd'"],
"CROPTYPE": [1, 2, 3],
})
crop = psp.components.crop.Crop(
swcrop=1,
rds=200.0,
croprotation=croprotation,
cropfiles={"maizes": crpmaize, "potatod": crppotato, "grassd": crpgrass},
)
ml.crop = crop
Irrigation¶
The irrigation section is relatively short, and if the fixed irrigation application is used, a dataframe of the irrigation events will be necessary.
irrig_events = psp.components.irrigation.IRRIGEVENTS.create({
"IRDATE": ["2002-01-05"],
"IRDEPTH": [5.0],
"IRCONC": [1000.0],
"IRTYPE": [1],
})
fixed_irrigation = psp.components.irrigation.FixedIrrigation(
swirfix=1, swirgfil=0, irrigevents=irrig_events
)
ml.fixedirrigation = fixed_irrigation
Soil-water parameters section¶
This section is defining the soil water interaction parameters.
Soil moisture¶
soilmoisture = psp.components.soilwater.SoilMoisture(swinco=2, gwli=-75.0)
ml.soilmoisture = soilmoisture
Surface flow¶
surfaceflow = psp.components.soilwater.SurfaceFlow(
swpondmx=0, pondmx=0.2, rsro=0.5, rsroexp=1.0, swrunon=0
)
ml.surfaceflow = surfaceflow
Evaporation¶
evaporation = psp.components.soilwater.Evaporation(
cfevappond=1.25, swcfbs=0, rsoil=30.0, swredu=1, cofredbl=0.35, rsigni=0.5
)
ml.evaporation = evaporation
Soil profile¶
soil_profile = psp.components.soilwater.SOILPROFILE.create({
"ISUBLAY": [1, 2, 3, 4],
"ISOILLAY": [1, 1, 2, 2],
"HSUBLAY": [10.0, 20.0, 30.0, 140.0],
"HCOMP": [1.0, 5.0, 5.0, 10.0],
"NCOMP": [10, 4, 6, 14],
})
soil_hydraulic_functions = psp.components.soilwater.SOILHYDRFUNC.create({
"ORES": [0.01, 0.02],
"OSAT": [0.42, 0.38],
"ALFA": [0.0276, 0.0213],
"NPAR": [1.491, 1.951],
"KSATFIT": [12.52, 12.68],
"LEXP": [-1.060, 0.168],
"ALFAW": [0.0542, 0.0426],
"H_ENPR": [0.0, 0.0],
"KSATEXM": [12.52, 12.68],
"BDENS": [1315.0, 1315.0],
})
soilprofile = psp.components.soilwater.SoilProfile(
swsophy=0,
soilprofile=soil_profile,
swhyst=0,
tau=0.2,
soilhydrfunc=soil_hydraulic_functions,
swmacro=0,
)
ml.soilprofile = soilprofile
Drainage¶
dra = psp.components.drainage.DraFile(
dramet=2,
swdivd=1,
cofani=[1.0, 1.0],
swdislay=0,
lm2=11.0,
shape=0.8,
wetper=30.0,
zbotdr=-80.0,
entres=20.0,
ipos=2,
basegw=-200.0,
khtop=25.0,
)
drainage = psp.components.drainage.Drainage(swdra=1, drafile=dra)
ml.lateraldrainage = drainage
Bottom boundary conditions¶
bottom_boundary = psp.components.boundary.BottomBoundary(swbbcfile=0, swbotb=6)
ml.bottomboundary = bottom_boundary
result = ml.run()
Warning from module Readswap : simulation with additonal Ksat value (Ksatexm)
print(result.yearly_summary)
RAIN IRRIG INTERC RUNOFF EPOT EACT DRAINAGE \ DATETIME 2002-12-31 84.18 0.5 3.74188 0.0 33.11621 16.68886 22.11524 2003-12-31 71.98 0.0 2.05788 0.0 36.00281 17.18128 26.44559 2004-12-31 80.55 0.0 4.91521 0.0 29.90119 17.89115 24.75732 QBOTTOM GWL TPOT TACT DSTOR DATETIME 2002-12-31 0.0 -1107.59506 38.70771 38.16895 3.96511 2003-12-31 0.0 -1154.50523 29.42375 29.21989 -2.92464 2004-12-31 0.0 -1036.91954 32.57631 32.57291 0.41342