UPDATE
This commit is contained in:
+240
-10
@@ -93,6 +93,123 @@ def _normalize_agromanagement(agromanagement: Any) -> list[dict[str, Any]]:
|
||||
return campaigns
|
||||
|
||||
|
||||
def _deep_copy_json_like(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {key: _deep_copy_json_like(item) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_deep_copy_json_like(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _parse_recommendation_events(
|
||||
recommendation: dict[str, Any] | None,
|
||||
*,
|
||||
event_signal: str,
|
||||
amount_keys: tuple[str, ...],
|
||||
extra_keys: tuple[str, ...],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not recommendation:
|
||||
return []
|
||||
|
||||
raw_events = recommendation.get("events")
|
||||
if raw_events is None:
|
||||
raw_events = recommendation.get("schedule")
|
||||
if raw_events is None:
|
||||
raw_events = recommendation.get("applications")
|
||||
if raw_events is None:
|
||||
raw_events = recommendation.get("plan")
|
||||
|
||||
if not isinstance(raw_events, list):
|
||||
return []
|
||||
|
||||
events_table = []
|
||||
for item in raw_events:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
raw_date = item.get("date") or item.get("day")
|
||||
if raw_date is None:
|
||||
continue
|
||||
payload = {}
|
||||
amount_value = None
|
||||
amount_key = None
|
||||
for candidate in amount_keys:
|
||||
if item.get(candidate) is not None:
|
||||
amount_value = item.get(candidate)
|
||||
amount_key = candidate
|
||||
break
|
||||
if amount_key is not None:
|
||||
payload[amount_key] = float(amount_value)
|
||||
for extra_key in extra_keys:
|
||||
if item.get(extra_key) is not None:
|
||||
payload[extra_key] = float(item[extra_key])
|
||||
if payload:
|
||||
events_table.append({_coerce_date(raw_date): payload})
|
||||
|
||||
if not events_table:
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
"event_signal": event_signal,
|
||||
"name": recommendation.get("name", f"{event_signal} recommendation"),
|
||||
"comment": recommendation.get("comment", ""),
|
||||
"events_table": events_table,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _merge_management_recommendations(
|
||||
agromanagement: Any,
|
||||
*,
|
||||
irrigation_recommendation: dict[str, Any] | None = None,
|
||||
fertilization_recommendation: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
campaigns = _deep_copy_json_like(_normalize_agromanagement(agromanagement))
|
||||
|
||||
irrigation_events = _parse_recommendation_events(
|
||||
irrigation_recommendation,
|
||||
event_signal="irrigate",
|
||||
amount_keys=("amount", "irrigation_amount"),
|
||||
extra_keys=("efficiency",),
|
||||
)
|
||||
fertilization_events = _parse_recommendation_events(
|
||||
fertilization_recommendation,
|
||||
event_signal="apply_n",
|
||||
amount_keys=("N_amount", "amount"),
|
||||
extra_keys=("N_recovery",),
|
||||
)
|
||||
|
||||
if not irrigation_events and not fertilization_events:
|
||||
return campaigns
|
||||
|
||||
target_campaign = None
|
||||
for campaign in campaigns:
|
||||
if isinstance(campaign, dict) and campaign:
|
||||
target_campaign = campaign
|
||||
break
|
||||
|
||||
if target_campaign is None:
|
||||
raise CropSimulationError(
|
||||
"Agromanagement must contain at least one non-empty campaign."
|
||||
)
|
||||
|
||||
campaign_start = next(iter(target_campaign.keys()))
|
||||
campaign_payload = target_campaign[campaign_start]
|
||||
if not isinstance(campaign_payload, dict):
|
||||
raise CropSimulationError("Agromanagement campaign payload must be a dictionary.")
|
||||
|
||||
timed_events = campaign_payload.get("TimedEvents")
|
||||
if timed_events in (None, ""):
|
||||
timed_events = []
|
||||
if not isinstance(timed_events, list):
|
||||
raise CropSimulationError("TimedEvents must be a list when recommendations are merged.")
|
||||
|
||||
timed_events.extend(irrigation_events)
|
||||
timed_events.extend(fertilization_events)
|
||||
campaign_payload["TimedEvents"] = timed_events
|
||||
return campaigns
|
||||
|
||||
|
||||
def _normalize_pcse_output_records(records: Any) -> list[dict[str, Any]]:
|
||||
if records is None:
|
||||
return []
|
||||
@@ -298,8 +415,15 @@ class CropSimulationService:
|
||||
crop_parameters: dict[str, Any],
|
||||
agromanagement: Any,
|
||||
site_parameters: dict[str, Any] | None = None,
|
||||
irrigation_recommendation: dict[str, Any] | None = None,
|
||||
fertilization_recommendation: dict[str, Any] | None = None,
|
||||
name: str = "",
|
||||
) -> dict[str, Any]:
|
||||
merged_agromanagement = _merge_management_recommendations(
|
||||
agromanagement,
|
||||
irrigation_recommendation=irrigation_recommendation,
|
||||
fertilization_recommendation=fertilization_recommendation,
|
||||
)
|
||||
scenario = SimulationScenario.objects.create(
|
||||
name=name,
|
||||
scenario_type=SimulationScenario.ScenarioType.SINGLE,
|
||||
@@ -310,7 +434,9 @@ class CropSimulationService:
|
||||
"soil": soil,
|
||||
"crop_parameters": crop_parameters,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": agromanagement,
|
||||
"agromanagement": merged_agromanagement,
|
||||
"irrigation_recommendation": irrigation_recommendation or {},
|
||||
"fertilization_recommendation": fertilization_recommendation or {},
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -322,7 +448,7 @@ class CropSimulationService:
|
||||
soil_payload=_json_ready(soil),
|
||||
crop_payload=_json_ready(crop_parameters),
|
||||
site_payload=_json_ready(site_parameters or {}),
|
||||
agromanagement_payload=_json_ready(agromanagement),
|
||||
agromanagement_payload=_json_ready(merged_agromanagement),
|
||||
)
|
||||
return self._execute_scenario(
|
||||
scenario=scenario,
|
||||
@@ -333,7 +459,7 @@ class CropSimulationService:
|
||||
"soil": soil,
|
||||
"crop_parameters": crop_parameters,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": agromanagement,
|
||||
"agromanagement": merged_agromanagement,
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -347,8 +473,15 @@ class CropSimulationService:
|
||||
crop_b: dict[str, Any],
|
||||
agromanagement: Any,
|
||||
site_parameters: dict[str, Any] | None = None,
|
||||
irrigation_recommendation: dict[str, Any] | None = None,
|
||||
fertilization_recommendation: dict[str, Any] | None = None,
|
||||
name: str = "",
|
||||
) -> dict[str, Any]:
|
||||
merged_agromanagement = _merge_management_recommendations(
|
||||
agromanagement,
|
||||
irrigation_recommendation=irrigation_recommendation,
|
||||
fertilization_recommendation=fertilization_recommendation,
|
||||
)
|
||||
scenario = SimulationScenario.objects.create(
|
||||
name=name,
|
||||
scenario_type=SimulationScenario.ScenarioType.CROP_COMPARISON,
|
||||
@@ -360,7 +493,9 @@ class CropSimulationService:
|
||||
"crop_a": crop_a,
|
||||
"crop_b": crop_b,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": agromanagement,
|
||||
"agromanagement": merged_agromanagement,
|
||||
"irrigation_recommendation": irrigation_recommendation or {},
|
||||
"fertilization_recommendation": fertilization_recommendation or {},
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -373,7 +508,7 @@ class CropSimulationService:
|
||||
soil_payload=_json_ready(soil),
|
||||
crop_payload=_json_ready(crop_a),
|
||||
site_payload=_json_ready(site_parameters or {}),
|
||||
agromanagement_payload=_json_ready(agromanagement),
|
||||
agromanagement_payload=_json_ready(merged_agromanagement),
|
||||
),
|
||||
SimulationRun.objects.create(
|
||||
scenario=scenario,
|
||||
@@ -383,7 +518,7 @@ class CropSimulationService:
|
||||
soil_payload=_json_ready(soil),
|
||||
crop_payload=_json_ready(crop_b),
|
||||
site_payload=_json_ready(site_parameters or {}),
|
||||
agromanagement_payload=_json_ready(agromanagement),
|
||||
agromanagement_payload=_json_ready(merged_agromanagement),
|
||||
),
|
||||
]
|
||||
return self._execute_scenario(
|
||||
@@ -395,7 +530,7 @@ class CropSimulationService:
|
||||
"soil": soil,
|
||||
"crop_parameters": crop_a,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": agromanagement,
|
||||
"agromanagement": merged_agromanagement,
|
||||
},
|
||||
{
|
||||
"instance": runs[1],
|
||||
@@ -403,11 +538,92 @@ class CropSimulationService:
|
||||
"soil": soil,
|
||||
"crop_parameters": crop_b,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": agromanagement,
|
||||
"agromanagement": merged_agromanagement,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def recommend_best_crop(
|
||||
self,
|
||||
*,
|
||||
weather: Any,
|
||||
soil: dict[str, Any],
|
||||
crops: list[dict[str, Any]],
|
||||
agromanagement: Any,
|
||||
site_parameters: dict[str, Any] | None = None,
|
||||
irrigation_recommendation: dict[str, Any] | None = None,
|
||||
fertilization_recommendation: dict[str, Any] | None = None,
|
||||
name: str = "",
|
||||
) -> dict[str, Any]:
|
||||
if len(crops) < 2:
|
||||
raise CropSimulationError("At least two crop options are required.")
|
||||
|
||||
merged_agromanagement = _merge_management_recommendations(
|
||||
agromanagement,
|
||||
irrigation_recommendation=irrigation_recommendation,
|
||||
fertilization_recommendation=fertilization_recommendation,
|
||||
)
|
||||
|
||||
scenario = SimulationScenario.objects.create(
|
||||
name=name,
|
||||
scenario_type=SimulationScenario.ScenarioType.CROP_COMPARISON,
|
||||
model_name=self.manager.model_name,
|
||||
input_payload=_json_ready(
|
||||
{
|
||||
"weather": weather,
|
||||
"soil": soil,
|
||||
"crops": crops,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": merged_agromanagement,
|
||||
"irrigation_recommendation": irrigation_recommendation or {},
|
||||
"fertilization_recommendation": fertilization_recommendation or {},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
run_specs = []
|
||||
for index, crop in enumerate(crops, start=1):
|
||||
label = (
|
||||
crop.get("label")
|
||||
or crop.get("crop_name")
|
||||
or crop.get("name")
|
||||
or f"crop_{index}"
|
||||
)
|
||||
run = SimulationRun.objects.create(
|
||||
scenario=scenario,
|
||||
run_key=f"crop_{index}",
|
||||
label=label,
|
||||
weather_payload=_json_ready(weather),
|
||||
soil_payload=_json_ready(soil),
|
||||
crop_payload=_json_ready(crop),
|
||||
site_payload=_json_ready(site_parameters or {}),
|
||||
agromanagement_payload=_json_ready(merged_agromanagement),
|
||||
)
|
||||
run_specs.append(
|
||||
{
|
||||
"instance": run,
|
||||
"weather": weather,
|
||||
"soil": soil,
|
||||
"crop_parameters": crop,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": merged_agromanagement,
|
||||
}
|
||||
)
|
||||
|
||||
result = self._execute_scenario(scenario=scenario, run_specs=run_specs)
|
||||
comparison = result.get("comparison", {})
|
||||
return {
|
||||
"scenario_id": result["scenario_id"],
|
||||
"scenario_type": result["scenario_type"],
|
||||
"recommended_crop": {
|
||||
"run_key": comparison.get("best_run_key"),
|
||||
"label": comparison.get("best_label"),
|
||||
"expected_yield_estimate": comparison.get("best_yield_estimate"),
|
||||
},
|
||||
"candidates": comparison.get("runs", []),
|
||||
"raw_result": result,
|
||||
}
|
||||
|
||||
def compare_fertilization_strategies(
|
||||
self,
|
||||
*,
|
||||
@@ -416,6 +632,8 @@ class CropSimulationService:
|
||||
crop_parameters: dict[str, Any],
|
||||
strategies: list[dict[str, Any]],
|
||||
site_parameters: dict[str, Any] | None = None,
|
||||
irrigation_recommendation: dict[str, Any] | None = None,
|
||||
fertilization_recommendation: dict[str, Any] | None = None,
|
||||
name: str = "",
|
||||
) -> dict[str, Any]:
|
||||
if len(strategies) < 2:
|
||||
@@ -432,11 +650,18 @@ class CropSimulationService:
|
||||
"crop_parameters": crop_parameters,
|
||||
"site_parameters": site_parameters or {},
|
||||
"strategies": strategies,
|
||||
"irrigation_recommendation": irrigation_recommendation or {},
|
||||
"fertilization_recommendation": fertilization_recommendation or {},
|
||||
}
|
||||
),
|
||||
)
|
||||
run_specs = []
|
||||
for index, strategy in enumerate(strategies, start=1):
|
||||
merged_agromanagement = _merge_management_recommendations(
|
||||
strategy["agromanagement"],
|
||||
irrigation_recommendation=irrigation_recommendation,
|
||||
fertilization_recommendation=fertilization_recommendation,
|
||||
)
|
||||
run = SimulationRun.objects.create(
|
||||
scenario=scenario,
|
||||
run_key=f"strategy_{index}",
|
||||
@@ -445,7 +670,7 @@ class CropSimulationService:
|
||||
soil_payload=_json_ready(soil),
|
||||
crop_payload=_json_ready(crop_parameters),
|
||||
site_payload=_json_ready(site_parameters or {}),
|
||||
agromanagement_payload=_json_ready(strategy["agromanagement"]),
|
||||
agromanagement_payload=_json_ready(merged_agromanagement),
|
||||
)
|
||||
run_specs.append(
|
||||
{
|
||||
@@ -454,7 +679,7 @@ class CropSimulationService:
|
||||
"soil": soil,
|
||||
"crop_parameters": crop_parameters,
|
||||
"site_parameters": site_parameters or {},
|
||||
"agromanagement": strategy["agromanagement"],
|
||||
"agromanagement": merged_agromanagement,
|
||||
}
|
||||
)
|
||||
return self._execute_scenario(scenario=scenario, run_specs=run_specs)
|
||||
@@ -598,6 +823,11 @@ def compare_crops(**kwargs) -> dict[str, Any]:
|
||||
return CropSimulationService().compare_crops(**kwargs)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def recommend_best_crop(**kwargs) -> dict[str, Any]:
|
||||
return CropSimulationService().recommend_best_crop(**kwargs)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def compare_fertilization_strategies(**kwargs) -> dict[str, Any]:
|
||||
return CropSimulationService().compare_fertilization_strategies(**kwargs)
|
||||
|
||||
Reference in New Issue
Block a user