"""Interface to APSIM simulation models using Python.NET."""fromtypingimportUnionimportpythonnet# Prefer dotnettry:ifpythonnet.get_runtime_info()isNone:pythonnet.load("coreclr")except:print("dotnet not found loading alternate runtime")print("Using: pythonnet.get_runtime_info()")pythonnet.load()importclrimportsysimportnumpyasnpimportpandasaspdimportshutilimportosimportpathlibimportshutilimportdatetimeimportwarnings# Try to load from pythonpath and only then look for Model.exetry:clr.AddReference("Models")except:print("Looking for APSIM")apsim_path=shutil.which("Models")ifapsim_pathisnotNone:apsim_path=os.path.split(os.path.realpath(apsim_path))[0]sys.path.append(apsim_path)clr.AddReference("Models")clr.AddReference("System")# C# importsimportModelsimportModels.CoreimportModels.Core.ApsimFileimportModels.Core.Run;importModels.PMFimportSystem.IOimportSystem.LinqfromSystem.Collections.Genericimport*fromSystemimport*#from Models.Core import Zone, SimulationsfromModels.PMFimportCultivarfromModels.Core.ApsimFileimportFileFormatfromModels.ClimateimportWeatherfromModels.SoilsimportSoil,Physical,SoilCropfromModels.AgPastureimportPastureSpeciesfromModels.CoreimportSimulations
[docs]classAPSIMX():"""Modify and run Apsim next generation simulation models."""
[docs]def__init__(self,model:Union[str,Simulations],copy=True,out_path=None):""" Parameters ---------- model Path to .apsimx file or C# Models.Core.Simulations object copy, optional If `True` a copy of original simulation will be created on init, by default True. out_path, optional Path of modified simulation, if `None` will be set automatically. """self.results=None#: Simulation results as dataframeself.Model=None#TopLevel Simulations object#self.simulations = None # List of Simulation object#self.py_simulations = Noneself.datastore=Noneself.harvest_date=Noneiftype(model)==str:apsimx_file=modelname,ext=os.path.splitext(apsimx_file)ifcopy:ifout_pathisNone:copy_path=f"{name}_py{ext}"else:copy_path=out_pathshutil.copy(apsimx_file,copy_path)pathlib.Path(f"{name}.db").unlink(missing_ok=True)pathlib.Path(f"{name}.db-shm").unlink(missing_ok=True)pathlib.Path(f"{name}.db-wal").unlink(missing_ok=True)self.path=copy_pathelse:self.path=apsimx_fileself._load(self.path)eliftype(model)==Simulations:self.Model=modelself.datastore=self.Model.FindChild[Models.Storage.DataStore]().FileNameself._DataStore=self.Model.FindChild[Models.Storage.DataStore]()plant=self.Model.FindDescendant[Models.Core.Zone]().Plants[0]cultivar=plant.FindChild[Cultivar]()# TODO fix this to work with the sown cultivar and# accept simulation name as argumenttry:self.cultivar_command=self._cultivar_params(cultivar)except:pass
@propertydefsimulations(self):returnlist(self.Model.FindAllChildren[Models.Core.Simulation]())def_load(self,path):# When the last argument (init in another thread) is False,# models with errors fail to load. More elegant solution would be handle# errors like the GUI does.# If loading fails the the model has errors -> Use ApsimNG user interface to debugself.Model=FileFormat.ReadFromFile[Models.Core.Simulations](path,None,False)# This is needed for APSIM ~5/2023, hacky attempt to also support old version# TODO catch errors etc.try:self.Model=self.Model.get_NewModel()except:pass#self.simulations = list(self._Simulation.FindAllChildren[Models.Core.Simulation]())#self.py_simulations = [Simulation(s) for s in self.simulations]self.datastore=self.Model.FindChild[Models.Storage.DataStore]().FileNameself._DataStore=self.Model.FindChild[Models.Storage.DataStore]()def_reload(self):self.save()self._load(self.path)defsave(self,out_path=None):"""Save the model Parameters ---------- out_path, optional Path of output .apsimx file, by default `None` """ifout_pathisNone:out_path=self.pathjson=Models.Core.ApsimFile.FileFormat.WriteToString(self.Model)withopen(out_path,"w")asf:f.write(json)defrun(self,simulations=None,clean=True,multithread=True):"""Run simulations Parameters ---------- simulations, optional List of simulation names to run, if `None` runs all simulations, by default `None`. clean, optional If `True` remove existing database for the file before running, by default `True` multithread, optional If `True` APSIM uses multiple threads, by default `True` """ifmultithread:runtype=Models.Core.Run.Runner.RunTypeEnum.MultiThreadedelse:runtype=Models.Core.Run.Runner.RunTypeEnum.SingleThreaded# Clear old data before runningself.results=Noneifclean:self._DataStore.Dispose()pathlib.Path(self._DataStore.FileName).unlink(missing_ok=True)pathlib.Path(self._DataStore.FileName+"-wal").unlink(missing_ok=True)pathlib.Path(self._DataStore.FileName+"-shm").unlink(missing_ok=True)self._DataStore.Open()ifsimulationsisNone:r=Models.Core.Run.Runner(self.Model,True,False,False,None,runtype)else:sims=self.find_simulations(simulations)# Runner needs C# listcs_sims=List[Models.Core.Simulation]()forsinsims:cs_sims.Add(s)r=Models.Core.Run.Runner(cs_sims,True,False,False,None,runtype)e=r.Run()if(len(e)>0):print(e[0].ToString())self.results=self._read_results()try:self.harvest_date=self.results.loc[self.results.WheatPhenologyCurrentStageName=='HarvestRipe',["Zone","ClockToday"]]except:self.harvest_date=Nonedefclone_simulation(self,target,simulation=None):"""Clone a simulation and add it to Model Parameters ---------- target target simulation name simulation, optional Simulation name to be cloned, of None clone the first simulation in model """sim=self._find_simulation(simulation)clone_sim=Models.Core.Apsim.Clone(sim)clone_sim.Name=target#clone_zone = clone_sim.FindChild[Models.Core.Zone]()#clone_zone.Name = targetself.Model.Children.Add(clone_sim)self._reload()defremove_simulation(self,simulation):"""Remove a simulation from the model Parameters ---------- simulation The name of the simulation to remove """sim=self._find_simulation(simulation)self.Model.Children.Remove(sim)self.save()self._load(self.path)defclone_zone(self,target,zone,simulation=None):"""Clone a zone and add it to Model Parameters ---------- target target simulation name zone Name of the zone to clone simulation, optional Simulation name to be cloned, of None clone the first simulation in model """sim=self._find_simulation(simulation)zone=sim.FindChild[Models.Core.Zone](zone)clone_zone=Models.Core.Apsim.Clone(zone)clone_zone.Name=targetsim.Children.Add(clone_zone)self.save()self._load(self.path)deffind_zones(self,simulation):"""Find zones from a simulation Parameters ---------- simulation simulation name Returns ------- list of zones as APSIM Models.Core.Zone objects """sim=self._find_simulation(simulation)zones=sim.FindAllDescendants[Models.Core.Zone]()returnlist(zones)def_read_results(self):#df = pd.read_sql_table("Report", "sqlite:///" + self.datastore) # errors with datetime since 5/2023df=pd.read_sql_query("select * from Report","sqlite:///"+self.datastore)df=df.rename(mapper=lambdax:x.replace(".",""),axis=1)try:# ClockToday has . delimiters on Macdf["ClockToday"]=[datetime.datetime.strptime(t.replace(".",":"),"%Y-%m-%d %H:%M:%S")fortindf.ClockToday]except:warnings.warn("Unable to parse time format, 'ClockToday' column is still a string")returndf"""Convert cultivar command to dict"""def_cultivar_params(self,cultivar):cmd=cultivar.Commandparams={}forcincmd:ifc:p,v=c.split("=")params[p.strip()]=v.strip()returnparamsdefupdate_cultivar(self,parameters,simulations=None,clear=False):"""Update cultivar parameters Parameters ---------- parameters Parameter = value dictionary of cultivar paramaters to update. simulations, optional List of simulation names to update, if `None` update all simulations. clear, optional If `True` remove all existing parameters, by default `False`. """forsiminself.find_simulations(simulations):zone=sim.FindChild[Models.Core.Zone]()cultivar=zone.Plants[0].FindChild[Models.PMF.Cultivar]()ifclear:params=parameterselse:params=self._cultivar_params(cultivar)params.update(parameters)cultivar.Command=[f"{k}={v}"fork,vinparams.items()]self.cultivar_command=paramsdefprint_cultivar(self,simulation=None):"""Print current cultivar parameters, can be copied to APSIM user interface Parameters ---------- simulation, optional Simulation name to be cloned, of None clone the first simulation in model """sim=self._find_simulation(simulation)zone=sim.FindChild[Models.Core.Zone]()cultivar=zone.Plants[0].FindChild[Models.PMF.Cultivar]()print('\n'.join(list(cultivar.Command)))defget_default_phenological_parameters(self,simulation=None):""" Return all default parameters for a PMF crop in the simulation Parameters ---------- simulation, optional Simulation name to be cloned, of None clone the first simulation in model Returns ------- dictionary of parameters with default values """sim=self._find_simulation(simulation)phenology=sim.FindDescendant[Models.PMF.Phen.Phenology]()targets={}forchinphenology.FindAllDescendants[Models.Functions.Constant]():pth=ch.FullPath.split("Phenology.")[1]targets[f"[Phenology].{pth}"]=ch.Value()returntargetsdefshow_management(self,simulations=None):"""Show management Parameters ---------- simulations, optional List of simulation names to update, if `None` show all simulations. """forsiminself.find_simulations(simulations):zone=sim.FindChild[Models.Core.Zone]()print("Zone:",zone.Name)foractioninzone.FindAllChildren[Models.Manager]():print("\t",action.Name,":")forparaminaction.Parameters:print("\t\t",param.Key,":",param.Value)defupdate_management(self,management,simulations=None,reload=True):"""Update management Parameters ---------- management Parameter = value dictionary of management paramaters to update. Call `show_management` to see current values. simulations, optional List of simulation names to update, if `None` update all simulations. reload, optional _description_, by default True """forsiminself.find_simulations(simulations):zone=sim.FindChild[Models.Core.Zone]()foractioninzone.FindAllChildren[Models.Manager]():ifaction.Nameinmanagement:#print("Updating", action.Name)values=management[action.Name]foriinrange(len(action.Parameters)):param=action.Parameters[i].Keyifparaminvalues:action.Parameters[i]=KeyValuePair[String,String](param,f"{values[param]}")# Saved and restored the model to recompile the scripts# haven't figured out another way to make it workifreload:self.save()self._load(self.path)# Convert CS KeyValuePair to dictionarydef_kvtodict(self,kv):return{kv[i].Key:kv[i].Valueforiinrange(kv.Count)}defget_management(self):"""Get management of all simulations as dataframe"""res=[]forsiminself.simulations:actions=sim.FindAllDescendants[Models.Manager]()out={}out["simulation"]=sim.Nameforactioninactions:params=self._kvtodict(action.Parameters)if"FertiliserType"inparams:out[params["FertiliserType"]]=float(params["Amount"])if"CultivarName"inparams:out["crop"]=params["Crop"]out["cultivar"]=params["CultivarName"]out["plant_population"]=params["Population"]iflen(out)>1:res.append(out)returnpd.DataFrame(res)defget_agpasture_crops(self,simulations=None):"""Get AgPasture crops from simulations. Parameters ---------- start_date, optional Start date as string, by default `None` end_date, optional End date as string, by default `None` simulations, optional List of simulation names to update, if `None` get from all simulations Returns ---- List of PastureSpecies (C# class exposed trough pythonnet) """species=[]forsiminself.find_simulations(simulations):species+=sim.FindAllDescendants[PastureSpecies]()returnspeciesdefset_dates(self,start_date=None,end_date=None,simulations=None):"""Set simulation dates Parameters ---------- start_date, optional Start date as string, by default `None` end_date, optional End date as string, by default `None` simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):clock=sim.FindChild[Models.IClock]()ifstart_dateisnotNone:#clock.End = DateTime(start_time.year, start_time.month, start_time.day, 0, 0, 0)clock.StartDate=DateTime.Parse(start_date)ifend_dateisnotNone:#clock.End = DateTime(end_time.year, end_time.month, end_time.day, 0, 0, 0)clock.EndDate=DateTime.Parse(end_date)defget_dates(self,simulations=None):"""Get simulation dates Parameters ---------- simulations, optional List of simulation names to get, if `None` get all simulations Returns ------- Dictionary of simulation names with dates """dates={}forsiminself.find_simulations(simulations):clock=sim.FindChild[Models.IClock]()st=clock.StartDateet=clock.EndDatedates[sim.Name]={}dates[sim.Name]["start"]=datetime.date(st.Year,st.Month,st.Day)dates[sim.Name]["end"]=datetime.date(et.Year,et.Month,et.Day)returndatesdefset_weather(self,weather_file,simulations=None):"""Set simulation weather file Parameters ---------- weather_file Weather file name, path should be relative to simulation or absolute. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):weathers=sim.FindAllDescendants[Weather]()forweatherinweathers:weather.FileName=weather_filedefshow_weather(self):"""Show weather file for all simulations"""forweatherinself.Model.FindAllDescendants[Weather]():print(weather.FileName)defset_report(self,report,simulations=None):"""Set APSIM report Parameters ---------- report New report string. simulations, optional List of simulation names to update, if `None` update all simulations """simulations=self.find_simulations(simulations)forsiminsimulations:r=sim.FindDescendant[Models.Report]()r.set_VariableNames(report.strip().splitlines())defget_report(self,simulation=None):"""Get current report string Parameters ---------- simulation, optional Simulation name, if `None` use the first simulation. Returns ------- List of report lines. """sim=self._find_simulation(simulation)report=list(sim.FindAllDescendants[Models.Report]())[0]returnlist(report.get_VariableNames())deffind_physical_soil(self,simulation=None):"""Find physical soil Parameters ---------- simulation, optional Simulation name, if `None` use the first simulation. Returns ------- APSIM Models.Soils.Physical object """sim=self._find_simulation(simulation)soil=sim.FindDescendant[Soil]()psoil=soil.FindDescendant[Physical]()returnpsoil# Find a list of simulations by namedeffind_simulations(self,simulations=None):"""Find simulations by name Parameters ---------- simulations, optional List of simulation names to find, if `None` return all simulations Returns ------- list of APSIM Models.Core.Simulation objects """ifsimulationsisNone:returnself.simulationsiftype(simulations)==str:simulations=[simulations]sims=[]forsinself.simulations:ifs.Nameinsimulations:sims.append(s)iflen(sims)==0:print("Not found!")else:returnsims# Find a single simulation by namedef_find_simulation(self,simulation=None):ifsimulationisNone:returnself.simulations[0]sim=Noneforsinself.simulations:ifs.Name==simulation:sim=sbreakifsimisNone:print("Not found!")else:returnsimdefget_dul(self,simulation=None):"""Get soil dry upper limit (DUL) Parameters ---------- simulation, optional Simulation name. Returns ------- Array of DUL values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.DUL)defset_dul(self,dul,simulations=None):"""Set soil dry upper limit (DUL) Parameters ---------- dul Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.DUL=dulself._fix_crop_ll(sim.Name)# Set crop LL to LL15 and make sure it's below DUL in all layersdef_fix_crop_ll(self,simulation):tmp_cll=self.get_crop_ll(simulation)dul=self.get_dul(simulation)ll15=self.get_ll15(simulation)forjinrange(len(tmp_cll)):iftmp_cll[j]>dul[j]:tmp_cll[j]=dul[j]-0.02forjinrange(len(tmp_cll)):tmp_cll[j]=ll15[j]self.set_crop_ll(tmp_cll,simulation)def_fill_layer(self,p,N_layers):ns=len(p)ifns==N_layers:returnpelse:pfill=np.repeat(p[-1],N_layers-ns)returnnp.concatenate([p,pfill])defset_soil(self,soildf,simulations=None):"""Set soil properties using a DataFrame Parameters ---------- soildf DataFrame with column names matching the parameter to be set. Soil will be filled to have the same depth as current soil in the model. cf. `get_soil`. simulations, optional List of simulation names to update, if `None` update all simulations """csoil=self.get_soil(simulations)N_layers=csoil.shape[0]forcolumninsoildf:col=column.lower()p=soildf[column].to_numpy()new=self._fill_layer(p,N_layers)ifcol=="sat":self.set_sat(new,simulations)ifcolin["fc_10","fc","dul"]:self.set_dul(new,simulations)ifcolin["wp","pwp","ll15"]:self.set_ll15(new,simulations)ifcolin["nh4","initial nh4"]:self.set_initial_nh4(new,simulations)ifcolin["no3","initial no3"]:self.set_initial_no3(new,simulations)ifcolin["bd","bulk density"]:self.set_bd(new,simulations)ifcolin["swcon"]:self.set_swcon(new,simulations)ifcolin["ksat","ksat_mm"]:self.set_ksat(new,simulations)ifcolin["sw"]:self.set_sw(new,simulations)#SW can't exceed SATcsoil=self.get_soil(simulations)self.set_sw(np.min(csoil[["SAT","SW"]],axis=1),simulations)defset_sat(self,sat,simulations=None):"""Set soil saturated water content (SAT) Parameters ---------- sat Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.SAT=satpsoil.SW=psoil.DULdefget_sat(self,simulation=None):"""Get soil saturated water content (SAT) Parameters ---------- simulation, optional Simulation name. Returns ------- Array of SAT values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.SAT)defget_ll15(self,simulation=None):"""Get soil water content lower limit (LL15) Parameters ---------- simulation, optional Simulation name. Returns ------- Array of LL15 values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.LL15)defset_ll15(self,ll15,simulations=None):"""Set soil water content lower limit (LL15) Parameters ---------- ll15 Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.LL15=ll15psoil.AirDry=ll15self._fix_crop_ll(sim.Name)defset_bd(self,bd,simulations=None):"""Set soil bulk density Parameters ---------- bd Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.BD=bddefget_bd(self,simulation=None):"""Get soil bulk density Parameters ---------- simulation, optional Simulation name. Returns ------- Array of BD values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.BD)defset_ksat(self,ksat,simulations=None):"""Set saturated hydraulic conductivity of soil mm/day Parameters ---------- bd Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.KS=ksatdefget_ksat(self,simulation=None):"""Get saturated hydraulic conductivity of soil mm/day Parameters ---------- simulation, optional Simulation name. Returns ------- Array of BD values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.KS)defset_sw(self,sw,simulations=None):"""Set soil water content Parameters ---------- bd Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()psoil.SW=swdefget_sw(self,simulation=None):"""Get soil water content Parameters ---------- simulation, optional Simulation name. Returns ------- Array of BD values """psoil=self.find_physical_soil(simulation)returnnp.array(psoil.SW)defset_swcon(self,swcon,simulations=None):"""Set soil water conductivity (SWCON) constant for each soil layer. Parameters ---------- swcon Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):wb=sim.FindDescendant[Models.WaterModel.WaterBalance]()wb.SWCON=swcondefget_swcon(self,simulation=None):"""Get soil water conductivity (SWCON) constant for each soil layer. Parameters ---------- simulation, optional Simulation name. Returns ------- Array of SWCON values """sim=self._find_simulation(simulation)wb=sim.FindDescendant[Models.WaterModel.WaterBalance]()returnnp.array(wb.SWCON)defget_crop_ll(self,simulation=None):"""Get crop lower limit Parameters ---------- simulation, optional Simulation name. Returns ------- Array of values """psoil=self.find_physical_soil(simulation)sc=psoil.FindChild[SoilCrop]()returnnp.array(sc.LL)defset_crop_ll(self,ll,simulations=None):"""Set crop lower limit Parameters ---------- ll Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """forsiminself.find_simulations(simulations):psoil=sim.FindDescendant[Physical]()sc=psoil.FindChild[SoilCrop]()sc.LL=lldefget_soil(self,simulation=None):"""Get soil definition as dataframe Parameters ---------- simulation, optional Simulation name. Returns ------- Dataframe with soil definition """sat=self.get_sat(simulation)dul=self.get_dul(simulation)ll15=self.get_ll15(simulation)cll=self.get_crop_ll(simulation)psoil=self.find_physical_soil(simulation)depth=psoil.Depthreturnpd.DataFrame({"Depth":depth,"LL15":ll15,"DUL":dul,"SAT":sat,"Crop LL":cll,"Bulk density":self.get_bd(simulation),"Ksat":self.get_ksat(simulation),"SW":self.get_sw(simulation),"SWCON":self.get_swcon(simulation),"Initial NO3":self.get_initial_no3(simulation),"Initial NH4":self.get_initial_nh4(simulation)})def_find_solute(self,solute,simulation=None):sim=self._find_simulation(simulation)solutes=sim.FindAllDescendants[Models.Soils.Solute]()return[sforsinsolutesifs.Name==solute][0]def_get_initial_values(self,name,simulation):s=self._find_solute(name,simulation)returnnp.array(s.InitialValues)def_set_initial_values(self,name,values,simulations):sims=self.find_simulations(simulations)forsiminsims:s=self._find_solute(name,sim.Name)s.InitialValues=valuesdefget_initial_no3(self,simulation=None):"""Get soil initial NO3 content"""returnself._get_initial_values("NO3",simulation)defset_initial_no3(self,values,simulations=None):"""Set soil initial NO3 content Parameters ---------- values Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """self._set_initial_values("NO3",values,simulations)defget_initial_nh4(self,simulation=None):"""Get soil initial NH4 content"""returnself._get_initial_values("NH4",simulation)defset_initial_nh4(self,values,simulations=None):"""Set soil initial NH4 content Parameters ---------- values Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """self._set_initial_values("NH4",values,simulations)defget_initial_urea(self,simulation=None):"""Get soil initial urea content"""returnself._get_initial_values("Urea",simulation)defset_initial_urea(self,values,simulations=None):"""Set soil initial urea content Parameters ---------- values Collection of values, has to be the same length as existing values. simulations, optional List of simulation names to update, if `None` update all simulations """self._set_initial_values("Urea",values,simulations)
classSimulation(object):def__init__(self,simulation):self.simulation=simulationself.zones=[Zone(z)forzinsimulation.FindAllChildren[Models.Core.Zone]()]deffind_physical_soil(self):soil=self.simulation.FindDescendant[Soil]()psoil=soil.FindDescendant[Physical]()returnpsoil# TODO should these be linked to zones instead?defget_dul(self):psoil=self.find_physical_soil()returnnp.array(psoil.DUL)defset_dul(self,dul):psoil=self.find_physical_soil()psoil.DUL=dulclassZone(object):def__init__(self,zone):self.zone=zoneself.name=zone.Nameself.soil=self.zone.FindDescendant[Soil]()self.physical_soil=self.soil.FindDescendant[Physical]()# TODO should these be linked to zones instead?@propertydefdul(self):returnnp.array(self.physical_soil.DUL)@dul.setterdefdul(self,dul):self.physical_soil.DUL=dul