simpest 0.0.1__py2.py3-none-any.whl
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.
- simpest/__init__.py +5 -0
- simpest/common.py +7 -0
- simpest/models/__init__.py +10 -0
- simpest/models/franchestyn.py +281 -0
- simpest/models/simplace.py +213 -0
- simpest/pipeline.py +104 -0
- simpest/plot.py +137 -0
- simpest/simpest.py +1 -0
- simpest-0.0.1.dist-info/METADATA +44 -0
- simpest-0.0.1.dist-info/RECORD +15 -0
- simpest-0.0.1.dist-info/WHEEL +6 -0
- simpest-0.0.1.dist-info/entry_points.txt +2 -0
- simpest-0.0.1.dist-info/licenses/AUTHORS.rst +13 -0
- simpest-0.0.1.dist-info/licenses/LICENSE +22 -0
- simpest-0.0.1.dist-info/top_level.txt +1 -0
simpest/__init__.py
ADDED
simpest/common.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import importlib.util
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from franchestyn.optimizer import FranchestynOptimizer
|
|
9
|
+
from franchestyn.runner import FranchestynRunner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolve_default_reference() -> str:
|
|
13
|
+
"""Resolve a local default reference CSV using repository files only."""
|
|
14
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
15
|
+
candidates = [
|
|
16
|
+
repo_root
|
|
17
|
+
/ "franchestyn"
|
|
18
|
+
/ "src_csharp"
|
|
19
|
+
/ "FraNchEstYN"
|
|
20
|
+
/ "FraNchEstYN"
|
|
21
|
+
/ "files"
|
|
22
|
+
/ "reference"
|
|
23
|
+
/ "Indiana.csv",
|
|
24
|
+
repo_root
|
|
25
|
+
/ "franchestyn"
|
|
26
|
+
/ "src_csharp"
|
|
27
|
+
/ "FraNchEstYN"
|
|
28
|
+
/ "FraNchEstYN"
|
|
29
|
+
/ "bin"
|
|
30
|
+
/ "Debug"
|
|
31
|
+
/ "net8.0"
|
|
32
|
+
/ "files"
|
|
33
|
+
/ "reference"
|
|
34
|
+
/ "Indiana.csv",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for path in candidates:
|
|
38
|
+
if path.exists():
|
|
39
|
+
return str(path)
|
|
40
|
+
|
|
41
|
+
# Fall back to a repository-local canonical path if files are not present yet.
|
|
42
|
+
return str(candidates[0])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_franchestyn_param_file() -> str:
|
|
46
|
+
pkg = Path(importlib.util.find_spec("franchestyn").origin).parent
|
|
47
|
+
return str(pkg / "parameters_by_crop.json")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class FranchestynConfig:
|
|
52
|
+
param_file: str = _resolve_franchestyn_param_file()
|
|
53
|
+
reference_path: str = _resolve_default_reference()
|
|
54
|
+
crop_type: str = "wheat"
|
|
55
|
+
disease_type: str = "septoria"
|
|
56
|
+
fungicide_type: str | None = "protectant"
|
|
57
|
+
site: str = "indiana"
|
|
58
|
+
variety: str = "Generic"
|
|
59
|
+
disease: str = "thisDisease"
|
|
60
|
+
is_calibration: bool = True
|
|
61
|
+
calibration_variable: str = "all"
|
|
62
|
+
use_gdd: bool = True
|
|
63
|
+
n_restarts: int = 1
|
|
64
|
+
max_iter: int = 100
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _outputs_to_records(date_outputs):
|
|
68
|
+
records = []
|
|
69
|
+
for dt, out in sorted(date_outputs.items()):
|
|
70
|
+
records.append(
|
|
71
|
+
{
|
|
72
|
+
"Date": dt.strftime("%d/%m/%Y"),
|
|
73
|
+
"DaysAfterSowing": out.crop.day_after_sowing,
|
|
74
|
+
"GrowingDegreeDays": out.crop.growing_degree_days,
|
|
75
|
+
"CycleCompletionPercentage": out.crop.cycle_completion_percentage,
|
|
76
|
+
"LightInterception": out.crop.light_interception_attainable,
|
|
77
|
+
"LightIntHealthy": out.crop.light_interception_actual,
|
|
78
|
+
"AGBattainable": out.crop.agb_attainable,
|
|
79
|
+
"AGBactual": out.crop.agb_actual,
|
|
80
|
+
"YieldAttainable": out.crop.yield_attainable,
|
|
81
|
+
"YieldActual": out.crop.yield_actual,
|
|
82
|
+
"HTtimeRinoculum": out.disease.hydro_thermal_time_rate,
|
|
83
|
+
"HTtimeSinoculum": out.disease.hydro_thermal_time_state,
|
|
84
|
+
"Susceptible": out.disease.susceptible_fraction,
|
|
85
|
+
"Latent": out.disease.latent_sum,
|
|
86
|
+
"Sporulating": out.disease.sporulating_sum,
|
|
87
|
+
"Affected": out.disease.affected_sum,
|
|
88
|
+
"Dead": out.disease.dead_sum,
|
|
89
|
+
"DiseaseSeverity": out.disease.disease_severity,
|
|
90
|
+
"FungicideEfficacy": out.fungicide.efficacy,
|
|
91
|
+
"Tmax": out.inputs_daily.tmax if out.inputs_daily else None,
|
|
92
|
+
"Tmin": out.inputs_daily.tmin if out.inputs_daily else None,
|
|
93
|
+
"RHx": out.inputs_daily.rhx if out.inputs_daily else None,
|
|
94
|
+
"RHn": out.inputs_daily.rhn if out.inputs_daily else None,
|
|
95
|
+
"TotalPrec": out.inputs_daily.precipitation if out.inputs_daily else None,
|
|
96
|
+
"TotalRad": out.inputs_daily.rad if out.inputs_daily else None,
|
|
97
|
+
"TotalLW": out.inputs_daily.leaf_wetness if out.inputs_daily else None,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
return records
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_franchestyn(
|
|
104
|
+
weather_path: str,
|
|
105
|
+
management_path: str,
|
|
106
|
+
start_year: int,
|
|
107
|
+
end_year: int,
|
|
108
|
+
config: FranchestynConfig,
|
|
109
|
+
cropmodel_path: str | None = None,
|
|
110
|
+
crop_param_file: str | None = None,
|
|
111
|
+
disease_param_file: str | None = None,
|
|
112
|
+
fungicide_param_file: str | None = None,
|
|
113
|
+
) -> dict:
|
|
114
|
+
runner = FranchestynRunner(
|
|
115
|
+
weather_dir=weather_path,
|
|
116
|
+
param_file=config.param_file,
|
|
117
|
+
sowing_file=management_path,
|
|
118
|
+
ref_dir=config.reference_path,
|
|
119
|
+
crop_model_dir=cropmodel_path,
|
|
120
|
+
site=config.site,
|
|
121
|
+
variety=config.variety,
|
|
122
|
+
disease=config.disease,
|
|
123
|
+
start_year=start_year,
|
|
124
|
+
end_year=end_year,
|
|
125
|
+
weather_time_step="daily",
|
|
126
|
+
calibration_variable=config.calibration_variable,
|
|
127
|
+
is_calibration=config.is_calibration,
|
|
128
|
+
use_gdd=config.use_gdd,
|
|
129
|
+
crop_type=config.crop_type,
|
|
130
|
+
crop_param_file=crop_param_file,
|
|
131
|
+
disease_param_file=disease_param_file,
|
|
132
|
+
disease_type=config.disease_type,
|
|
133
|
+
fungicide_param_file=fungicide_param_file,
|
|
134
|
+
fungicide_type=config.fungicide_type,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
best_params = {}
|
|
138
|
+
if config.is_calibration:
|
|
139
|
+
optimizer = FranchestynOptimizer(
|
|
140
|
+
runner=runner,
|
|
141
|
+
calibration_variable=config.calibration_variable,
|
|
142
|
+
n_restarts=config.n_restarts,
|
|
143
|
+
max_iter=config.max_iter,
|
|
144
|
+
)
|
|
145
|
+
best_params = optimizer.calibrate()
|
|
146
|
+
date_outputs = runner.run(param_values=best_params)
|
|
147
|
+
else:
|
|
148
|
+
date_outputs = runner.run()
|
|
149
|
+
|
|
150
|
+
include_crop = config.calibration_variable in ("crop", "all")
|
|
151
|
+
include_disease = config.calibration_variable in ("disease", "all")
|
|
152
|
+
rmse = runner.compute_rmse(
|
|
153
|
+
date_outputs,
|
|
154
|
+
include_crop=include_crop,
|
|
155
|
+
include_disease=include_disease,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
records = _outputs_to_records(date_outputs)
|
|
159
|
+
return {
|
|
160
|
+
"outputs": {
|
|
161
|
+
"simulation": records,
|
|
162
|
+
"summary": {
|
|
163
|
+
"rmse": rmse,
|
|
164
|
+
"is_calibration": config.is_calibration,
|
|
165
|
+
"calibration_variable": config.calibration_variable,
|
|
166
|
+
"best_params": best_params,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def save_simulation_results_csv(res_ot_simulation, output_root: Path, filename: str = "franchestyn_simulation_results.csv") -> Path:
|
|
173
|
+
output_file = output_root / "SimulationExperimentTemplate" / filename
|
|
174
|
+
df = pd.DataFrame(res_ot_simulation)
|
|
175
|
+
df.to_csv(output_file, index=False)
|
|
176
|
+
return output_file
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def build_season_summary(df: pd.DataFrame, site: str, variety: str) -> pd.DataFrame:
|
|
180
|
+
if df.empty:
|
|
181
|
+
return pd.DataFrame()
|
|
182
|
+
|
|
183
|
+
d = df.copy()
|
|
184
|
+
d["Date"] = pd.to_datetime(d["Date"], dayfirst=True, errors="coerce")
|
|
185
|
+
d = d.dropna(subset=["Date"])
|
|
186
|
+
|
|
187
|
+
if "DaysAfterSowing" in d.columns:
|
|
188
|
+
d = d[d["DaysAfterSowing"] > 0]
|
|
189
|
+
if d.empty:
|
|
190
|
+
return pd.DataFrame()
|
|
191
|
+
|
|
192
|
+
if "GrowingSeason" not in d.columns:
|
|
193
|
+
d["GrowingSeason"] = d["Date"].dt.year
|
|
194
|
+
|
|
195
|
+
rows = []
|
|
196
|
+
for season, g in d.groupby("GrowingSeason"):
|
|
197
|
+
g = g.sort_values("Date")
|
|
198
|
+
|
|
199
|
+
y = g["DiseaseSeverity"].fillna(0.0).to_numpy(dtype=float) * 100.0
|
|
200
|
+
x = g["Date"].map(pd.Timestamp.toordinal).to_numpy(dtype=float)
|
|
201
|
+
audpc = float(np.trapezoid(y, x)) if len(g) >= 2 else 0.0
|
|
202
|
+
|
|
203
|
+
yield_att = float(g["YieldAttainable"].max()) if "YieldAttainable" in g.columns else 0.0
|
|
204
|
+
yield_act = float(g["YieldActual"].max()) if "YieldActual" in g.columns else 0.0
|
|
205
|
+
agb_att = float(g["AGBattainable"].max()) if "AGBattainable" in g.columns else 0.0
|
|
206
|
+
agb_act = float(g["AGBactual"].max()) if "AGBactual" in g.columns else 0.0
|
|
207
|
+
dis_sev = float(g["DiseaseSeverity"].max()) if "DiseaseSeverity" in g.columns else 0.0
|
|
208
|
+
|
|
209
|
+
loss_raw = yield_att - yield_act
|
|
210
|
+
loss_perc = (loss_raw / yield_att * 100.0) if yield_att > 0 else 0.0
|
|
211
|
+
|
|
212
|
+
row = {
|
|
213
|
+
"GrowingSeason": int(season),
|
|
214
|
+
"Site": site,
|
|
215
|
+
"Variety": variety,
|
|
216
|
+
"AUDPC": audpc,
|
|
217
|
+
"DiseaseSeverity": dis_sev,
|
|
218
|
+
"YieldAttainable": yield_att,
|
|
219
|
+
"YieldActual": yield_act,
|
|
220
|
+
"YieldLossRaw": loss_raw,
|
|
221
|
+
"YieldLossPerc": loss_perc,
|
|
222
|
+
"AGBattainable": agb_att,
|
|
223
|
+
"AGBactual": agb_act,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if "Tmax" in g.columns:
|
|
227
|
+
row["AveTx"] = float(g["Tmax"].mean())
|
|
228
|
+
if "Tmin" in g.columns:
|
|
229
|
+
row["AveTn"] = float(g["Tmin"].mean())
|
|
230
|
+
if "RHx" in g.columns:
|
|
231
|
+
row["AveRHx"] = float(g["RHx"].mean())
|
|
232
|
+
if "RHn" in g.columns:
|
|
233
|
+
row["AveRHn"] = float(g["RHn"].mean())
|
|
234
|
+
if "TotalPrec" in g.columns:
|
|
235
|
+
row["TotalPrec"] = float(g["TotalPrec"].sum())
|
|
236
|
+
if "TotalRad" in g.columns:
|
|
237
|
+
row["TotalRad"] = float(g["TotalRad"].sum())
|
|
238
|
+
if "TotalLW" in g.columns:
|
|
239
|
+
row["TotalLW"] = float(g["TotalLW"].sum())
|
|
240
|
+
|
|
241
|
+
rows.append(row)
|
|
242
|
+
|
|
243
|
+
return pd.DataFrame(rows).sort_values("GrowingSeason").reset_index(drop=True)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def save_season_summary_csv(summary_df: pd.DataFrame, output_root: Path, filename: str = "franchestyn_season_summary.csv") -> Path | None:
|
|
247
|
+
if summary_df.empty:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
output_file = output_root / "SimulationExperimentTemplate" / filename
|
|
251
|
+
summary_df.to_csv(output_file, index=False)
|
|
252
|
+
return output_file
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def save_calibrated_parameters_csv(
|
|
256
|
+
best_params: dict,
|
|
257
|
+
output_root: Path,
|
|
258
|
+
site: str,
|
|
259
|
+
variety: str,
|
|
260
|
+
filename: str | None = None,
|
|
261
|
+
) -> Path | None:
|
|
262
|
+
if not best_params:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
out_dir = output_root / "SimulationExperimentTemplate" / "calibratedParameters"
|
|
266
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
if filename is None:
|
|
268
|
+
filename = f"calibratedParameters_{site}_{variety}.csv"
|
|
269
|
+
output_file = out_dir / filename
|
|
270
|
+
|
|
271
|
+
with output_file.open("w", newline="", encoding="utf-8") as f_out:
|
|
272
|
+
writer = csv.DictWriter(f_out, fieldnames=["model", "param", "value"], delimiter=",")
|
|
273
|
+
writer.writeheader()
|
|
274
|
+
for key, value in sorted(best_params.items()):
|
|
275
|
+
if "_" in key:
|
|
276
|
+
model, param = key.split("_", 1)
|
|
277
|
+
else:
|
|
278
|
+
model, param = "", key
|
|
279
|
+
writer.writerow({"model": model, "param": param, "value": value})
|
|
280
|
+
|
|
281
|
+
return output_file
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import math
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import jpype
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import simplace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SIMPLACE_INSTANCE = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SimplaceConfig:
|
|
17
|
+
install_dir: str = "C:/ParamVC/Research/simplace+/simplace_portable/workspace/"
|
|
18
|
+
work_dir: str = "C:/ParamVC/Research/simplace+/simplace_portable/workspace/simplace_run/simulation/"
|
|
19
|
+
output_dir: str = "C:/ParamVC/Research/simplace+/simplace_out/"
|
|
20
|
+
solution_path: str = "SimulationExperimentTemplate/solution/Lintul5_indiana.sol.xml"
|
|
21
|
+
project_path: str = "SimulationExperimentTemplate/project/Lintul5All_indiana.proj.xml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def init_simplace(config: SimplaceConfig):
|
|
25
|
+
global _SIMPLACE_INSTANCE
|
|
26
|
+
|
|
27
|
+
if _SIMPLACE_INSTANCE is not None and jpype.isJVMStarted():
|
|
28
|
+
return _SIMPLACE_INSTANCE
|
|
29
|
+
|
|
30
|
+
if jpype.isJVMStarted():
|
|
31
|
+
Wrapper = jpype.JClass('net.simplace.sim.wrapper.SimplaceWrapper')
|
|
32
|
+
_SIMPLACE_INSTANCE = Wrapper(config.work_dir, config.output_dir, None, None)
|
|
33
|
+
return _SIMPLACE_INSTANCE
|
|
34
|
+
|
|
35
|
+
_SIMPLACE_INSTANCE = simplace.initSimplace(
|
|
36
|
+
installDir=config.install_dir,
|
|
37
|
+
workDir=config.work_dir,
|
|
38
|
+
outputDir=config.output_dir,
|
|
39
|
+
)
|
|
40
|
+
return _SIMPLACE_INSTANCE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_simplace(shell, config: SimplaceConfig, project_lines: list[int]):
|
|
44
|
+
simplace.openProject(shell, config.solution_path, config.project_path)
|
|
45
|
+
simplace.setProjectLines(shell, project_lines)
|
|
46
|
+
simplace.runProject(shell)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_project_row(work_root: Path, selected_line: int) -> dict:
|
|
50
|
+
project_csv = (
|
|
51
|
+
work_root
|
|
52
|
+
/ "SimulationExperimentTemplate"
|
|
53
|
+
/ "data"
|
|
54
|
+
/ "projectdata"
|
|
55
|
+
/ "lintul5all_indiana.csv"
|
|
56
|
+
)
|
|
57
|
+
with project_csv.open(newline="", encoding="utf-8") as f_proj:
|
|
58
|
+
proj_reader = csv.reader(f_proj, delimiter=";")
|
|
59
|
+
_ = next(proj_reader, None)
|
|
60
|
+
project_rows = list(proj_reader)
|
|
61
|
+
|
|
62
|
+
row = project_rows[selected_line - 1]
|
|
63
|
+
return {
|
|
64
|
+
"projectid": row[0],
|
|
65
|
+
"simulationid": row[1],
|
|
66
|
+
"startdate": row[2],
|
|
67
|
+
"enddate": row[3],
|
|
68
|
+
"location": row[4],
|
|
69
|
+
"iopt": row[5],
|
|
70
|
+
"idem": int(row[6]),
|
|
71
|
+
"irri": row[7],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def export_crop_model_data(output_root: Path, project_row: dict) -> Path:
|
|
76
|
+
src = output_root / "SimulationExperimentTemplate" / f"{project_row['location']}{project_row['iopt']}_daily.csv"
|
|
77
|
+
dst = output_root / "SimulationExperimentTemplate" / "cropModel_data.csv"
|
|
78
|
+
|
|
79
|
+
with src.open(newline="", encoding="utf-8") as f_in, dst.open("w", newline="", encoding="utf-8") as f_out:
|
|
80
|
+
reader = csv.DictReader(f_in, delimiter=";")
|
|
81
|
+
writer = csv.DictWriter(
|
|
82
|
+
f_out,
|
|
83
|
+
fieldnames=["year", "doy", "agb", "yield", "fint", "lai", "gdd"],
|
|
84
|
+
delimiter=",",
|
|
85
|
+
)
|
|
86
|
+
writer.writeheader()
|
|
87
|
+
|
|
88
|
+
for row in reader:
|
|
89
|
+
if all(float(row[name]) == 0.0 for name in ("TAGB", "WSO", "FINT", "LAI")):
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
d = datetime.strptime(row["CURRENT.DATE"], "%d.%m.%Y")
|
|
93
|
+
writer.writerow(
|
|
94
|
+
{
|
|
95
|
+
"year": d.year,
|
|
96
|
+
"doy": d.timetuple().tm_yday,
|
|
97
|
+
"agb": float(row["TAGB"]) * 10,
|
|
98
|
+
"yield": float(row["WSO"]) * 10,
|
|
99
|
+
"fint": row["FINT"],
|
|
100
|
+
"lai": row["LAI"],
|
|
101
|
+
"gdd": row["TSUM"],
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return dst
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _saturation_vapor_pressure(t_celsius: float) -> float:
|
|
108
|
+
return 0.6108 * math.exp((17.27 * t_celsius) / (t_celsius + 237.3))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def convert_weather(work_root: Path, output_root: Path, location: str) -> Path:
|
|
112
|
+
weather_src = work_root / "SimulationExperimentTemplate" / "data" / "weather" / f"{location}.txt"
|
|
113
|
+
weather_dst = output_root / "SimulationExperimentTemplate" / "weather_franchestyn.csv"
|
|
114
|
+
|
|
115
|
+
with weather_src.open(newline="", encoding="utf-8-sig") as f_in, weather_dst.open(
|
|
116
|
+
"w", newline="", encoding="utf-8"
|
|
117
|
+
) as f_out:
|
|
118
|
+
reader = csv.DictReader(f_in, delimiter="\t")
|
|
119
|
+
reader.fieldnames = [h.lstrip("\ufeff").strip() for h in reader.fieldnames]
|
|
120
|
+
|
|
121
|
+
writer = csv.DictWriter(
|
|
122
|
+
f_out,
|
|
123
|
+
fieldnames=["site", "year", "month", "day", "tx", "tn", "p", "rad", "vp", "rhx", "rhn"],
|
|
124
|
+
delimiter=",",
|
|
125
|
+
)
|
|
126
|
+
writer.writeheader()
|
|
127
|
+
|
|
128
|
+
for row in reader:
|
|
129
|
+
d = datetime.strptime(row["CURRENTDAY"], "%d.%m.%Y")
|
|
130
|
+
tmax = float(row["Tmax"])
|
|
131
|
+
tmin = float(row["Tmin"])
|
|
132
|
+
vp = float(row["VapourPressure"])
|
|
133
|
+
|
|
134
|
+
es_tmax = _saturation_vapor_pressure(tmax)
|
|
135
|
+
es_tmin = _saturation_vapor_pressure(tmin)
|
|
136
|
+
rhx = min(100.0, max(0.0, 100.0 * vp / es_tmin)) if es_tmin > 0 else 0.0
|
|
137
|
+
rhn = min(100.0, max(0.0, 100.0 * vp / es_tmax)) if es_tmax > 0 else 0.0
|
|
138
|
+
|
|
139
|
+
writer.writerow(
|
|
140
|
+
{
|
|
141
|
+
"site": location,
|
|
142
|
+
"year": d.year,
|
|
143
|
+
"month": d.month,
|
|
144
|
+
"day": d.day,
|
|
145
|
+
"tx": tmax,
|
|
146
|
+
"tn": tmin,
|
|
147
|
+
"p": row["Precipitation"],
|
|
148
|
+
"rad": float(row["Irradiation"]) / 1000.0,
|
|
149
|
+
"vp": vp,
|
|
150
|
+
"rhx": rhx,
|
|
151
|
+
"rhn": rhn,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return weather_dst
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_management(output_root: Path, project_row: dict, crop: str = "wheat", variety: str = "generic") -> Path:
|
|
159
|
+
start_date = datetime.strptime(project_row["startdate"], "%d.%m.%Y")
|
|
160
|
+
_ = start_date
|
|
161
|
+
sowing_doy = max(1, project_row["idem"] - 7)
|
|
162
|
+
|
|
163
|
+
management_dst = output_root / "SimulationExperimentTemplate" / "management_franchestyn.csv"
|
|
164
|
+
with management_dst.open("w", newline="", encoding="utf-8") as f_out:
|
|
165
|
+
writer = csv.DictWriter(
|
|
166
|
+
f_out,
|
|
167
|
+
fieldnames=["site", "crop", "variety", "year", "sowingDOY"],
|
|
168
|
+
delimiter=",",
|
|
169
|
+
)
|
|
170
|
+
writer.writeheader()
|
|
171
|
+
writer.writerow(
|
|
172
|
+
{
|
|
173
|
+
"site": project_row["location"],
|
|
174
|
+
"crop": crop,
|
|
175
|
+
"variety": variety,
|
|
176
|
+
"year": "All",
|
|
177
|
+
"sowingDOY": sowing_doy,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return management_dst
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def merge_simplace_and_franchestyn(
|
|
185
|
+
output_root: Path,
|
|
186
|
+
project_row: dict,
|
|
187
|
+
franchestyn_df: pd.DataFrame,
|
|
188
|
+
out_name: str = "merged_simulation_data.csv",
|
|
189
|
+
) -> Path:
|
|
190
|
+
simplace_daily_path = output_root / "SimulationExperimentTemplate" / f"{project_row['location']}{project_row['iopt']}_daily.csv"
|
|
191
|
+
simplace_df = pd.read_csv(simplace_daily_path, sep=";")
|
|
192
|
+
|
|
193
|
+
simplace_df.columns = simplace_df.columns.str.strip()
|
|
194
|
+
franchestyn_df.columns = franchestyn_df.columns.str.strip()
|
|
195
|
+
|
|
196
|
+
simplace_df["date"] = pd.to_datetime(simplace_df["CURRENT.DATE"], format="%d.%m.%Y")
|
|
197
|
+
franchestyn_df["date"] = pd.to_datetime(franchestyn_df["Date"], dayfirst=True)
|
|
198
|
+
|
|
199
|
+
sim_df = simplace_df.rename(columns={c: f"{c}_S" for c in simplace_df.columns if c != "date"})
|
|
200
|
+
fran_df = franchestyn_df.rename(columns={c: f"{c}_F" for c in franchestyn_df.columns if c != "date"})
|
|
201
|
+
|
|
202
|
+
merged = pd.merge(sim_df, fran_df, on="date", how="left")
|
|
203
|
+
merged = merged.rename(
|
|
204
|
+
columns={
|
|
205
|
+
"WSO_S": "WSO_S_g_m2",
|
|
206
|
+
"YieldAttainable_F": "YieldAttainable_F_kg_ha",
|
|
207
|
+
"YieldActual_F": "YieldActual_F_kg_ha",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
out_path = output_root / "SimulationExperimentTemplate" / out_name
|
|
212
|
+
merged.to_csv(out_path, index=False)
|
|
213
|
+
return out_path
|
simpest/pipeline.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import simplace
|
|
5
|
+
|
|
6
|
+
from .models.franchestyn import (
|
|
7
|
+
FranchestynConfig,
|
|
8
|
+
build_season_summary,
|
|
9
|
+
run_franchestyn,
|
|
10
|
+
save_calibrated_parameters_csv,
|
|
11
|
+
save_season_summary_csv,
|
|
12
|
+
save_simulation_results_csv,
|
|
13
|
+
)
|
|
14
|
+
from .models.simplace import (
|
|
15
|
+
SimplaceConfig,
|
|
16
|
+
build_management,
|
|
17
|
+
convert_weather,
|
|
18
|
+
export_crop_model_data,
|
|
19
|
+
get_project_row,
|
|
20
|
+
init_simplace,
|
|
21
|
+
merge_simplace_and_franchestyn,
|
|
22
|
+
run_simplace,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_pipeline(
|
|
27
|
+
simplace_config: SimplaceConfig,
|
|
28
|
+
franchestyn_config: FranchestynConfig,
|
|
29
|
+
project_lines: list[int],
|
|
30
|
+
shutdown_simplace: bool = False,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Run the full SIMPLACE -> FraNchEstYN pipeline.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
shutdown_simplace : bool
|
|
37
|
+
If True, calls ``simplace.shutDown(shell)`` in ``finally``.
|
|
38
|
+
Keep this False in Jupyter to avoid possible kernel restarts when
|
|
39
|
+
the underlying JVM is terminated.
|
|
40
|
+
"""
|
|
41
|
+
shell = init_simplace(simplace_config)
|
|
42
|
+
try:
|
|
43
|
+
run_simplace(shell, simplace_config, project_lines=project_lines)
|
|
44
|
+
|
|
45
|
+
dirs = simplace.getSimplaceDirectories(shell)
|
|
46
|
+
work_root = Path(dirs["_WORKDIR_"])
|
|
47
|
+
output_root = Path(dirs["_OUTPUTDIR_"])
|
|
48
|
+
|
|
49
|
+
selected_line = project_lines[0]
|
|
50
|
+
project_row = get_project_row(work_root, selected_line=selected_line)
|
|
51
|
+
|
|
52
|
+
crop_model_path = export_crop_model_data(output_root, project_row)
|
|
53
|
+
weather_path = convert_weather(work_root, output_root, project_row["location"])
|
|
54
|
+
management_path = build_management(output_root, project_row)
|
|
55
|
+
|
|
56
|
+
start_year = int(project_row["startdate"].split(".")[-1])
|
|
57
|
+
end_year = int(project_row["enddate"].split(".")[-1])
|
|
58
|
+
|
|
59
|
+
result = run_franchestyn(
|
|
60
|
+
weather_path=str(weather_path),
|
|
61
|
+
management_path=str(management_path),
|
|
62
|
+
cropmodel_path=str(crop_model_path),
|
|
63
|
+
start_year=start_year,
|
|
64
|
+
end_year=end_year,
|
|
65
|
+
config=franchestyn_config,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
summary = result.get("outputs", {}).get("summary", {})
|
|
69
|
+
simulation = result.get("outputs", {}).get("simulation", [])
|
|
70
|
+
|
|
71
|
+
save_simulation_results_csv(simulation, output_root)
|
|
72
|
+
|
|
73
|
+
simulation_df = pd.DataFrame(simulation)
|
|
74
|
+
season_summary = build_season_summary(
|
|
75
|
+
simulation_df,
|
|
76
|
+
site=franchestyn_config.site,
|
|
77
|
+
variety=franchestyn_config.variety,
|
|
78
|
+
)
|
|
79
|
+
save_season_summary_csv(season_summary, output_root)
|
|
80
|
+
|
|
81
|
+
best_params = summary.get("best_params", {}) if isinstance(summary, dict) else {}
|
|
82
|
+
save_calibrated_parameters_csv(
|
|
83
|
+
best_params,
|
|
84
|
+
output_root,
|
|
85
|
+
site=franchestyn_config.site,
|
|
86
|
+
variety=franchestyn_config.variety,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
merged_path = merge_simplace_and_franchestyn(output_root, project_row, simulation_df)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"work_root": str(work_root),
|
|
93
|
+
"output_root": str(output_root),
|
|
94
|
+
"project_row": project_row,
|
|
95
|
+
"merged_csv": str(merged_path),
|
|
96
|
+
"result": result,
|
|
97
|
+
"simplace_shutdown": bool(shutdown_simplace),
|
|
98
|
+
}
|
|
99
|
+
finally:
|
|
100
|
+
if shutdown_simplace:
|
|
101
|
+
try:
|
|
102
|
+
simplace.shutDown(shell)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
print(f"Warning: SIMPLACE shutdown failed: {exc}")
|
simpest/plot.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def plot_distribution_lines(
|
|
11
|
+
df: pd.DataFrame,
|
|
12
|
+
variables: Sequence[str],
|
|
13
|
+
*,
|
|
14
|
+
bins: int = 50,
|
|
15
|
+
figsize: tuple[float, float] = (12, 5),
|
|
16
|
+
title: str = "Distribution Line Plot",
|
|
17
|
+
xlabel: str = "Value",
|
|
18
|
+
ylabel: str = "Density",
|
|
19
|
+
ax=None,
|
|
20
|
+
):
|
|
21
|
+
"""Plot line-based distributions for a list of numeric variables.
|
|
22
|
+
|
|
23
|
+
This function draws one line per variable using histogram-bin centers,
|
|
24
|
+
so the output stays a line plot while still showing value distribution.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
df : pd.DataFrame
|
|
29
|
+
Source table containing the variables.
|
|
30
|
+
variables : Sequence[str]
|
|
31
|
+
Column names to include in the distribution line plot.
|
|
32
|
+
bins : int, optional
|
|
33
|
+
Number of histogram bins used for each variable. Default is 50.
|
|
34
|
+
figsize : tuple[float, float], optional
|
|
35
|
+
Figure size used when creating a new figure.
|
|
36
|
+
title, xlabel, ylabel : str, optional
|
|
37
|
+
Plot labels.
|
|
38
|
+
ax : matplotlib.axes.Axes, optional
|
|
39
|
+
Existing axes to draw on. If None, a new figure/axes is created.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]
|
|
44
|
+
Figure and axes containing the plot.
|
|
45
|
+
"""
|
|
46
|
+
if not variables:
|
|
47
|
+
raise ValueError("'variables' cannot be empty.")
|
|
48
|
+
|
|
49
|
+
missing = [col for col in variables if col not in df.columns]
|
|
50
|
+
if missing:
|
|
51
|
+
raise ValueError(f"Variables not found in DataFrame: {missing}")
|
|
52
|
+
|
|
53
|
+
created_fig = False
|
|
54
|
+
if ax is None:
|
|
55
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
56
|
+
created_fig = True
|
|
57
|
+
else:
|
|
58
|
+
fig = ax.figure
|
|
59
|
+
|
|
60
|
+
plotted = 0
|
|
61
|
+
for var in variables:
|
|
62
|
+
values = pd.to_numeric(df[var], errors="coerce").dropna().to_numpy(dtype=float)
|
|
63
|
+
if values.size == 0:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
hist, edges = np.histogram(values, bins=bins, density=True)
|
|
67
|
+
centers = (edges[:-1] + edges[1:]) / 2.0
|
|
68
|
+
ax.plot(centers, hist, linewidth=2, label=var)
|
|
69
|
+
plotted += 1
|
|
70
|
+
|
|
71
|
+
if plotted == 0:
|
|
72
|
+
raise ValueError("No numeric data available to plot for the selected variables.")
|
|
73
|
+
|
|
74
|
+
ax.set_title(title)
|
|
75
|
+
ax.set_xlabel(xlabel)
|
|
76
|
+
ax.set_ylabel(ylabel)
|
|
77
|
+
ax.grid(True, alpha=0.3)
|
|
78
|
+
ax.legend()
|
|
79
|
+
|
|
80
|
+
if created_fig:
|
|
81
|
+
fig.tight_layout()
|
|
82
|
+
|
|
83
|
+
return fig, ax
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def plot_timeseries_lines(
|
|
87
|
+
df: pd.DataFrame,
|
|
88
|
+
x: str,
|
|
89
|
+
variables: Sequence[str],
|
|
90
|
+
*,
|
|
91
|
+
figsize: tuple[float, float] = (14, 5),
|
|
92
|
+
title: str = "Time Series Plot",
|
|
93
|
+
xlabel: str = "Date",
|
|
94
|
+
ylabel: str = "Value",
|
|
95
|
+
ax=None,
|
|
96
|
+
):
|
|
97
|
+
"""Plot multiple variables as time-series lines against a date/time column."""
|
|
98
|
+
if x not in df.columns:
|
|
99
|
+
raise ValueError(f"x column '{x}' not found in DataFrame.")
|
|
100
|
+
if not variables:
|
|
101
|
+
raise ValueError("'variables' cannot be empty.")
|
|
102
|
+
|
|
103
|
+
missing = [col for col in variables if col not in df.columns]
|
|
104
|
+
if missing:
|
|
105
|
+
raise ValueError(f"Variables not found in DataFrame: {missing}")
|
|
106
|
+
|
|
107
|
+
created_fig = False
|
|
108
|
+
if ax is None:
|
|
109
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
110
|
+
created_fig = True
|
|
111
|
+
else:
|
|
112
|
+
fig = ax.figure
|
|
113
|
+
|
|
114
|
+
x_values = pd.to_datetime(df[x], errors="coerce")
|
|
115
|
+
plotted = 0
|
|
116
|
+
for var in variables:
|
|
117
|
+
y_values = pd.to_numeric(df[var], errors="coerce")
|
|
118
|
+
valid = x_values.notna() & y_values.notna()
|
|
119
|
+
if not valid.any():
|
|
120
|
+
continue
|
|
121
|
+
ax.plot(x_values[valid], y_values[valid], linewidth=1.2, label=var)
|
|
122
|
+
plotted += 1
|
|
123
|
+
|
|
124
|
+
if plotted == 0:
|
|
125
|
+
raise ValueError("No valid numeric time-series values available to plot.")
|
|
126
|
+
|
|
127
|
+
ax.set_title(title)
|
|
128
|
+
ax.set_xlabel(xlabel)
|
|
129
|
+
ax.set_ylabel(ylabel)
|
|
130
|
+
ax.grid(True, alpha=0.3)
|
|
131
|
+
ax.legend(ncol=2, fontsize=9)
|
|
132
|
+
|
|
133
|
+
if created_fig:
|
|
134
|
+
fig.tight_layout()
|
|
135
|
+
|
|
136
|
+
return fig, ax
|
|
137
|
+
|
simpest/simpest.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Main module."""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simpest
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: integrated crop growth modelling with pest and disease affect
|
|
5
|
+
Author-email: Kazi Rahaman <kazijahidur.rahaman@zalf.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/KaziJahidurRahaman/simpest
|
|
8
|
+
Keywords: simpest
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: AUTHORS.rst
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: simpest[extra]; extra == "all"
|
|
24
|
+
Provides-Extra: extra
|
|
25
|
+
Requires-Dist: pandas; extra == "extra"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# simpest
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
[](https://pypi.python.org/pypi/simpest)
|
|
32
|
+
[](https://anaconda.org/conda-forge/simpest)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
**integrated crop growth modelling with pest and disease affect**
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
- Free software: MIT License
|
|
39
|
+
- Documentation: https://KaziJahidurRahaman.github.io/simpest
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- TODO
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
simpest/__init__.py,sha256=qOncQ2F-oR3_cMtklLIS9WyJc5s0sD0UdNvUq-kSI2w,134
|
|
2
|
+
simpest/common.py,sha256=UkxoF8RazqaFu7o-ZH3HUoPu7oVGAAB3Ja02Zm6IW9I,189
|
|
3
|
+
simpest/pipeline.py,sha256=ipTcsVdmpFUqDIGm0N9sYjCmjCJnkfnS7LsX7L173Io,3364
|
|
4
|
+
simpest/plot.py,sha256=8uNhFz6c4uj7L56crxsPt3uy9dGSF4EJZxiaSrO5E8s,3885
|
|
5
|
+
simpest/simpest.py,sha256=h0hwdogXGFqerm-5ZPeT-irPn91pCcQRjiHThXsRzEk,19
|
|
6
|
+
simpest/models/__init__.py,sha256=ZEqB5wJ-AaD9frZ_vJAqWeaVxGjPbGxmEytOA7vTgwY,243
|
|
7
|
+
simpest/models/franchestyn.py,sha256=aFa37pzzAxcr51i80OW75GpuWi47hV_yivI9cS0MHXI,9946
|
|
8
|
+
simpest/models/simplace.py,sha256=jWc_0mL5zjvqRZieLQpxw0bdddIZmBatZMF5eQ-MCs4,7563
|
|
9
|
+
simpest-0.0.1.dist-info/licenses/AUTHORS.rst,sha256=_FR4-DPaw9K6vKgBvHSzxqkCLhS75qCN8SNfET2Mn3M,165
|
|
10
|
+
simpest-0.0.1.dist-info/licenses/LICENSE,sha256=PW54hdSF4p4OYx3GQ9cFEQaa5Eowv4ETM3_1SE0Lqw8,1071
|
|
11
|
+
simpest-0.0.1.dist-info/METADATA,sha256=23tDVJcGcsVu-shc3L9zTzsZ9LM3uN2LM9SFz6oxEm4,1365
|
|
12
|
+
simpest-0.0.1.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
13
|
+
simpest-0.0.1.dist-info/entry_points.txt,sha256=bi_iC_exixFRaJreyMKpmbx3ielc9KJnzjdIbjv1FdE,45
|
|
14
|
+
simpest-0.0.1.dist-info/top_level.txt,sha256=CaomRknVGLDQbLQ35PQERc4eHlpZ9cLzv9-uE-6HA7w,8
|
|
15
|
+
simpest-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Kazi Rahaman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simpest
|