UPDATE
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user