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 ADDED
@@ -0,0 +1,5 @@
1
+ """Top-level package for simpest."""
2
+
3
+ __author__ = """Kazi Rahaman"""
4
+ __email__ = "kazijahidur.rahaman@zalf.de"
5
+ __version__ = "0.0.1"
simpest/common.py ADDED
@@ -0,0 +1,7 @@
1
+ """The common module contains common functions and classes used by the other modules.
2
+ """
3
+
4
+ def hello_world():
5
+ """Prints "Hello World!" to the console.
6
+ """
7
+ print("Hello World!")
@@ -0,0 +1,10 @@
1
+ """Model modules for SIMPLACE and FraNchEstYN workflows."""
2
+
3
+ from .franchestyn import FranchestynConfig, run_franchestyn
4
+ from .simplace import SimplaceConfig
5
+
6
+ __all__ = [
7
+ "SimplaceConfig",
8
+ "FranchestynConfig",
9
+ "run_franchestyn",
10
+ ]
@@ -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
+ [![image](https://img.shields.io/pypi/v/simpest.svg)](https://pypi.python.org/pypi/simpest)
32
+ [![image](https://img.shields.io/conda/vn/conda-forge/simpest.svg)](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,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ simpest = simpest.cli:main
@@ -0,0 +1,13 @@
1
+ =======
2
+ Credits
3
+ =======
4
+
5
+ Development Lead
6
+ ----------------
7
+
8
+ * Kazi Rahaman <kazijahidur.rahaman@zalf.de>
9
+
10
+ Contributors
11
+ ------------
12
+
13
+ None yet. Why not be the first?
@@ -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