diff --git a/fpcup/parameters.py b/fpcup/parameters.py index a32e6c8..ac345ef 100644 --- a/fpcup/parameters.py +++ b/fpcup/parameters.py @@ -86,7 +86,7 @@ def __str__(self) -> str: PCSEParameter = PCSEFlag | PCSELabel | PCSENumericParameter | PCSETabularParameter ### Parameters -# Parameter information from "A gentle introduction to WOFOST" (De Wit & Boogaard 2021), CABO file descriptions, YAML file descriptions +# Parameter information from "A gentle introduction to WOFOST" (De Wit & Boogaard 2021), CABO file descriptions, YAML file descriptions, WOFOSTSiteProvider defaults C = "°C" Cday = f"{C} day" cm3percm3 = "cm^3 / cm^3" @@ -100,10 +100,10 @@ def __str__(self) -> str: n = PCSENumericParameter(name="n", description="Number of sites") area = PCSENumericParameter(name="area", description="Total plot area", unit="ha") WAV = PCSENumericParameter(name="WAV", description="Initial amount of water in rootable zone in excess of wilting point", plotname="Initial excess water", unit="cm", bounds=(0, 50), default=10) -NOTINF = PCSENumericParameter(name="NOTINF", description="Non-infiltrating fraction", bounds=(0, 1)) -SMLIM = PCSENumericParameter(name="SMLIM", description="Maximum initial soil moisture in rooted zone", plotname="Maximum initial soil moisture", unit="cm", bounds=(0, 10)) -SSI = PCSENumericParameter(name="SSI", description="Initial surface storage", unit="cm", bounds=(0, 2)) -SSMAX = PCSENumericParameter(name="SSMAX", description="Maximum surface storage capacity", unit="cm", bounds=(0, 2)) +NOTINF = PCSENumericParameter(name="NOTINF", description="Non-infiltrating fraction", bounds=(0, 1), default=0) +SMLIM = PCSENumericParameter(name="SMLIM", description="Maximum initial soil moisture in rooted zone", plotname="Maximum initial soil moisture", unit="cm", bounds=(0, 10), default=0.4) +SSI = PCSENumericParameter(name="SSI", description="Initial surface storage", unit="cm", bounds=(0, 2), default=0) +SSMAX = PCSENumericParameter(name="SSMAX", description="Maximum surface storage capacity", unit="cm", bounds=(0, 2), default=0) CRAIRC = PCSENumericParameter(name="CRAIRC", description="Critical soil air content for aeration (used when IOX = 1)", plotname="Critical soil air content for aeration", unit=cm3percm3, bounds=(0.04, 0.1)) SM0 = PCSENumericParameter(name="SM0", description="Soil moisture content of saturated soil", plotname="Saturated soil moisture content", unit=cm3percm3, bounds=(0.3, 0.9)) SMFCF = PCSENumericParameter(name="SMFCF", description="Soil moisture content at field capacity", unit=cm3percm3, bounds=(0.05, 0.74)) diff --git a/fpcup/tools.py b/fpcup/tools.py index 8f05948..296cdb2 100644 --- a/fpcup/tools.py +++ b/fpcup/tools.py @@ -3,7 +3,7 @@ """ from copy import copy from functools import partial -from itertools import product +from itertools import chain, product from textwrap import indent indent2 = partial(indent, prefix=" ") @@ -17,6 +17,13 @@ RUNNING_IN_IPYTHON = True +def flatten_list(x: Iterable[Iterable[object]]) -> list[object]: + """ + Flatten a list of lists (or other iterables), using itertools.chain. + """ + return list(chain(x)) + + def make_iterable(x: object, exclude: Iterable[type]=[str]) -> Iterable: """ Check if x is an iterable. diff --git a/wofost_ensemble_parameters.py b/wofost_ensemble_parameters.py index 01bc4e2..b95cc24 100644 --- a/wofost_ensemble_parameters.py +++ b/wofost_ensemble_parameters.py @@ -2,9 +2,10 @@ Run a PCSE ensemble for different values of input parameters to determine their effect. All available soil types are tested. A specified number of sites, roughly central to the Netherlands, are used. +By default, three crops (spring barley, green maize, winter wheat) are used; a single crop can be specified with -c. Example: - python wofost_ensemble_parameters.py rdmsol wav -n 100 -v -c barley -s 16 + python wofost_ensemble_parameters.py rdmsol wav -n 100 -v To simulate only multiple sowing dates, simply leave out the other parameters, e.g.: python wofost_ensemble_parameters.py -v -c barley -s 16 -d @@ -17,7 +18,7 @@ parser.add_argument("parameter_names", help="parameter(s) to iterate over", type=str.upper, nargs="*") parser.add_argument("-n", "--number", help="number of values per parameter; note the exponential increase in runs when doing multiple parameters", type=int, default=100) parser.add_argument("-d", "--sowdates", help="run the simulation for multiple sowing dates (based on the crop's range)", action="store_true") -parser.add_argument("-c", "--crop", help="crop to run simulations on", default="barley", type=fpcup.crop.select_crop) +parser.add_argument("-c", "--crops", help="crop(s) to run simulations on", default=None) parser.add_argument("-s", "--number_sites", help="number of sites; result may be lower due to rounding", type=int, default=16) parser.add_argument("--data_dir", help="folder to load PCSE data from", type=fpcup.io.Path, default=fpcup.settings.DEFAULT_DATA) parser.add_argument("--output_dir", help="folder to save PCSE outputs to (default: generated from parameters)", type=fpcup.io.Path, default=None) @@ -40,7 +41,7 @@ if __name__ == "__main__": fpcup.multiprocessing.freeze_support() - ### SETUP + ### GENERAL SETUP # Check that enough inputs were provided assert len(args.parameter_names) > 0 or args.sowdates, "Please provide parameters to iterate over and/or use the -d flag to iterate over sowing dates." @@ -49,6 +50,16 @@ if args.verbose: print(f"Save folder: {args.output_dir.absolute()}") + + ### ITERABLE SETUP + # Get crop data + if args.crops is None: # Default + args.crops = [fpcup.crop.crops["barley (spring)"], fpcup.crop.crops["maize (green)"], fpcup.crop.crops["wheat (winter)"]] + elif isinstance(args.crops, str): # Single crop + args.crops = [fpcup.crop.select_crop(args.crops)] + else: + raise ValueError(f"Cannot determine target crop from input '{args.crops}'") + # Generate the parameter iterable if args.parameter_names: combined_parameters = fpcup.parameters.generate_ensemble_space(*args.parameter_names, n=args.number) @@ -57,22 +68,22 @@ if args.verbose: print(f"Iterating over the following parameters: {args.parameter_names}") - # Mix in agromanagement data - # Generate one or multiple agromanagement calendars, corresponding to sowing dates - if args.sowdates: - agromanagements = args.crop.agromanagement_all_sowingdates(YEAR) - variables = {"agromanagement": agromanagements, **variables} + # Mix in agromanagement + if args.sowdates: # Multiple sowing dates + agromanagements = fpcup.tools.flatten_list(c.agromanagement_all_sowingdates(YEAR) for c in args.crops) + else: # Single sowing date + agromanagements = [c.agromanagement_first_sowingdate(YEAR) for c in args.crops] - first, last = agromanagements[0].crop_start_date, agromanagements[-1].crop_start_date + if len(agromanagements) == 1: # Single crop, single sowing date + CONSTANTS = {"agromanagement": agromanagements[0], **CONSTANTS} if args.verbose: - print(f"Generated {len(agromanagements)} agromanagement calendars ({first} -- {last}).") - else: - agromanagements = args.crop.agromanagement_first_sowingdate(YEAR) - CONSTANTS = {"agromanagement": agromanagements, **CONSTANTS} - if args.verbose: - print("Loaded agro management data:") - print(agromanagements) + print("Generated 1 agromanagement calendar:") + print(agromanagements[0]) print() + else: # Multiple crops and/or multiple sowing dates + variables = {"agromanagement": agromanagements, **variables} + if args.verbose: + print(f"Generated {len(agromanagements)} agromanagement calendars for {len(args.crops)} crops.") # Generate the coordinates coords = fpcup.site.generate_sites_space(latitude=(51, 53), longitude=(4, 6), n=args.number_sites) @@ -84,6 +95,7 @@ if args.verbose: print(f"Expected total runs: {args.number}**{len(args.parameter_names)} parameter values" + f" * {len(args.crops)} crops" f" * {len(agromanagements) if args.sowdates else 1} sowing dates" f" * {len(SOILTYPES)} soil types" f" * {len(coords)} sites"