Cycles-utils 4.1.0__tar.gz → 4.2.0__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.1.0 → cycles_utils-4.2.0}/Cycles_utils.egg-info/PKG-INFO +4 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/Cycles_utils.egg-info/SOURCES.txt +3 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/Cycles_utils.egg-info/requires.txt +2 -1
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/PKG-INFO +4 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/README.md +1 -0
- {cycles_utils-4.1.0/cycles/cycles_tools → cycles_utils-4.2.0/cycles}/_base_file.py +22 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles.py +2 -43
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_runner.py +130 -55
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/control_file.py +4 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/converter.py +1 -1
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/crop_file.py +2 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/nudge_file.py +3 -2
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/operation_file.py +1 -1
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/gadm/gadm.py +71 -5
- cycles_utils-4.2.0/cycles/rotation_builder.py +679 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/weather/weather.py +7 -5
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/setup.py +4 -5
- cycles_utils-4.2.0/test/test_runner.py +45 -0
- cycles_utils-4.1.0/cycles/rotation_builder.py +0 -495
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/Cycles_utils.egg-info/dependency_links.txt +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/Cycles_utils.egg-info/top_level.txt +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/LICENSE +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/MANIFEST.in +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/_geo_file.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/output_file.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/plot_tools.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/reinit_file.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/soil_file.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/cycles_tools/weather_file.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/GLDASp5_elevation_025d.nc4 +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/GLDASp5_landmask_025d.nc4 +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/NLDAS_elevation.nc4 +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/NLDAS_masks-veg-soil.nc4 +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/cb_2018_us_state_20m.cpg +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/cb_2018_us_state_20m.dbf +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/cb_2018_us_state_20m.prj +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/cb_2018_us_state_20m.shp +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/cb_2018_us_state_20m.shx +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/fips_gid_conversion.csv +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/gridMET_elevation_mask.nc +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/metdata_elevationdata.nc +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/data/us_states.csv +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/gadm/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/soilgrids/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/soilgrids/soilgrids.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/ssurgo/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/ssurgo/ssurgo.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/cycles/weather/__init__.py +0 -0
- {cycles_utils-4.1.0 → cycles_utils-4.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Cycles-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
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
|
|
@@ -12,6 +12,8 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: pandas>=1.2.4
|
|
13
13
|
Requires-Dist: numpy>=1.19.5
|
|
14
14
|
Requires-Dist: matplotlib>=3.4.2
|
|
15
|
+
Requires-Dist: tqdm>=4.60.0
|
|
16
|
+
Requires-Dist: IPython>=7.0.0
|
|
15
17
|
Provides-Extra: plot
|
|
16
18
|
Requires-Dist: cartopy>=0.18.0; extra == "plot"
|
|
17
19
|
Requires-Dist: geopandas>=0.9.0; extra == "plot"
|
|
@@ -19,7 +21,6 @@ Requires-Dist: fiona>=1.8.20; extra == "plot"
|
|
|
19
21
|
Requires-Dist: shapely>=1.7.1; extra == "plot"
|
|
20
22
|
Provides-Extra: weather
|
|
21
23
|
Requires-Dist: netCDF4>=1.5.7; extra == "weather"
|
|
22
|
-
Requires-Dist: tqdm>=4.60.0; extra == "weather"
|
|
23
24
|
Requires-Dist: scipy>=1.6.3; extra == "weather"
|
|
24
25
|
Provides-Extra: soilgrids
|
|
25
26
|
Requires-Dist: geopandas>=0.9.0; extra == "soilgrids"
|
|
@@ -82,6 +83,7 @@ pip install Cycles-utils
|
|
|
82
83
|
|
|
83
84
|
| Cycles-utils version | Cycles version |
|
|
84
85
|
| -------------------- | -------------- |
|
|
86
|
+
| 4.2.0 | 1.5.20+ |
|
|
85
87
|
| 4.1.0 | 1.5.20+ |
|
|
86
88
|
| 4.0.6 | 1.5.20 |
|
|
87
89
|
| 4.0.5 | 1.5.20 |
|
|
@@ -8,11 +8,11 @@ Cycles_utils.egg-info/dependency_links.txt
|
|
|
8
8
|
Cycles_utils.egg-info/requires.txt
|
|
9
9
|
Cycles_utils.egg-info/top_level.txt
|
|
10
10
|
cycles/__init__.py
|
|
11
|
+
cycles/_base_file.py
|
|
11
12
|
cycles/cycles.py
|
|
12
13
|
cycles/cycles_runner.py
|
|
13
14
|
cycles/rotation_builder.py
|
|
14
15
|
cycles/cycles_tools/__init__.py
|
|
15
|
-
cycles/cycles_tools/_base_file.py
|
|
16
16
|
cycles/cycles_tools/_geo_file.py
|
|
17
17
|
cycles/cycles_tools/control_file.py
|
|
18
18
|
cycles/cycles_tools/converter.py
|
|
@@ -44,4 +44,5 @@ cycles/soilgrids/soilgrids.py
|
|
|
44
44
|
cycles/ssurgo/__init__.py
|
|
45
45
|
cycles/ssurgo/ssurgo.py
|
|
46
46
|
cycles/weather/__init__.py
|
|
47
|
-
cycles/weather/weather.py
|
|
47
|
+
cycles/weather/weather.py
|
|
48
|
+
test/test_runner.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Cycles-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
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
|
|
@@ -12,6 +12,8 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: pandas>=1.2.4
|
|
13
13
|
Requires-Dist: numpy>=1.19.5
|
|
14
14
|
Requires-Dist: matplotlib>=3.4.2
|
|
15
|
+
Requires-Dist: tqdm>=4.60.0
|
|
16
|
+
Requires-Dist: IPython>=7.0.0
|
|
15
17
|
Provides-Extra: plot
|
|
16
18
|
Requires-Dist: cartopy>=0.18.0; extra == "plot"
|
|
17
19
|
Requires-Dist: geopandas>=0.9.0; extra == "plot"
|
|
@@ -19,7 +21,6 @@ Requires-Dist: fiona>=1.8.20; extra == "plot"
|
|
|
19
21
|
Requires-Dist: shapely>=1.7.1; extra == "plot"
|
|
20
22
|
Provides-Extra: weather
|
|
21
23
|
Requires-Dist: netCDF4>=1.5.7; extra == "weather"
|
|
22
|
-
Requires-Dist: tqdm>=4.60.0; extra == "weather"
|
|
23
24
|
Requires-Dist: scipy>=1.6.3; extra == "weather"
|
|
24
25
|
Provides-Extra: soilgrids
|
|
25
26
|
Requires-Dist: geopandas>=0.9.0; extra == "soilgrids"
|
|
@@ -82,6 +83,7 @@ pip install Cycles-utils
|
|
|
82
83
|
|
|
83
84
|
| Cycles-utils version | Cycles version |
|
|
84
85
|
| -------------------- | -------------- |
|
|
86
|
+
| 4.2.0 | 1.5.20+ |
|
|
85
87
|
| 4.1.0 | 1.5.20+ |
|
|
86
88
|
| 4.0.6 | 1.5.20 |
|
|
87
89
|
| 4.0.5 | 1.5.20 |
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
2
4
|
import types
|
|
3
5
|
from dataclasses import fields
|
|
4
6
|
from pathlib import Path
|
|
@@ -74,3 +76,23 @@ def _unwrap_optional(t) -> type:
|
|
|
74
76
|
if origin is Union or origin is types.UnionType or isinstance(t, types.UnionType):
|
|
75
77
|
return next(arg for arg in t.__args__ if arg is not type(None))
|
|
76
78
|
return t
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _if_ipython() -> bool:
|
|
82
|
+
try:
|
|
83
|
+
from IPython import get_ipython # type: ignore
|
|
84
|
+
shell = get_ipython()
|
|
85
|
+
return shell is not None and shell.__class__.__name__ == 'ZMQInteractiveShell'
|
|
86
|
+
except ImportError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _disable_progress_bar() -> bool:
|
|
91
|
+
if _if_ipython():
|
|
92
|
+
return False
|
|
93
|
+
if os.environ.get('SLURM_JOB_ID') is not None:
|
|
94
|
+
return True
|
|
95
|
+
try:
|
|
96
|
+
return not sys.stderr.isatty()
|
|
97
|
+
except AttributeError:
|
|
98
|
+
return True
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import numpy as np
|
|
3
|
-
import os
|
|
4
3
|
import pandas as pd
|
|
5
|
-
import subprocess
|
|
6
4
|
from collections.abc import Collection
|
|
7
5
|
from dataclasses import dataclass, field
|
|
8
6
|
from pathlib import Path
|
|
@@ -24,13 +22,12 @@ class Output:
|
|
|
24
22
|
class Cycles:
|
|
25
23
|
"""Interface for executing one Cycles simulation and reading its files.
|
|
26
24
|
|
|
27
|
-
Provides methods to
|
|
28
|
-
|
|
25
|
+
Provides methods to read outputs, and inspect soil/weather/operation configurations. Automatically loads the control
|
|
26
|
+
file upon initialization.
|
|
29
27
|
|
|
30
28
|
Args:
|
|
31
29
|
path: Path to the simulation directory (containing input/ and output/ subdirs).
|
|
32
30
|
simulation: Name of the simulation (base name of control file without extension).
|
|
33
|
-
executable: Optional absolute path to the Cycles executable binary. This is required to run simulations using the `run` method.
|
|
34
31
|
|
|
35
32
|
Attributes:
|
|
36
33
|
path: Path to the simulation directory (containing input/ and output/ subdirs).
|
|
@@ -42,12 +39,10 @@ class Cycles:
|
|
|
42
39
|
curve_number: Runoff curve number for hydrologic calculations.
|
|
43
40
|
slope: Land slope used in erosion and runoff models.
|
|
44
41
|
weather: DataFrame of weather forcing data (temperature, precipitation, etc.).
|
|
45
|
-
executable: Absolute path to the Cycles executable binary.
|
|
46
42
|
"""
|
|
47
43
|
|
|
48
44
|
simulation: str
|
|
49
45
|
path: Path | str = '.'
|
|
50
|
-
executable: Path | str | None = None
|
|
51
46
|
output: dict[str, Output] = field(init=False, default_factory=dict[str, Output])
|
|
52
47
|
control: ControlConfig | None = field(init=False, default=None)
|
|
53
48
|
operations: list | None = field(init=False, default=None)
|
|
@@ -61,42 +56,6 @@ class Cycles:
|
|
|
61
56
|
self.path = Path(self.path)
|
|
62
57
|
assert isinstance(self.path, Path)
|
|
63
58
|
self.control = _read_control_file(self.path / 'input' / f'{self.simulation}.ctrl')
|
|
64
|
-
if self.executable is not None:
|
|
65
|
-
self.executable = str(Path(self.executable).resolve())
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def run(self, options: str, silence: bool=False) -> tuple[int, str]:
|
|
69
|
-
"""Run the Cycles executable for this simulation.
|
|
70
|
-
|
|
71
|
-
To use the `run` method, the `executable` attribute must be set to the path of the Cycles executable. The
|
|
72
|
-
`options` string is passed directly to the command line when invoking Cycles, allowing you to specify any
|
|
73
|
-
command-line options supported by Cycles (e.g., `-s` for spin-up, etc.).
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
options: Command-line options passed to Cycles.
|
|
77
|
-
silence: If True, suppress stdout and stderr printing.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
A tuple with process return code and stdout text.
|
|
81
|
-
"""
|
|
82
|
-
cwd = os.getcwd()
|
|
83
|
-
cmd = [self.executable, *(options.split() if options else []), self.simulation]
|
|
84
|
-
|
|
85
|
-
os.chdir(self.path)
|
|
86
|
-
result = subprocess.run(
|
|
87
|
-
cmd,
|
|
88
|
-
shell=os.name == 'nt',
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
)
|
|
92
|
-
if not silence:
|
|
93
|
-
print(result.stdout)
|
|
94
|
-
if result.stderr:
|
|
95
|
-
print(result.stderr)
|
|
96
|
-
|
|
97
|
-
os.chdir(cwd)
|
|
98
|
-
|
|
99
|
-
return result.returncode, result.stdout
|
|
100
59
|
|
|
101
60
|
|
|
102
61
|
def read_output(self, output_types: Collection) -> None:
|
|
@@ -3,15 +3,20 @@ import os
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Sequence
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from string import Template
|
|
9
11
|
from typing import Any
|
|
10
12
|
from .cycles import Cycles
|
|
11
13
|
from .cycles_tools import generate_control_file, generate_nudge_file
|
|
12
|
-
from .cycles_tools.
|
|
14
|
+
from .cycles_tools.control_file import DEFAULT_CROP_FILE
|
|
15
|
+
from ._base_file import _resolve_dict_values, _if_ipython, _disable_progress_bar
|
|
16
|
+
if _if_ipython(): from tqdm.notebook import tqdm
|
|
17
|
+
else: from tqdm import tqdm
|
|
13
18
|
|
|
14
|
-
SimulationConfig = list[dict] | pd.DataFrame
|
|
19
|
+
SimulationConfig = list[dict] | pd.DataFrame | list[None] | None
|
|
15
20
|
|
|
16
21
|
INPUT_DIR: str = 'input'
|
|
17
22
|
OUTPUT_DIR: str = 'output'
|
|
@@ -33,8 +38,10 @@ OUTPUT_CONTROL_FLAGS: dict = {
|
|
|
33
38
|
class SimulationContext:
|
|
34
39
|
name: str
|
|
35
40
|
control_dict: dict
|
|
41
|
+
crop_dict: dict | None
|
|
36
42
|
operation_dict: dict | None
|
|
37
43
|
calibration_dict: dict | None
|
|
44
|
+
crop_fn: Path
|
|
38
45
|
operation_fn: Path
|
|
39
46
|
|
|
40
47
|
|
|
@@ -57,20 +64,28 @@ class CyclesRunner:
|
|
|
57
64
|
self.path = Path(self.path)
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
def run(self,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
def run(self, *, control_dict: dict[str, Any], simulations: SimulationConfig=None,
|
|
68
|
+
summary: Sequence[str] | None=None,
|
|
69
|
+
summary_prefix: str | None=None,
|
|
70
|
+
operation_template: Path | str | None=None, operation_dict: dict[str, Any] | None=None,
|
|
71
|
+
crop_template: Path | str | None=None, crop_dict: dict[str, Any] | None=None,
|
|
72
|
+
calibration_dict: dict[str, Any] | None=None,
|
|
73
|
+
options: str='', rm_input: bool=False, rm_output: bool=False, rm_steady_state_soil: bool=True, silence: bool=True, user_comment: str='',
|
|
74
|
+
_progress_bar: tqdm | None=None) -> None: # type: ignore
|
|
65
75
|
"""Execute a batch of simulations and write a consolidated summary.
|
|
66
76
|
|
|
67
77
|
Args:
|
|
68
78
|
simulations: Simulation configurations as list of dicts or a DataFrame. Each dict or DataFrame row should be
|
|
69
79
|
corresponding to a single simulation and contain values to support the control, operation, and
|
|
70
|
-
calibration dictionaries.
|
|
80
|
+
calibration dictionaries. If None, a single simulation is run directly from `control_dict`/
|
|
81
|
+
`operation_dict` (no per-row substitution). In this single-run mode, the summary CSV is **not**
|
|
82
|
+
written and `rm_input`/`rm_output`/`rm_steady_state_soil` are **not** applied, regardless of how
|
|
83
|
+
those flags are set -- the full input/output is always left in place for inspection.
|
|
71
84
|
control_dict: Control-file values or callables evaluated per simulation.
|
|
72
|
-
summary:
|
|
73
|
-
|
|
85
|
+
summary: List or tuple of output file types to be summarized/aggregated. If None, only the harvest summary is written into `summary/harvest.csv`.
|
|
86
|
+
summary_prefix: Prefix for the summary CSV files. If None, no prefix is added.
|
|
87
|
+
crop_template: Template file for generated crop files.
|
|
88
|
+
crop_dict: Substitutions used with crop template.
|
|
74
89
|
operation_template: Template file for generated operation files.
|
|
75
90
|
operation_dict: Substitutions used with operation template.
|
|
76
91
|
calibration_dict: Nudge-file values or callables per simulation.
|
|
@@ -81,6 +96,10 @@ class CyclesRunner:
|
|
|
81
96
|
silence: If True, suppress simulation screen output.
|
|
82
97
|
user_comment: Optional text prefixed to summary header comments.
|
|
83
98
|
|
|
99
|
+
In batch mode (`simulations` is a list/DataFrame), a simulation that fails (non-zero exit code) is skipped --
|
|
100
|
+
its summary row is not written -- but does not abort the remaining batch; cleanup (`rm_input`/`rm_output`)
|
|
101
|
+
still runs for the failed row if requested.
|
|
102
|
+
|
|
84
103
|
The following fields are required in `control_dict`:
|
|
85
104
|
|
|
86
105
|
- `simulation_name`
|
|
@@ -139,8 +158,9 @@ class CyclesRunner:
|
|
|
139
158
|
}
|
|
140
159
|
```
|
|
141
160
|
|
|
142
|
-
The operation dictionary should work with a template operation file to generate the appropriate operation files
|
|
143
|
-
placeholders for planting `DOY`, `END_DOY`, and `CROP`
|
|
161
|
+
The operation dictionary should work with a template operation file to generate the appropriate operation files
|
|
162
|
+
for each simulation. In the template operation file, use placeholders for planting `DOY`, `END_DOY`, and `CROP`
|
|
163
|
+
like below:
|
|
144
164
|
|
|
145
165
|
```
|
|
146
166
|
DOY $PD1
|
|
@@ -148,7 +168,8 @@ class CyclesRunner:
|
|
|
148
168
|
CROP $CROP
|
|
149
169
|
```
|
|
150
170
|
|
|
151
|
-
Then define the operation dictionary to substitute the placeholders with values from the simulation
|
|
171
|
+
Then define the operation dictionary to substitute the placeholders with values from the simulation
|
|
172
|
+
configurations:
|
|
152
173
|
|
|
153
174
|
```python
|
|
154
175
|
operation_dict: dict = {
|
|
@@ -166,60 +187,65 @@ class CyclesRunner:
|
|
|
166
187
|
control_dict=control_dict,
|
|
167
188
|
operation_template='path/to/template.operation',
|
|
168
189
|
operation_dict=operation_dict,
|
|
169
|
-
summary='summary.csv',
|
|
170
190
|
options='-s',
|
|
171
191
|
)
|
|
172
192
|
```
|
|
173
193
|
|
|
174
|
-
The `-s` option enables spin-up for the simulations. The results will be consolidated into `summary/
|
|
194
|
+
The `-s` option enables spin-up for the simulations. The results will be consolidated into `summary/harvest.csv`.
|
|
175
195
|
"""
|
|
176
|
-
if
|
|
177
|
-
|
|
178
|
-
assert isinstance(simulations, list)
|
|
196
|
+
if calibration_dict is not None and 'n' not in options:
|
|
197
|
+
warnings.warn('Nudge parameters are provided but Cycles is not running in nudge mode.', UserWarning)
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"operation_template and operation_dict must be provided together or not at all. "
|
|
183
|
-
f"Got operation_template={'None' if operation_template is None else repr(operation_template)}, "
|
|
184
|
-
f"operation_dict={'None' if operation_dict is None else '...'}"
|
|
185
|
-
)
|
|
199
|
+
_check_template_and_dict('crop', crop_template, crop_dict)
|
|
200
|
+
_check_template_and_dict('operation', operation_template, operation_dict)
|
|
186
201
|
|
|
202
|
+
crop_template = Path(crop_template) if crop_template is not None else None
|
|
187
203
|
operation_template = Path(operation_template) if operation_template is not None else None
|
|
188
|
-
if user_comment
|
|
189
|
-
user_comment = f'# {user_comment.lstrip("# ").rstrip()}\n'
|
|
204
|
+
user_comment = f'# {user_comment.lstrip("# ").rstrip()}\n' if user_comment else ''
|
|
190
205
|
comment = user_comment + _generate_comment(self.executable, options)
|
|
191
206
|
first_run = True
|
|
192
207
|
|
|
193
208
|
if summary is None:
|
|
194
|
-
summary =
|
|
195
|
-
|
|
196
|
-
summary = {'harvest'
|
|
197
|
-
assert isinstance(summary, dict)
|
|
209
|
+
summary = ['harvest']
|
|
210
|
+
else:
|
|
211
|
+
summary = list(set(summary) | {'harvest'})
|
|
198
212
|
|
|
199
|
-
for
|
|
200
|
-
if
|
|
201
|
-
control_dict[OUTPUT_CONTROL_FLAGS[
|
|
213
|
+
for s in summary:
|
|
214
|
+
if s == 'harvest': continue
|
|
215
|
+
control_dict[OUTPUT_CONTROL_FLAGS[s]] = 1
|
|
202
216
|
|
|
203
217
|
assert isinstance(self.path, Path)
|
|
204
218
|
(self.path / SUMMARY_DIR).mkdir(exist_ok=True)
|
|
205
219
|
|
|
220
|
+
simulations = _prepare_simulations(simulations)
|
|
221
|
+
|
|
222
|
+
owns_progress = _progress_bar is None and silence and len(simulations) > 1
|
|
223
|
+
progress = tqdm(simulations, unit='simulation', disable=_disable_progress_bar()) if owns_progress else _progress_bar
|
|
224
|
+
|
|
225
|
+
def _report(message: str) -> None:
|
|
226
|
+
tqdm.write(message) if progress is not None else print(message)
|
|
227
|
+
|
|
228
|
+
assert simulations is not None
|
|
206
229
|
for s in simulations:
|
|
207
|
-
cxt: SimulationContext = self._resolve(s, control_dict, operation_dict, calibration_dict)
|
|
208
|
-
|
|
230
|
+
cxt: SimulationContext = self._resolve(s, control_dict, crop_dict, operation_dict, calibration_dict)
|
|
231
|
+
if owns_progress and progress is not None:
|
|
232
|
+
progress.set_description(f'Running {cxt.name}')
|
|
209
233
|
|
|
210
|
-
self._write_inputs(cxt, operation_template)
|
|
234
|
+
self._write_inputs(cxt, crop_template, operation_template)
|
|
211
235
|
|
|
212
|
-
|
|
236
|
+
code, _ = _run_cycles_simulation(self.path, self.executable, cxt.name, options, silence)
|
|
237
|
+
if code != 0:
|
|
238
|
+
_report(f'{cxt.name} - Fail (exit code {code})')
|
|
239
|
+
if progress is not None:
|
|
240
|
+
progress.update()
|
|
213
241
|
|
|
214
|
-
|
|
242
|
+
if s is None:
|
|
243
|
+
return
|
|
215
244
|
|
|
216
245
|
if code == 0:
|
|
217
|
-
|
|
246
|
+
cycles = Cycles(simulation=cxt.name, path=self.path)
|
|
247
|
+
_write_summary(self.path, cycles, summary, summary_prefix, header=first_run, comment=comment)
|
|
218
248
|
first_run = False
|
|
219
|
-
print('Success')
|
|
220
|
-
elif code == 1:
|
|
221
|
-
print('Fail')
|
|
222
|
-
|
|
223
249
|
if rm_input:
|
|
224
250
|
self._remove_inputs(cxt)
|
|
225
251
|
if rm_output:
|
|
@@ -229,21 +255,30 @@ class CyclesRunner:
|
|
|
229
255
|
# If using an existing steady-state soil file, it should not be removed.
|
|
230
256
|
(self.path / INPUT_DIR / f'{cxt.name}_ss.soil').unlink(missing_ok=True)
|
|
231
257
|
|
|
258
|
+
if owns_progress and progress is not None:
|
|
259
|
+
progress.set_description('Done')
|
|
260
|
+
|
|
232
261
|
|
|
233
|
-
def _resolve(self, simulation: dict[str, Any], control_dict: dict[str, Any],
|
|
262
|
+
def _resolve(self, simulation: dict[str, Any] | None, control_dict: dict[str, Any], crop_dict: dict[str, Any] | None, operation_dict: dict[str, Any] | None,
|
|
263
|
+
calibration_dict: dict[str, Any] | None) -> SimulationContext:
|
|
234
264
|
control = _resolve_dict_values(control_dict, simulation)
|
|
235
265
|
assert isinstance(self.path, Path)
|
|
236
266
|
return SimulationContext(
|
|
237
267
|
name=control['simulation_name'],
|
|
238
268
|
control_dict=control,
|
|
269
|
+
crop_dict=_resolve_dict_values(crop_dict, simulation) if crop_dict is not None else None,
|
|
239
270
|
operation_dict=_resolve_dict_values(operation_dict, simulation) if operation_dict is not None else None,
|
|
240
271
|
calibration_dict=_resolve_dict_values(calibration_dict, simulation) if calibration_dict is not None else None,
|
|
241
272
|
operation_fn=self.path / INPUT_DIR / control['operation_file'],
|
|
273
|
+
crop_fn=self.path / INPUT_DIR / control.get('crop_file', DEFAULT_CROP_FILE),
|
|
242
274
|
)
|
|
243
275
|
|
|
244
276
|
|
|
245
|
-
def _write_inputs(self, cxt: SimulationContext, operation_template: Path | None) -> None:
|
|
277
|
+
def _write_inputs(self, cxt: SimulationContext, crop_template: Path | None, operation_template: Path | None) -> None:
|
|
246
278
|
assert isinstance(self.path, Path)
|
|
279
|
+
if crop_template is not None:
|
|
280
|
+
assert cxt.crop_dict is not None
|
|
281
|
+
_render_template(crop_template, cxt.crop_fn, cxt.crop_dict)
|
|
247
282
|
if operation_template is not None:
|
|
248
283
|
assert cxt.operation_dict is not None
|
|
249
284
|
_render_template(operation_template, cxt.operation_fn, cxt.operation_dict)
|
|
@@ -256,21 +291,31 @@ class CyclesRunner:
|
|
|
256
291
|
assert isinstance(self.path, Path)
|
|
257
292
|
(self.path / INPUT_DIR / f'{cxt.name}.ctrl').unlink(missing_ok=True)
|
|
258
293
|
(self.path / INPUT_DIR / f'{cxt.name}.nudge').unlink(missing_ok=True)
|
|
294
|
+
if cxt.crop_dict is not None:
|
|
295
|
+
cxt.crop_fn.unlink(missing_ok=True)
|
|
259
296
|
if cxt.operation_dict is not None:
|
|
260
297
|
cxt.operation_fn.unlink(missing_ok=True)
|
|
261
298
|
|
|
262
299
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
300
|
+
def _write_summary(path: Path, cycles: Cycles, summary: list[str], summary_prefix: str | None, *, header: bool, comment: str) -> None:
|
|
301
|
+
cycles.read_output(summary)
|
|
302
|
+
for s in summary:
|
|
303
|
+
cycles.output[s].data.insert(0, 'simulation', cycles.simulation)
|
|
304
|
+
|
|
305
|
+
mode = 'w' if header else 'a'
|
|
306
|
+
with open(path / SUMMARY_DIR / (f'{summary_prefix}_{s}.csv' if summary_prefix else f'{s}.csv'), mode) as f:
|
|
307
|
+
if header:
|
|
308
|
+
f.write(comment)
|
|
309
|
+
cycles.output[s].data.to_csv(f, header=header, index=False)
|
|
310
|
+
|
|
268
311
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
312
|
+
def _check_template_and_dict(input_type: str, template_name: Path | str | None, dict_name: dict | None):
|
|
313
|
+
if (template_name is None) != (dict_name is None):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"{input_type}_template and {input_type}_dict must be provided together or not at all. "
|
|
316
|
+
f"Got {input_type}_template={'None' if template_name is None else repr(template_name)}, "
|
|
317
|
+
f"{input_type}_dict={'None' if dict_name is None else '...'}"
|
|
318
|
+
)
|
|
274
319
|
|
|
275
320
|
|
|
276
321
|
def _render_template(template_fn: Path, dest_fn: Path, substitutions: dict) -> None:
|
|
@@ -293,3 +338,33 @@ def _generate_comment(executable: str, options: str) -> str:
|
|
|
293
338
|
'dynamically reduced fertilization rates' if 'x' in options else None,
|
|
294
339
|
]
|
|
295
340
|
return ', '.join(p for p in parts if p) + '\n'
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _run_cycles_simulation(path: Path, executable: str, simulation: str, options: str, silence: bool) -> tuple[int, str]:
|
|
344
|
+
cwd = os.getcwd()
|
|
345
|
+
cmd = [executable, *(options.split() if options else []), simulation]
|
|
346
|
+
|
|
347
|
+
os.chdir(path)
|
|
348
|
+
result = subprocess.run(
|
|
349
|
+
cmd,
|
|
350
|
+
shell=os.name == 'nt',
|
|
351
|
+
capture_output=True,
|
|
352
|
+
text=True,
|
|
353
|
+
)
|
|
354
|
+
if not silence:
|
|
355
|
+
print(result.stdout)
|
|
356
|
+
if result.stderr:
|
|
357
|
+
print(result.stderr)
|
|
358
|
+
|
|
359
|
+
os.chdir(cwd)
|
|
360
|
+
|
|
361
|
+
return result.returncode, result.stdout
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _prepare_simulations(simulations: SimulationConfig) -> list:
|
|
365
|
+
if simulations is None:
|
|
366
|
+
simulations = [None]
|
|
367
|
+
if isinstance(simulations, pd.DataFrame):
|
|
368
|
+
simulations = simulations.to_dict(orient='records')
|
|
369
|
+
|
|
370
|
+
return simulations
|
|
@@ -3,7 +3,9 @@ import warnings
|
|
|
3
3
|
from dataclasses import dataclass, field, fields
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, get_type_hints
|
|
6
|
-
from
|
|
6
|
+
from .._base_file import _write_file, _resolve_dict_values, _extract, _parse_value, _unwrap_optional, FMT_1F
|
|
7
|
+
|
|
8
|
+
DEFAULT_CROP_FILE = 'GenericCrops.crop'
|
|
7
9
|
|
|
8
10
|
@dataclass(kw_only=True)
|
|
9
11
|
class SimulationYears:
|
|
@@ -13,7 +15,7 @@ class SimulationYears:
|
|
|
13
15
|
|
|
14
16
|
@dataclass(kw_only=True)
|
|
15
17
|
class InputFiles:
|
|
16
|
-
crop_file: str =
|
|
18
|
+
crop_file: str = DEFAULT_CROP_FILE
|
|
17
19
|
operation_file: str
|
|
18
20
|
soil_file: str
|
|
19
21
|
weather_file: str
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass, field, fields
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import get_type_hints, Any
|
|
4
|
-
from
|
|
4
|
+
from .._base_file import _unwrap_optional, _format_field, FMT_1F, FMT_2F, FMT_3F
|
|
5
5
|
from .operation_file import Planting, Tillage, FixedFertilization
|
|
6
6
|
from .operation_file import generate_operation_file
|
|
7
7
|
from .crop_file import Crop, generate_crop_file
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
from dataclasses import dataclass, field, fields
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any, get_type_hints, Protocol
|
|
5
|
-
from
|
|
6
|
-
from
|
|
5
|
+
from .._base_file import _write_file, _resolve_dict_values, _extract, _parse_value, _unwrap_optional, _format_block
|
|
6
|
+
from .._base_file import FMT_1F, FMT_2F, FMT_3F, FMT_4F
|
|
7
7
|
|
|
8
8
|
@dataclass(kw_only=True)
|
|
9
9
|
class Phenology:
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
|
-
from
|
|
6
|
-
from
|
|
5
|
+
from .._base_file import _write_file, _resolve_dict_values, _extract
|
|
6
|
+
from .._base_file import FMT_2F
|
|
7
7
|
|
|
8
8
|
@dataclass(kw_only=True)
|
|
9
9
|
class CalibrationMultipliers:
|
|
@@ -18,6 +18,7 @@ class CalibrationMultipliers:
|
|
|
18
18
|
nitrif_rate: float = field(default=1.0, metadata={'description': 'nitrification rate', 'fmt': FMT_2F})
|
|
19
19
|
pot_denitrif_rate: float = field(default=1.0, metadata={'description': 'potential denitrification rate', 'fmt': FMT_2F})
|
|
20
20
|
denitrif_half_rate: float = field(default=1.0, metadata={'description': 'half saturation constant for denitrification', 'fmt': FMT_2F})
|
|
21
|
+
n2o_respiration_factor: float = field(default=1.0, metadata={'description': 'respiration factor that modulates proportion of N2O from denitrification', 'fmt': FMT_2F})
|
|
21
22
|
decomp_half_resp: float = field(default=1.0, metadata={'description': 'decomposition half response to saturation (default 0.22)', 'fmt': FMT_2F})
|
|
22
23
|
decomp_resp_power: float = field(default=1.0, metadata={'description': 'decomposition exponential response to saturation (default 3.0)', 'fmt': FMT_2F})
|
|
23
24
|
root_progression: float = field(default=1.0, metadata={'description': 'rooting depth progression rate', 'fmt': FMT_2F})
|
|
@@ -3,7 +3,7 @@ import re
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from dataclasses import dataclass, field, fields
|
|
5
5
|
from typing import get_type_hints, Protocol, Any
|
|
6
|
-
from
|
|
6
|
+
from .._base_file import _parse_value, FMT_1F, FMT_2F, _format_field
|
|
7
7
|
|
|
8
8
|
class Operation(Protocol):
|
|
9
9
|
year: int | None
|