Cycles-utils 4.0.4__tar.gz → 4.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/Cycles_utils.egg-info/PKG-INFO +4 -2
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/Cycles_utils.egg-info/SOURCES.txt +5 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/Cycles_utils.egg-info/requires.txt +1 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/PKG-INFO +4 -2
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/README.md +2 -1
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/__init__.py +1 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles.py +35 -15
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_runner.py +139 -20
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/__init__.py +4 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/_base_file.py +29 -1
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/control_file.py +19 -19
- cycles_utils-4.0.6/cycles/cycles_tools/nudge_file.py +63 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/operation_file.py +61 -16
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/output_file.py +5 -5
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/plot_tools.py +53 -14
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/soil_file.py +13 -13
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/weather_file.py +3 -3
- cycles_utils-4.0.6/cycles/data/cb_2018_us_state_20m.cpg +1 -0
- cycles_utils-4.0.6/cycles/data/cb_2018_us_state_20m.dbf +0 -0
- cycles_utils-4.0.6/cycles/data/cb_2018_us_state_20m.prj +1 -0
- cycles_utils-4.0.6/cycles/data/cb_2018_us_state_20m.shp +0 -0
- cycles_utils-4.0.6/cycles/data/cb_2018_us_state_20m.shx +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/rotation_builder.py +13 -33
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/soilgrids/soilgrids.py +14 -9
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/ssurgo/ssurgo.py +103 -57
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/setup.py +2 -2
- cycles_utils-4.0.4/cycles/cycles_tools/nudge_file.py +0 -63
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/Cycles_utils.egg-info/dependency_links.txt +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/Cycles_utils.egg-info/top_level.txt +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/LICENSE +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/MANIFEST.in +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/cycles_tools/reinit_file.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/GLDASp5_elevation_025d.nc4 +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/GLDASp5_landmask_025d.nc4 +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/NLDAS_elevation.nc4 +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/NLDAS_masks-veg-soil.nc4 +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/fips_gid_conversion.csv +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/gridMET_elevation_mask.nc +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/metdata_elevationdata.nc +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/data/us_states.csv +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/gadm/__init__.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/gadm/gadm.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/soilgrids/__init__.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/ssurgo/__init__.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/weather/__init__.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/cycles/weather/weather.py +0 -0
- {cycles_utils-4.0.4 → cycles_utils-4.0.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Cycles-utils
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.6
|
|
4
4
|
Summary: Python scripts to build Cycles input files and post-process Cycles output files
|
|
5
5
|
Home-page: https://github.com/PSUmodeling/Cycles-utils
|
|
6
6
|
Author: Yuning Shi
|
|
@@ -21,6 +21,7 @@ Requires-Dist: rasterio>=1.2.3; extra == "soilgrids"
|
|
|
21
21
|
Requires-Dist: shapely>=1.7.1; extra == "soilgrids"
|
|
22
22
|
Provides-Extra: gssurgo
|
|
23
23
|
Requires-Dist: shapely>=1.7.1; extra == "gssurgo"
|
|
24
|
+
Requires-Dist: fiona>=1.8.20; extra == "gssurgo"
|
|
24
25
|
Provides-Extra: weather
|
|
25
26
|
Requires-Dist: netCDF4>=1.5.7; extra == "weather"
|
|
26
27
|
Requires-Dist: tqdm>=4.60.0; extra == "weather"
|
|
@@ -60,5 +61,6 @@ pip install Cycles-utils
|
|
|
60
61
|
|
|
61
62
|
| Cycles-utils version | Cycles version |
|
|
62
63
|
| -------------------- | -------------- |
|
|
64
|
+
| 4.0.6 | 1.5.20 |
|
|
65
|
+
| 4.0.5 | 1.5.20 |
|
|
63
66
|
| 4.0.4 | 1.5.20 |
|
|
64
|
-
|
|
@@ -25,6 +25,11 @@ cycles/data/GLDASp5_elevation_025d.nc4
|
|
|
25
25
|
cycles/data/GLDASp5_landmask_025d.nc4
|
|
26
26
|
cycles/data/NLDAS_elevation.nc4
|
|
27
27
|
cycles/data/NLDAS_masks-veg-soil.nc4
|
|
28
|
+
cycles/data/cb_2018_us_state_20m.cpg
|
|
29
|
+
cycles/data/cb_2018_us_state_20m.dbf
|
|
30
|
+
cycles/data/cb_2018_us_state_20m.prj
|
|
31
|
+
cycles/data/cb_2018_us_state_20m.shp
|
|
32
|
+
cycles/data/cb_2018_us_state_20m.shx
|
|
28
33
|
cycles/data/fips_gid_conversion.csv
|
|
29
34
|
cycles/data/gridMET_elevation_mask.nc
|
|
30
35
|
cycles/data/metdata_elevationdata.nc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Cycles-utils
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.6
|
|
4
4
|
Summary: Python scripts to build Cycles input files and post-process Cycles output files
|
|
5
5
|
Home-page: https://github.com/PSUmodeling/Cycles-utils
|
|
6
6
|
Author: Yuning Shi
|
|
@@ -21,6 +21,7 @@ Requires-Dist: rasterio>=1.2.3; extra == "soilgrids"
|
|
|
21
21
|
Requires-Dist: shapely>=1.7.1; extra == "soilgrids"
|
|
22
22
|
Provides-Extra: gssurgo
|
|
23
23
|
Requires-Dist: shapely>=1.7.1; extra == "gssurgo"
|
|
24
|
+
Requires-Dist: fiona>=1.8.20; extra == "gssurgo"
|
|
24
25
|
Provides-Extra: weather
|
|
25
26
|
Requires-Dist: netCDF4>=1.5.7; extra == "weather"
|
|
26
27
|
Requires-Dist: tqdm>=4.60.0; extra == "weather"
|
|
@@ -60,5 +61,6 @@ pip install Cycles-utils
|
|
|
60
61
|
|
|
61
62
|
| Cycles-utils version | Cycles version |
|
|
62
63
|
| -------------------- | -------------- |
|
|
64
|
+
| 4.0.6 | 1.5.20 |
|
|
65
|
+
| 4.0.5 | 1.5.20 |
|
|
63
66
|
| 4.0.4 | 1.5.20 |
|
|
64
|
-
|
|
@@ -8,6 +8,7 @@ from .cycles_tools import read_soil_file
|
|
|
8
8
|
from .cycles_tools import read_weather_file
|
|
9
9
|
from .cycles_tools import read_output
|
|
10
10
|
from .cycles_tools import read_operation_file
|
|
11
|
+
from .cycles_tools import generate_operation_file
|
|
11
12
|
from .cycles_tools import SoilLayer
|
|
12
13
|
from .cycles_tools import plot_yield
|
|
13
14
|
from .cycles_tools import plot_operations
|
|
@@ -29,6 +29,11 @@ class Cycles:
|
|
|
29
29
|
Provides methods to run simulations, read outputs, and inspect soil/weather/operation configurations. Automatically
|
|
30
30
|
loads the control file upon initialization.
|
|
31
31
|
|
|
32
|
+
Args:
|
|
33
|
+
path: Path to the simulation directory (containing input/ and output/ subdirs).
|
|
34
|
+
simulation: Name of the simulation (base name of control file without extension).
|
|
35
|
+
executable: Optional absolute path to the Cycles executable binary. This is required to run simulations using the `run` method.
|
|
36
|
+
|
|
32
37
|
Attributes:
|
|
33
38
|
path: Path to the simulation directory (containing input/ and output/ subdirs).
|
|
34
39
|
simulation: Name of the simulation (base name of control file without extension).
|
|
@@ -44,14 +49,14 @@ class Cycles:
|
|
|
44
49
|
|
|
45
50
|
path: Path | str
|
|
46
51
|
simulation: str
|
|
47
|
-
output: dict[str, Output] = field(default_factory=dict[str, Output])
|
|
48
|
-
control: ControlConfig | None = None
|
|
49
|
-
operations: list | None = None
|
|
50
|
-
soil_profile: list[SoilLayer] | None = None
|
|
51
|
-
curve_number: int | None = None
|
|
52
|
-
slope: float | None = None
|
|
53
|
-
weather: pd.DataFrame | None = None
|
|
54
52
|
executable: Path | str | None = None
|
|
53
|
+
output: dict[str, Output] = field(init=False, default_factory=dict[str, Output])
|
|
54
|
+
control: ControlConfig | None = field(init=False, default=None)
|
|
55
|
+
operations: list | None = field(init=False, default=None)
|
|
56
|
+
soil_profile: list[SoilLayer] | None = field(init=False, default=None)
|
|
57
|
+
curve_number: int | None = field(init=False, default=None)
|
|
58
|
+
slope: float | None = field(init=False, default=None)
|
|
59
|
+
weather: pd.DataFrame | None = field(init=False, default=None)
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
def __post_init__(self):
|
|
@@ -65,6 +70,10 @@ class Cycles:
|
|
|
65
70
|
def run(self, options: str, silence: bool=False) -> tuple[int, str]:
|
|
66
71
|
"""Run the Cycles executable for this simulation.
|
|
67
72
|
|
|
73
|
+
To use the `run` method, the `executable` attribute must be set to the path of the Cycles executable. The
|
|
74
|
+
`options` string is passed directly to the command line when invoking Cycles, allowing you to specify any
|
|
75
|
+
command-line options supported by Cycles (e.g., `-s` for spin-up, etc.).
|
|
76
|
+
|
|
68
77
|
Args:
|
|
69
78
|
options: Command-line options passed to Cycles.
|
|
70
79
|
silence: If True, suppress stdout and stderr printing.
|
|
@@ -89,6 +98,9 @@ class Cycles:
|
|
|
89
98
|
def read_output(self, output_types: Collection) -> None:
|
|
90
99
|
"""Read one or more output tables into memory.
|
|
91
100
|
|
|
101
|
+
When reading multiple output tables, the `output_types` argument can be a list or tuple of table names. Each
|
|
102
|
+
table is read into a pandas DataFrame and stored in the `output` dictionary with its corresponding units.
|
|
103
|
+
|
|
92
104
|
Args:
|
|
93
105
|
output_types: Output table name or collection of names.
|
|
94
106
|
"""
|
|
@@ -130,32 +142,40 @@ class Cycles:
|
|
|
130
142
|
self.weather = _read_weather_file(self.path / 'input' / self.control.input_files.weather_file, start_year=start_year, end_year=end_year, subdaily=subdaily)
|
|
131
143
|
|
|
132
144
|
|
|
133
|
-
def generate_reinit_file(self, doy: int, *,
|
|
145
|
+
def generate_reinit_file(self, doy: int, *, reinit_name: str | None=None) -> None:
|
|
134
146
|
"""Generate a reinitialization file from model output.
|
|
135
147
|
|
|
136
148
|
Args:
|
|
137
149
|
doy: Day-of-year to extract from reinit output.
|
|
138
|
-
|
|
150
|
+
reinit_name: Optional output stem for the reinit file.
|
|
139
151
|
"""
|
|
140
152
|
assert isinstance(self.path, Path)
|
|
141
|
-
_generate_reinit_file(self.path / 'input' / f'{self.simulation if
|
|
153
|
+
_generate_reinit_file(self.path / 'input' / f'{self.simulation if reinit_name is None else reinit_name}.reinit', self.path / 'output' / self.simulation, doy)
|
|
142
154
|
|
|
143
155
|
|
|
144
|
-
def plot_yield(self, *, ax: Axes | None=None, fontsize: int | None=None) -> Axes:
|
|
145
|
-
"""Plot grain and forage yields from harvest output.
|
|
156
|
+
def plot_yield(self, *, ax: Axes | None=None, crop_colors: dict | None=None, fontsize: int | None=None) -> Axes:
|
|
157
|
+
"""Plot grain and forage yields from harvest output.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
ax: Optional axes to draw on.
|
|
161
|
+
crop_colors: Optional mapping from crop names to plot colors.
|
|
162
|
+
fontsize: Optional global font size override.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Axes containing the yield plot.
|
|
166
|
+
"""
|
|
146
167
|
if 'harvest' not in self.output:
|
|
147
168
|
self.read_output('harvest')
|
|
148
169
|
|
|
149
|
-
return _plot_yield(self.output['harvest'].data, ax=ax, fontsize=fontsize)
|
|
170
|
+
return _plot_yield(self.output['harvest'].data, ax=ax, crop_colors=crop_colors, fontsize=fontsize)
|
|
150
171
|
|
|
151
172
|
|
|
152
173
|
def plot_operations(self, *, axs: Axes | np.ndarray | None=None, fontsize: int | None=None):
|
|
153
174
|
"""Plot operation timelines grouped by rotation year.
|
|
154
175
|
|
|
155
176
|
Args:
|
|
156
|
-
rotation_size: Number of years in the plotted rotation.
|
|
157
177
|
axs: Optional axes object(s) to draw on.
|
|
158
|
-
fontsize:
|
|
178
|
+
fontsize: Optional global matplotlib font size override.
|
|
159
179
|
|
|
160
180
|
Returns:
|
|
161
181
|
The axes used for plotting.
|
|
@@ -16,6 +16,18 @@ INPUT_DIR: Path = Path('input')
|
|
|
16
16
|
OUTPUT_DIR: Path = Path('output')
|
|
17
17
|
SUMMARY_DIR: Path = Path('summary')
|
|
18
18
|
|
|
19
|
+
OUTPUT_CONTROL_FLAGS: dict = {
|
|
20
|
+
'dailyEnviron': 'daily_weather_out',
|
|
21
|
+
'dailyResidue': 'daily_residue_out',
|
|
22
|
+
'dailyWater': 'daily_water_out',
|
|
23
|
+
'dailyN': 'daily_nitrogen_out',
|
|
24
|
+
'dailySoilC': 'daily_soil_carbon_out',
|
|
25
|
+
'dailySoilLayersCN': 'daily_soil_lyr_cn_out',
|
|
26
|
+
'annualSOM': 'annual_soil_out',
|
|
27
|
+
'annualSoilProfileC': 'annual_profile_out',
|
|
28
|
+
'annualN': 'annual_nflux_out',
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
@dataclass
|
|
20
32
|
class SimulationContext:
|
|
21
33
|
name: str
|
|
@@ -32,36 +44,131 @@ class CyclesRunner:
|
|
|
32
44
|
Manages batch execution of Cycles simulations by generating control files, operation files, and nudge files from
|
|
33
45
|
templates and parameter dictionaries. Consolidates results into a summary CSV file.
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
Args:
|
|
36
48
|
executable: Absolute path to the Cycles executable binary.
|
|
37
|
-
rotation_builder: If True, disables spin-up option and enables rotation features.
|
|
38
49
|
"""
|
|
39
50
|
|
|
40
51
|
executable: str
|
|
41
|
-
rotation_builder: bool = False
|
|
42
52
|
|
|
43
53
|
def __post_init__(self):
|
|
44
54
|
self.executable = str(Path(self.executable).resolve())
|
|
45
55
|
|
|
46
56
|
|
|
47
57
|
def run(self, simulations: SimulationConfig, control_dict: dict[str, Any], *,
|
|
48
|
-
summary: str
|
|
58
|
+
summary: str | dict[str, str] | None=None,
|
|
59
|
+
operation_template: Path | str | None=None, operation_dict: dict[str, Any] | None=None,
|
|
60
|
+
calibration_dict: dict[str, Any] | None=None,
|
|
49
61
|
options: str='', rm_input: bool=False, rm_output: bool=False, rm_steady_state_soil: bool=True, silence: bool=True, user_comment: str='') -> None:
|
|
50
62
|
"""Execute a batch of simulations and write a consolidated summary.
|
|
51
63
|
|
|
52
64
|
Args:
|
|
53
|
-
simulations: Simulation
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
simulations: Simulation configurations as list of dicts or a DataFrame. Each dict or DataFrame row should be
|
|
66
|
+
corresponding to a single simulation and contain values to support the control, operation, and
|
|
67
|
+
calibration dictionaries.
|
|
68
|
+
control_dict: Control-file values or callables evaluated per simulation.
|
|
69
|
+
summary: Summary CSV name for the summary harvest file written under summary directory. If a dictionary is provided, the keys are output file types and the values are
|
|
70
|
+
summary CSV names. If None, only the harvest summary is written into `summary/summary.csv`.
|
|
56
71
|
operation_template: Template file for generated operation files.
|
|
57
72
|
operation_dict: Substitutions used with operation template.
|
|
58
|
-
calibration_dict: Nudge-file values or callables per
|
|
73
|
+
calibration_dict: Nudge-file values or callables per simulation.
|
|
59
74
|
options: Cycles command options.
|
|
60
75
|
rm_input: Remove generated input files after each run.
|
|
61
76
|
rm_output: Remove run output directory after each run.
|
|
62
77
|
rm_steady_state_soil: Remove generated steady-state soil file.
|
|
63
78
|
silence: If True, suppress simulation screen output.
|
|
64
79
|
user_comment: Optional text prefixed to summary header comments.
|
|
80
|
+
|
|
81
|
+
The following fields are required in `control_dict`:
|
|
82
|
+
|
|
83
|
+
- `simulation_name`
|
|
84
|
+
- `simulation_start_year`
|
|
85
|
+
- `simulation_end_year`
|
|
86
|
+
- `rotation_size`
|
|
87
|
+
- `operation_file`
|
|
88
|
+
- `soil_file`
|
|
89
|
+
- `weather_file`
|
|
90
|
+
|
|
91
|
+
The default values for other fields are:
|
|
92
|
+
|
|
93
|
+
- `crop_file`: `GenericCrops.crop`
|
|
94
|
+
- `reinit_file`: `N/A`
|
|
95
|
+
- `soil_layers`: inferred from the soil file (if not provided)
|
|
96
|
+
- `co2_level`: `-999`
|
|
97
|
+
- `use_reinitialization`: `0`
|
|
98
|
+
- `adjusted_yields`: `0`
|
|
99
|
+
- `hydrology_option`: `1`
|
|
100
|
+
- `automatic_nitrogen`: `0`
|
|
101
|
+
- `automatic_phosphorus`: `0`
|
|
102
|
+
- `automatic_sulfur`: `0`
|
|
103
|
+
|
|
104
|
+
All output control fields default to `0`.
|
|
105
|
+
|
|
106
|
+
Note that `simulation_name` is used to generate the control file name for each simulation. The `simulation_name`
|
|
107
|
+
should be unique for each simulation in the batch.
|
|
108
|
+
|
|
109
|
+
#### Example:
|
|
110
|
+
|
|
111
|
+
To run a batch simulation of continuous corn in different counties of Iowa, you can use the following code snippet:
|
|
112
|
+
```python
|
|
113
|
+
from cycles import CyclesRunner
|
|
114
|
+
|
|
115
|
+
runner = CyclesRunner(executable='/path/to/Cycles')
|
|
116
|
+
|
|
117
|
+
simulations: list[dict] = [
|
|
118
|
+
'GID': 'USA.16.1_1', 'weather': 'NLDAS_41.438Nx94.562W', 'soil': 'maize_rainfed_SoilGrids_USA.16.1_1.soil', 'plant_start': 112, 'plant_end': 154, 'maturity_group': 100,
|
|
119
|
+
'GID': 'USA.16.2_1', 'weather': 'NLDAS_40.938Nx94.688W', 'soil': 'maize_rainfed_SoilGrids_USA.16.2_1.soil', 'plant_start': 112, 'plant_end': 154, 'maturity_group': 100,
|
|
120
|
+
'GID': 'USA.16.3_1', 'weather': 'NLDAS_43.188Nx91.562W', 'soil': 'maize_rainfed_SoilGrids_USA.16.3_1.soil', 'plant_start': 112, 'plant_end': 154, 'maturity_group': 90,
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The control dictionary should work with the simulation configurations to generate the appropriate control files for each simulation:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
control_dict: dict = {
|
|
128
|
+
'simulation_name': lambda x: x['GID'],
|
|
129
|
+
'simulation_start_year': 1981,
|
|
130
|
+
'simulation_end_year': 2016,
|
|
131
|
+
'rotation_size': 1,
|
|
132
|
+
'crop_file': 'GenericCrops.crop',
|
|
133
|
+
'operation_file': lambda x: f'{x["GID"]}.operation',
|
|
134
|
+
'soil_file': lambda x: f'path/to/{x["soil"]}',
|
|
135
|
+
'weather_file': lambda x: f'path/to/{x["gridMET_weather"]}.weather',
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The operation dictionary should work with a template operation file to generate the appropriate operation files for each simulation. In the template operation file, use
|
|
140
|
+
placeholders for planting `DOY`, `END_DOY`, and `CROP` like below:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
DOY $PD1
|
|
144
|
+
END_DOY $PD2
|
|
145
|
+
CROP $CROP
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then define the operation dictionary to substitute the placeholders with values from the simulation configurations:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
operation_dict: dict = {
|
|
152
|
+
'PD1': lambda x: x['plant_start'],
|
|
153
|
+
'PD2': lambda x: x['plant_end'],
|
|
154
|
+
'CROP': lambda x: f'CornRM.{x["relative_maturity_group"]}',
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Finally, run the simulations with the following code snippet:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
cycles_runner.run(
|
|
162
|
+
simulations=simulations,
|
|
163
|
+
control_dict=control_dict,
|
|
164
|
+
operation_template='path/to/template.operation',
|
|
165
|
+
operation_dict=operation_dict,
|
|
166
|
+
summary='summary.csv',
|
|
167
|
+
options='-s',
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The `-s` option enables spin-up for the simulations. The results will be consolidated into `summary/summary.csv`.
|
|
65
172
|
"""
|
|
66
173
|
if isinstance(simulations, pd.DataFrame):
|
|
67
174
|
simulations = simulations.to_dict(orient='records')
|
|
@@ -74,13 +181,22 @@ class CyclesRunner:
|
|
|
74
181
|
f"operation_dict={'None' if operation_dict is None else '...'}"
|
|
75
182
|
)
|
|
76
183
|
|
|
77
|
-
if 's' in options and self.rotation_builder:
|
|
78
|
-
raise ValueError('Spin-up cannot be used with rotation builder.')
|
|
79
|
-
|
|
80
184
|
operation_template = Path(operation_template) if operation_template is not None else None
|
|
185
|
+
if user_comment:
|
|
186
|
+
user_comment = f'# {user_comment.lstrip("# ").rstrip()}\n'
|
|
81
187
|
comment = user_comment + _generate_comment(self.executable, options)
|
|
82
188
|
first_run = True
|
|
83
189
|
|
|
190
|
+
if summary is None:
|
|
191
|
+
summary = {'harvest': 'summary.csv'}
|
|
192
|
+
elif isinstance(summary, str):
|
|
193
|
+
summary = {'harvest': summary}
|
|
194
|
+
assert isinstance(summary, dict)
|
|
195
|
+
|
|
196
|
+
for key in summary.keys():
|
|
197
|
+
if key == 'harvest': continue
|
|
198
|
+
control_dict[OUTPUT_CONTROL_FLAGS[key]] = 1
|
|
199
|
+
|
|
84
200
|
SUMMARY_DIR.mkdir(exist_ok=True)
|
|
85
201
|
|
|
86
202
|
for s in simulations:
|
|
@@ -104,7 +220,9 @@ class CyclesRunner:
|
|
|
104
220
|
self._remove_inputs(cxt)
|
|
105
221
|
if rm_output:
|
|
106
222
|
shutil.rmtree(OUTPUT_DIR / cxt.name, ignore_errors=True)
|
|
107
|
-
if rm_steady_state_soil:
|
|
223
|
+
if rm_steady_state_soil and 's' in options:
|
|
224
|
+
# Steady-state soil should only be removed if generated during this run (i.e., spin-up was requested).
|
|
225
|
+
# If using an existing steady-state soil file, it should not be removed.
|
|
108
226
|
(INPUT_DIR / f'{cxt.name}_ss.soil').unlink(missing_ok=True)
|
|
109
227
|
|
|
110
228
|
|
|
@@ -135,15 +253,16 @@ class CyclesRunner:
|
|
|
135
253
|
cxt.operation_fn.unlink(missing_ok=True)
|
|
136
254
|
|
|
137
255
|
|
|
138
|
-
def _write_summary(self, cycles: Cycles, summary:
|
|
139
|
-
cycles.read_output(
|
|
140
|
-
|
|
256
|
+
def _write_summary(self, cycles: Cycles, summary: dict, *, header: bool, comment: str) -> None:
|
|
257
|
+
cycles.read_output(summary.keys())
|
|
258
|
+
for key, fn in summary.items():
|
|
259
|
+
cycles.output[key].data.insert(0, 'simulation', cycles.simulation)
|
|
141
260
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
261
|
+
mode = 'w' if header else 'a'
|
|
262
|
+
with open(SUMMARY_DIR / fn, mode) as f:
|
|
263
|
+
if header:
|
|
264
|
+
f.write(comment)
|
|
265
|
+
cycles.output[key].data.to_csv(f, header=header, index=False)
|
|
147
266
|
|
|
148
267
|
|
|
149
268
|
def _render_template(template_fn: Path, dest_fn: Path, substitutions: dict) -> None:
|
|
@@ -3,6 +3,8 @@ from .control_file import read_control_file
|
|
|
3
3
|
from .control_file import ControlConfig
|
|
4
4
|
from .nudge_file import generate_nudge_file
|
|
5
5
|
from .operation_file import read_operation_file
|
|
6
|
+
from .operation_file import format_operation
|
|
7
|
+
from .operation_file import generate_operation_file
|
|
6
8
|
from .operation_file import Operation, Planting, Tillage, Harvest, Kill, FixedFertilization, FixedIrrigation, AutoIrrigation
|
|
7
9
|
from .output_file import read_output
|
|
8
10
|
from .soil_file import MAPPABLE_PARAMETERS
|
|
@@ -16,12 +18,14 @@ from .plot_tools import plot_operations
|
|
|
16
18
|
from .plot_tools import plot_map
|
|
17
19
|
from .plot_tools import plot_satellite_map
|
|
18
20
|
from ._base_file import resolve_dict_values
|
|
21
|
+
from ._base_file import read_geospatial_file
|
|
19
22
|
|
|
20
23
|
__all__ = [
|
|
21
24
|
"generate_control_file",
|
|
22
25
|
"read_control_file",
|
|
23
26
|
"generate_nudge_file",
|
|
24
27
|
"read_operation_file",
|
|
28
|
+
"generate_operation_file",
|
|
25
29
|
"read_output",
|
|
26
30
|
"generate_soil_file",
|
|
27
31
|
"read_soil_file",
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import fiona
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import pandas as pd
|
|
2
5
|
import types
|
|
3
6
|
from dataclasses import fields
|
|
4
7
|
from pathlib import Path
|
|
@@ -7,7 +10,11 @@ from typing import Union, Any
|
|
|
7
10
|
def _format_block(label: str, block) -> str:
|
|
8
11
|
lines = [f'## {label.replace("_", " ").upper()} ##']
|
|
9
12
|
for f in fields(block):
|
|
10
|
-
|
|
13
|
+
val = getattr(block, f.name)
|
|
14
|
+
if isinstance(val, float) and val == -999.0:
|
|
15
|
+
val = '-999'
|
|
16
|
+
description = f.metadata.get('description', '')
|
|
17
|
+
lines.append(f'{f.name.upper():<28}{val:<8}# {description}' if description else f'{f.name.upper():<28}{val}')
|
|
11
18
|
lines.append('')
|
|
12
19
|
|
|
13
20
|
return '\n'.join(lines)
|
|
@@ -45,3 +52,24 @@ def unwrap_optional(t) -> type:
|
|
|
45
52
|
if origin is Union or origin is types.UnionType or isinstance(t, types.UnionType):
|
|
46
53
|
return next(arg for arg in t.__args__ if arg is not type(None))
|
|
47
54
|
return t
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_geospatial_file(file_path: str | Path) -> gpd.GeoDataFrame:
|
|
58
|
+
file_path = Path(file_path)
|
|
59
|
+
ext = file_path.suffix.lstrip('.').lower()
|
|
60
|
+
match ext:
|
|
61
|
+
case 'shp':
|
|
62
|
+
return gpd.read_file(file_path)
|
|
63
|
+
case 'kml':
|
|
64
|
+
return _read_kml(file_path)
|
|
65
|
+
case _:
|
|
66
|
+
raise ValueError(f"Unsupported boundary format: '.{ext}'")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_kml(file_path: Path) -> gpd.GeoDataFrame:
|
|
70
|
+
return gpd.GeoDataFrame(
|
|
71
|
+
pd.concat(
|
|
72
|
+
[gpd.read_file(file_path, driver='KML', layer=layer) for layer in fiona.listlayers(file_path)],
|
|
73
|
+
ignore_index=True,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import warnings
|
|
3
|
-
from dataclasses import dataclass, fields
|
|
3
|
+
from dataclasses import dataclass, field, fields
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, get_type_hints
|
|
6
6
|
from ._base_file import write_file, resolve_dict_values, extract, parse_value, unwrap_optional
|
|
@@ -22,10 +22,10 @@ class InputFiles:
|
|
|
22
22
|
@dataclass(kw_only=True)
|
|
23
23
|
class SimulationOptions:
|
|
24
24
|
soil_layers: int
|
|
25
|
-
co2_level: float = -999
|
|
25
|
+
co2_level: float = field(default=-999, metadata={'description': 'atmospheric CO2 concentration (ppm). Use co2.txt file if set to -999'})
|
|
26
26
|
use_reinitialization: int = 0
|
|
27
27
|
adjusted_yields: int = 0
|
|
28
|
-
hydrology_option: int = 1
|
|
28
|
+
hydrology_option: int = field(default=1, metadata={'description': "1: gravity driven, 2: Richards' equation with Crank-Nicholson, 3: Richards' equation with CVode"})
|
|
29
29
|
automatic_nitrogen: int = 0
|
|
30
30
|
automatic_phosphorus: int = 0
|
|
31
31
|
automatic_sulfur: int = 0
|
|
@@ -65,23 +65,23 @@ def _build_control_config(control_dict: dict, simulation_dict: dict[str, Any] |
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def _get_soil_layers(
|
|
68
|
+
def _get_soil_layers(file_path: Path) -> int:
|
|
69
69
|
NUM_HEADER_LINES = 2
|
|
70
70
|
try:
|
|
71
|
-
lines = [line for line in
|
|
71
|
+
lines = [line for line in file_path.read_text().splitlines() if line.strip() and not line.strip().startswith('#')]
|
|
72
72
|
return len(lines) - NUM_HEADER_LINES - 1
|
|
73
73
|
except FileNotFoundError:
|
|
74
|
-
warnings.warn(f"Soil file not found: {
|
|
74
|
+
warnings.warn(f"Soil file not found: {file_path}")
|
|
75
75
|
return -999
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def generate_control_file(
|
|
78
|
+
def generate_control_file(file_path: str | Path, user_dict: dict[str, Any], *, simulation_dict: dict[str, Any] | None=None) -> ControlConfig:
|
|
79
79
|
"""Generate and write a Cycles control file.
|
|
80
80
|
|
|
81
|
-
Provide either direct values or callables that accept a simulation
|
|
82
|
-
parameter names should be in lowercase and correspond to the fields in Cycles simulation control files. If a
|
|
83
|
-
is not provided, it will be filled with a default value. If a field's value is a callable, it will be called
|
|
84
|
-
the `simulation_dict` to resolve its value.
|
|
81
|
+
Provide either direct values or callables that accept a simulation configuration and return a value in `user_dict`.
|
|
82
|
+
The parameter names should be in lowercase and correspond to the fields in Cycles simulation control files. If a
|
|
83
|
+
field is not provided, it will be filled with a default value. If a field's value is a callable, it will be called
|
|
84
|
+
with the `simulation_dict` to resolve its value.
|
|
85
85
|
|
|
86
86
|
The following fields are required in `user_dict`:
|
|
87
87
|
|
|
@@ -108,30 +108,30 @@ def generate_control_file(fn: str | Path, user_dict: dict, *, simulation_dict: d
|
|
|
108
108
|
All output control fields default to `0`.
|
|
109
109
|
|
|
110
110
|
Args:
|
|
111
|
-
|
|
111
|
+
file_path: Destination control file path.
|
|
112
112
|
user_dict: Values or callables for control fields.
|
|
113
113
|
simulation_dict: Optional simulation row for callable resolution.
|
|
114
114
|
|
|
115
115
|
Returns:
|
|
116
116
|
The generated control configuration.
|
|
117
117
|
"""
|
|
118
|
-
|
|
119
|
-
config = _build_control_config(user_dict, simulation_dict,
|
|
120
|
-
write_file(
|
|
118
|
+
file_path = Path(file_path)
|
|
119
|
+
config = _build_control_config(user_dict, simulation_dict, file_path.parent)
|
|
120
|
+
write_file(file_path, config)
|
|
121
121
|
|
|
122
122
|
return config
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def read_control_file(
|
|
126
|
-
"""Parse a Cycles control file into a ControlConfig instance.
|
|
125
|
+
def read_control_file(file_path: str | Path) -> ControlConfig:
|
|
126
|
+
"""Parse a Cycles control file into a `ControlConfig` dataclass instance.
|
|
127
127
|
|
|
128
128
|
Args:
|
|
129
|
-
|
|
129
|
+
file_path: Path to a Cycles control file path.
|
|
130
130
|
|
|
131
131
|
Returns:
|
|
132
132
|
Control configuration.
|
|
133
133
|
"""
|
|
134
|
-
with open(Path(
|
|
134
|
+
with open(Path(file_path)) as f:
|
|
135
135
|
lines = f.read().splitlines()
|
|
136
136
|
lines = iter([line for line in lines if (not line.strip().startswith('#')) and line.strip()])
|
|
137
137
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
from ._base_file import write_file, resolve_dict_values, extract
|
|
6
|
+
|
|
7
|
+
@dataclass(kw_only=True)
|
|
8
|
+
class CalibrationMultipliers:
|
|
9
|
+
soc_decomp_rate: float = field(default=1.0, metadata={'description': 'soil organic carbon decomposition rate'})
|
|
10
|
+
residue_decomp_rate: float = field(default=1.0, metadata={'description': 'residue decomposition rate'})
|
|
11
|
+
root_decomp_rate: float = field(default=1.0, metadata={'description': 'root decomposition rate'})
|
|
12
|
+
rhizo_decomp_rate: float = field(default=1.0, metadata={'description': 'rhizodeposit decomposition rate'})
|
|
13
|
+
manure_decomp_rate: float = field(default=1.0, metadata={'description': 'manure decomposition rate'})
|
|
14
|
+
ferment_decomp_rate: float = field(default=1.0, metadata={'description': 'ferment decomposition rate'})
|
|
15
|
+
microb_decomp_rate: float = field(default=1.0, metadata={'description': 'microbe decomposition rate'})
|
|
16
|
+
soc_humif_power: float = field(default=1.0, metadata={'description': 'soil organic carbon humification exponent'})
|
|
17
|
+
nitrif_rate: float = field(default=1.0, metadata={'description': 'nitrification rate'})
|
|
18
|
+
pot_denitrif_rate: float = field(default=1.0, metadata={'description': 'potential denitrification rate'})
|
|
19
|
+
denitrif_half_rate: float = field(default=1.0, metadata={'description': 'half saturation constant for denitrification'})
|
|
20
|
+
decomp_half_resp: float = field(default=1.0, metadata={'description': 'decomposition half response to saturation (default 0.22)'})
|
|
21
|
+
decomp_resp_power: float = field(default=1.0, metadata={'description': 'decomposition exponential response to saturation (default 3.0)'})
|
|
22
|
+
root_progression: float = field(default=1.0, metadata={'description': 'rooting depth progression rate'})
|
|
23
|
+
radiation_use_efficiency: float = field(default=1.0, metadata={'description': 'crop radiation use efficiency'})
|
|
24
|
+
|
|
25
|
+
@dataclass(kw_only=True)
|
|
26
|
+
class ParameterValues:
|
|
27
|
+
kd_no3: float = field(default=0.0, metadata={'description': 'adsorption coefficient for NO3 (default 0.0 cm3/g)'})
|
|
28
|
+
kd_nh4: float = field(default=5.6, metadata={'description': 'adsorption coefficient for NH4 (default 5.6 cm3/g)'})
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class NudgeConfig:
|
|
32
|
+
calibration_multipliers: CalibrationMultipliers
|
|
33
|
+
parameter_values: ParameterValues
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_nudge_config(user_dict: dict[str, Any], calibration_dict: dict[str, Any] | None) -> NudgeConfig:
|
|
37
|
+
resolved = resolve_dict_values(user_dict, calibration_dict)
|
|
38
|
+
|
|
39
|
+
return NudgeConfig(
|
|
40
|
+
calibration_multipliers=CalibrationMultipliers(**extract(CalibrationMultipliers, resolved)),
|
|
41
|
+
parameter_values=ParameterValues(**extract(ParameterValues, resolved)),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def generate_nudge_file(file_path: str | Path, user_dict: dict[str, Any], *, calibration_dict: dict[str, Any] | None=None) -> None:
|
|
46
|
+
"""Write a Cycles nudge file from user-provided values.
|
|
47
|
+
|
|
48
|
+
Provide either direct values or callables that accept a calibration configuration and return a value in `user_dict`.
|
|
49
|
+
The parameter names should be in lowercase and correspond to the fields in Cycles nudge (calibration) files. If a
|
|
50
|
+
field is not provided, it will be filled with a default value. If a field's value is a callable, it will be called
|
|
51
|
+
with the `calibration_dict` to resolve its value.
|
|
52
|
+
|
|
53
|
+
The default values for all calibration multipliers are `1.0`, and the default values for `kd_no3` and `kd_nh4` are
|
|
54
|
+
`0.0` and `5.6`, respectively.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file_path: Destination nudge file path.
|
|
58
|
+
user_dict: Values or callables for nudge parameters.
|
|
59
|
+
calibration_dict: Optional simulation row for callable resolution.
|
|
60
|
+
"""
|
|
61
|
+
file_path = Path(file_path)
|
|
62
|
+
config = _build_nudge_config(user_dict, calibration_dict)
|
|
63
|
+
write_file(file_path, config)
|