This commit is contained in:
2026-04-30 00:53:47 +03:30
parent 88f56da582
commit 46ba01e4cc
13 changed files with 2925 additions and 20 deletions
+76 -8
View File
@@ -5,6 +5,7 @@ from dataclasses import dataclass
from datetime import date, datetime, timedelta
from math import exp
from typing import Any
import logging
from django.core.paginator import EmptyPage, Paginator
@@ -18,6 +19,7 @@ from .services import CropSimulationService, build_simulation_payload_from_farm
DEFAULT_DYNAMIC_PARAMETERS = ["DVS", "LAI", "TAGP", "TWSO", "SM"]
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 50
logger = logging.getLogger(__name__)
DEFAULT_STAGE_LABELS = {
"pre_emergence": "پیش از سبز شدن",
@@ -56,6 +58,24 @@ def _safe_float(value: Any, default: float = 0.0) -> float:
return default
def _pick_first_not_none(*values: Any) -> Any:
for value in values:
if value is not None:
return value
return None
def _clamp(value: float, minimum: float, maximum: float) -> float:
if minimum > maximum:
minimum, maximum = maximum, minimum
return max(minimum, min(value, maximum))
def _mm_to_cm_day(value: Any, default: float) -> float:
scaled = _safe_float(value, default * 10.0) / 10.0
return round(max(scaled, 0.0), 4)
def _coerce_date(value: Any) -> date:
if isinstance(value, date) and not isinstance(value, datetime):
return value
@@ -134,10 +154,11 @@ def _build_weather_from_farm(sensor: SensorData) -> list[dict[str, Any]]:
"TMAX": _safe_float(forecast.temperature_max, 24.0),
"VAP": max(_safe_float(forecast.humidity_mean, 55.0) / 5.0, 6.0),
"WIND": _safe_float(forecast.wind_speed_max, 7.2) / 3.6,
"RAIN": _safe_float(forecast.precipitation, 0.0),
"E0": _safe_float(forecast.et0, 0.35),
"ES0": max(_safe_float(forecast.et0, 0.35) * 0.9, 0.1),
"ET0": _safe_float(forecast.et0, 0.35),
# WeatherForecast stores precipitation/ET0 in mm/day, while PCSE expects cm/day.
"RAIN": _mm_to_cm_day(forecast.precipitation, 0.0),
"E0": _mm_to_cm_day(forecast.et0, 0.35),
"ES0": max(round(_mm_to_cm_day(forecast.et0, 0.35) * 0.9, 4), 0.1),
"ET0": _mm_to_cm_day(forecast.et0, 0.35),
}
)
return records
@@ -148,6 +169,10 @@ def _build_soil_and_site_from_farm(sensor: SensorData) -> tuple[dict[str, Any],
top_depth = depths[0] if depths else None
smfcf = _safe_float(getattr(top_depth, "wv0033", None), 0.34)
smw = _safe_float(getattr(top_depth, "wv1500", None), 0.14)
sm0 = _safe_float(
_pick_first_not_none(getattr(top_depth, "porosity", None), getattr(top_depth, "wv0000", None)),
min(max(smfcf + 0.08, smw + 0.12), 0.6),
)
soil_moisture = None
payload = sensor.sensor_payload or {}
if isinstance(payload, dict):
@@ -155,8 +180,23 @@ def _build_soil_and_site_from_farm(sensor: SensorData) -> tuple[dict[str, Any],
if isinstance(block, dict) and block.get("soil_moisture") is not None:
soil_moisture = _safe_float(block.get("soil_moisture"))
break
site = {"WAV": soil_moisture if soil_moisture is not None else 40.0}
soil = {"SMFCF": smfcf, "SMW": smw, "RDMSOL": 120.0}
site = {
"WAV": soil_moisture if soil_moisture is not None else 40.0,
"IFUNRN": 0,
"NOTINF": 0.0,
"SSI": 0.0,
"SSMAX": 0.0,
"SMLIM": round(_clamp(smfcf, smw, sm0), 3),
}
soil = {
"SMFCF": smfcf,
"SMW": smw,
"SM0": sm0,
"RDMSOL": 120.0,
"CRAIRC": 0.06,
"SOPE": 10.0,
"KSUB": 10.0,
}
return soil, site
@@ -193,7 +233,8 @@ def _build_default_agromanagement(plant_name: str, weather: list[dict[str, Any]]
"TimedEvents": [],
"StateEvents": [],
}
}
},
{},
]
@@ -286,8 +327,27 @@ def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
site_parameters = {**farm_site, **site_parameters}
soil_parameters.setdefault("SMFCF", 0.34)
soil_parameters.setdefault("SMW", 0.14)
soil_parameters.setdefault("SM0", 0.42)
soil_parameters.setdefault("RDMSOL", 120.0)
soil_parameters.setdefault("CRAIRC", 0.06)
soil_parameters.setdefault("SOPE", 10.0)
soil_parameters.setdefault("KSUB", 10.0)
site_parameters.setdefault("WAV", 40.0)
site_parameters.setdefault("IFUNRN", 0)
site_parameters.setdefault("NOTINF", 0.0)
site_parameters.setdefault("SSI", 0.0)
site_parameters.setdefault("SSMAX", 0.0)
site_parameters.setdefault(
"SMLIM",
round(
_clamp(
_safe_float(site_parameters.get("SMLIM"), soil_parameters.get("SMFCF", 0.34)),
_safe_float(soil_parameters.get("SMW"), 0.14),
_safe_float(soil_parameters.get("SM0"), 0.42),
),
3,
),
)
agromanagement = deepcopy(
payload.get("agromanagement")
@@ -402,7 +462,15 @@ def _run_simulation(context: GrowthSimulationContext) -> tuple[dict[str, Any], i
)
return response["result"], response.get("scenario_id"), None
except Exception as exc:
raise GrowthSimulationError(f"Simulation engine failed: {exc}") from exc
logger.warning(
"Falling back to projection engine for farm_uuid=%s plant_name=%s because PCSE failed: %s",
context.farm_uuid,
context.plant_name,
exc,
)
fallback_result = _run_projection_engine(context)
warning = f"Simulation engine failed, fallback projection used: {exc}"
return fallback_result, None, warning
def summarize_growth_stages(