Files
Ai/crop_simulation/harvest_prediction.py
T
2026-04-30 02:10:15 +03:30

151 lines
6.2 KiB
Python

from __future__ import annotations
from datetime import date, timedelta
from typing import Any
from farm_data.models import SensorData
from plant.gdd import resolve_growth_profile
from .growth_simulation import (
DEFAULT_DYNAMIC_PARAMETERS,
DEFAULT_PAGE_SIZE,
GrowthSimulationError,
_run_simulation,
build_growth_context,
)
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
if value in (None, ""):
return default
return float(value)
except (TypeError, ValueError):
return default
def _harvest_description(
*,
plant_name: str,
current_gdd: float,
required_gdd: float,
remaining_gdd: float,
estimated_days: int,
maturity_reached_in_simulation: bool,
) -> str:
if maturity_reached_in_simulation:
return (
f"شبيه ساز رشد نشان مي دهد {plant_name} با روند فعلي در حدود {estimated_days} روز ديگر "
f"به بازه برداشت مي رسد. تا امروز {round(current_gdd, 1)} واحد-روز رشد ثبت شده و "
f"نياز باقيمانده تا بلوغ حدود {round(remaining_gdd, 1)} واحد-روز است."
)
return (
f"شبيه ساز تا انتهاي forecast هنوز به رسيدگي کامل نرسيده، اما با ميانگين رشد فعلي "
f"براورد مي شود {plant_name} حدود {estimated_days} روز ديگر به برداشت برسد. "
f"تا امروز {round(current_gdd, 1)} از {round(required_gdd, 1)} واحد-روز مورد نياز طي شده است."
)
def build_harvest_prediction_payload(*, farm_uuid: str, plant_name: str | None = None) -> dict[str, Any]:
resolved_plant_name = plant_name
if not resolved_plant_name:
sensor = SensorData.objects.prefetch_related("plants").filter(farm_uuid=farm_uuid).first()
if sensor is None:
raise GrowthSimulationError("مزرعه پیدا نشد.")
plant = sensor.plants.first()
if plant is None:
raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.")
resolved_plant_name = plant.name
context = build_growth_context(
{
"farm_uuid": farm_uuid,
"plant_name": resolved_plant_name,
"dynamic_parameters": DEFAULT_DYNAMIC_PARAMETERS,
"page_size": DEFAULT_PAGE_SIZE,
}
)
simulation_result, scenario_id, simulation_warning = _run_simulation(context)
daily_output = simulation_result.get("daily_output") or []
if not daily_output:
raise GrowthSimulationError("هیچ خروجی شبیه سازی در دسترس نیست.")
profile = resolve_growth_profile(context.plant)
required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
current_gdd = _safe_float(profile.get("current_cumulative_gdd"), 0.0)
cumulative_gdd = current_gdd
maturity_date = None
daily_gdd_forecast = []
for item in daily_output:
day_gdd = _safe_float(item.get("GDD"), 0.0)
cumulative_gdd += day_gdd
day_value = item.get("DAY")
iso_day = day_value.isoformat() if isinstance(day_value, date) else str(day_value)
daily_gdd_forecast.append(
{
"date": iso_day,
"gdd": round(day_gdd, 3),
"cumulative_gdd": round(cumulative_gdd, 3),
"development_stage": round(_safe_float(item.get("DVS"), 0.0), 4),
}
)
if _safe_float(item.get("DVS"), 0.0) >= 2.0 or cumulative_gdd >= required_gdd:
maturity_date = date.fromisoformat(iso_day)
break
maturity_reached_in_simulation = maturity_date is not None
if maturity_date is None:
last_day = date.fromisoformat(str(daily_output[-1].get("DAY")))
simulated_days = max(len(daily_output), 1)
avg_daily_gdd = max((cumulative_gdd - current_gdd) / simulated_days, 0.0)
remaining_after_simulation = max(required_gdd - cumulative_gdd, 0.0)
extra_days = 0
if avg_daily_gdd > 0 and remaining_after_simulation > 0:
extra_days = int(remaining_after_simulation / avg_daily_gdd)
if remaining_after_simulation % avg_daily_gdd:
extra_days += 1
maturity_date = last_day + timedelta(days=max(extra_days, 0))
remaining_gdd = max(required_gdd - current_gdd, 0.0)
days_until = max((maturity_date - date.today()).days, 0)
window_start = maturity_date - timedelta(days=3)
window_end = maturity_date + timedelta(days=3)
return {
"date": maturity_date.isoformat(),
"dateFormatted": f"{maturity_date.day} {maturity_date.strftime('%B')} {maturity_date.year}",
"daysUntil": days_until,
"description": _harvest_description(
plant_name=context.plant_name,
current_gdd=current_gdd,
required_gdd=required_gdd,
remaining_gdd=remaining_gdd,
estimated_days=days_until,
maturity_reached_in_simulation=maturity_reached_in_simulation,
),
"optimalWindowStart": window_start.isoformat(),
"optimalWindowEnd": window_end.isoformat(),
"gddDetails": {
"current_cumulative_gdd": round(current_gdd, 3),
"required_gdd_for_maturity": round(required_gdd, 3),
"remaining_gdd": round(remaining_gdd, 3),
"estimated_days_to_harvest": days_until,
"predicted_harvest_date": maturity_date.isoformat(),
"predicted_harvest_window": {
"start": window_start.isoformat(),
"end": window_end.isoformat(),
},
"daily_gdd_forecast": daily_gdd_forecast,
"simulation_engine": simulation_result.get("engine"),
"simulation_model_name": simulation_result.get("model_name"),
"simulation_warning": simulation_warning,
"scenario_id": scenario_id,
},
}
class HarvestPredictionService:
def get_harvest_prediction(self, *, farm_uuid: str, plant_name: str | None = None) -> dict[str, Any]:
return build_harvest_prediction_payload(farm_uuid=farm_uuid, plant_name=plant_name)