Files
Ai/crop_simulation/views.py
T
2026-05-02 14:03:48 +03:30

496 lines
20 KiB
Python

from __future__ import annotations
from django.apps import apps
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from config.openapi import (
build_envelope_serializer,
build_response,
build_task_status_data_serializer,
)
from .growth_simulation import MAX_PAGE_SIZE, paginate_growth_stages
from .serializers import (
CurrentFarmChartRequestSerializer,
CurrentFarmChartResponseSerializer,
GrowthSimulationQueuedSerializer,
GrowthSimulationRequestSerializer,
GrowthSimulationResultSerializer,
HarvestPredictionRequestSerializer,
HarvestPredictionResponseSerializer,
YieldHarvestSummaryQuerySerializer,
YieldHarvestSummaryResponseSerializer,
YieldPredictionRequestSerializer,
YieldPredictionResponseSerializer,
)
from .tasks import run_growth_simulation_task
from .yield_harvest_summary import YieldHarvestSummaryService
GrowthSimulationQueuedResponseSerializer = build_envelope_serializer(
"GrowthSimulationQueuedResponseSerializer",
GrowthSimulationQueuedSerializer,
)
GrowthSimulationStatusResponseSerializer = build_envelope_serializer(
"GrowthSimulationStatusResponseSerializer",
build_task_status_data_serializer(
"GrowthSimulationTaskStatusDataSerializer",
GrowthSimulationResultSerializer,
),
)
GrowthSimulationErrorSerializer = build_envelope_serializer(
"GrowthSimulationErrorSerializer",
data_required=False,
allow_null=True,
)
def _get_async_result(task_id: str):
from celery.result import AsyncResult
return AsyncResult(task_id)
def _coerce_positive_int(value, default: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return default
return max(parsed, 1)
def _fa_task_status(status_name: str) -> str:
return {
"PENDING": "در انتظار",
"PROGRESS": "در حال پردازش",
"SUCCESS": "موفق",
"FAILURE": "ناموفق",
}.get(status_name, status_name)
class PlantGrowthSimulationView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="شروع شبیه سازی رشد گیاه",
description=(
"نوع گیاه و پارامترهای متغیر رشد را می گیرد، "
"شبیه سازی را داخل Celery اجرا می کند و فقط task_id برمی گرداند."
),
request=GrowthSimulationRequestSerializer,
responses={
202: build_response(
GrowthSimulationQueuedResponseSerializer,
"تسک شبیه سازی رشد گیاه در صف قرار گرفت.",
),
400: build_response(
GrowthSimulationErrorSerializer,
"داده ورودی نامعتبر است.",
),
},
examples=[
OpenApiExample(
"نمونه درخواست با weather مستقیم",
value={
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI", "TAGP", "TWSO", "SM"],
"weather": [
{
"DAY": "2026-04-01",
"LAT": 35.7,
"LON": 51.4,
"TMIN": 12,
"TMAX": 24,
"RAIN": 0.0,
"ET0": 0.32,
}
],
"soil_parameters": {"SMFCF": 0.34, "SMW": 0.14, "RDMSOL": 120.0},
"site_parameters": {"WAV": 40.0},
"irrigation_recommendation": {"events": [{"date": "2026-04-02", "amount": 2.5}]},
"fertilization_recommendation": {
"events": [{"date": "2026-04-02", "N_amount": 45, "N_recovery": 0.7}]
},
"page_size": 2,
},
request_only=True,
),
OpenApiExample(
"نمونه درخواست با farm",
value={
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI", "TAGP"],
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"irrigation_recommendation": {"events": [{"date": "2026-04-25", "amount": 2.5}]},
"fertilization_recommendation": {
"events": [{"date": "2026-04-20", "N_amount": 45, "N_recovery": 0.7}]
},
},
request_only=True,
),
],
)
def post(self, request):
serializer = GrowthSimulationRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
task = run_growth_simulation_task.delay(serializer.validated_data)
return Response(
{
"code": 202,
"msg": "تسک شبیه سازی رشد در صف قرار گرفت.",
"data": {
"task_id": task.id,
"status_url": f"/api/crop-simulation/growth/{task.id}/status/",
"plant_name": serializer.validated_data["plant_name"],
},
},
status=status.HTTP_202_ACCEPTED,
)
class PlantGrowthSimulationStatusView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="وضعیت شبیه سازی رشد گیاه",
description="وضعیت تسک Celery را برمی گرداند و در صورت موفقیت مراحل رشد را به صورت صفحه بندی شده بازمی گرداند.",
responses={
200: build_response(
GrowthSimulationStatusResponseSerializer,
"وضعیت فعلی تسک شبیه سازی رشد گیاه.",
)
},
)
def get(self, request, task_id: str):
result = _get_async_result(task_id)
payload = {
"task_id": task_id,
"status": result.state,
"status_fa": _fa_task_status(result.state),
}
if result.state == "PENDING":
payload["message"] = "تسک در صف یا یافت نشد."
elif result.state == "PROGRESS":
payload["progress"] = result.info
elif result.state == "SUCCESS":
task_result = dict(result.result or {})
page = _coerce_positive_int(request.query_params.get("page", 1), 1)
page_size = min(
_coerce_positive_int(
request.query_params.get("page_size", task_result.get("default_page_size", 10)),
10,
),
MAX_PAGE_SIZE,
)
paginated = paginate_growth_stages(
task_result.get("stage_timeline", []),
page=page,
page_size=page_size,
)
task_result["stages_page"] = paginated["items"]
task_result["pagination"] = paginated["pagination"]
payload["result"] = task_result
elif result.state == "FAILURE":
payload["error"] = str(result.result)
return Response(
{"code": 200, "msg": "موفق", "data": payload},
status=status.HTTP_200_OK,
)
CurrentFarmChartEnvelopeSerializer = build_envelope_serializer(
"CurrentFarmChartEnvelopeSerializer",
CurrentFarmChartResponseSerializer,
)
HarvestPredictionEnvelopeSerializer = build_envelope_serializer(
"HarvestPredictionEnvelopeSerializer",
HarvestPredictionResponseSerializer,
)
YieldPredictionEnvelopeSerializer = build_envelope_serializer(
"YieldPredictionEnvelopeSerializer",
YieldPredictionResponseSerializer,
)
YieldHarvestSummaryEnvelopeSerializer = build_envelope_serializer(
"YieldHarvestSummaryEnvelopeSerializer",
YieldHarvestSummaryResponseSerializer,
)
class CurrentFarmSimulationChartView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="chart شبیه سازی وضعیت فعلی مزرعه",
description=(
"با دریافت farm_uuid، یک شبیه سازی از وضعیت فعلی مزرعه اجرا می کند و داده chart شامل برگ، وزن، بیوماس، رطوبت و خروجی روزانه را برمی گرداند."
),
request=CurrentFarmChartRequestSerializer,
responses={
200: build_response(
CurrentFarmChartEnvelopeSerializer,
"خروجی chart شبیه سازی وضعیت فعلی مزرعه.",
),
400: build_response(
GrowthSimulationErrorSerializer,
"داده ورودی نامعتبر است.",
),
500: build_response(
GrowthSimulationErrorSerializer,
"خطا در اجرای chart شبیه سازی مزرعه.",
),
},
examples=[
OpenApiExample(
"نمونه درخواست chart",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"plant_name": "گوجه‌فرنگی",
"irrigation_recommendation": {"events": [{"date": "2026-04-25", "amount": 2.5}]},
"fertilization_recommendation": {
"events": [{"date": "2026-04-20", "N_amount": 45, "N_recovery": 0.7}]
},
},
request_only=True,
),
],
)
def post(self, request):
serializer = CurrentFarmChartRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
simulator = apps.get_app_config("crop_simulation").get_current_farm_chart_simulator()
try:
result = simulator.simulate(**serializer.validated_data)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در اجرای chart شبیه سازی مزرعه: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response(
{"code": 200, "msg": "موفق", "data": result},
status=status.HTTP_200_OK,
)
class HarvestPredictionView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="پیش بینی زمان تقریبی برداشت",
description=(
"با دریافت farm_uuid، از شبیه ساز رشد برای برآورد زمان باقی مانده تا برداشت استفاده می کند "
"و تاریخ تقریبی برداشت را برمی گرداند."
),
request=HarvestPredictionRequestSerializer,
responses={
200: build_response(
HarvestPredictionEnvelopeSerializer,
"خروجی پیش بینی زمان برداشت مزرعه.",
),
400: build_response(
GrowthSimulationErrorSerializer,
"داده ورودی نامعتبر است.",
),
500: build_response(
GrowthSimulationErrorSerializer,
"خطا در پیش بینی زمان برداشت.",
),
},
examples=[
OpenApiExample(
"نمونه درخواست harvest prediction",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"plant_name": "گوجه‌فرنگی",
"irrigation_recommendation": {"events": [{"date": "2026-04-25", "amount": 2.5}]},
"fertilization_recommendation": {
"events": [{"date": "2026-04-20", "N_amount": 45, "N_recovery": 0.7}]
},
},
request_only=True,
),
],
)
def post(self, request):
serializer = HarvestPredictionRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
service = apps.get_app_config("crop_simulation").get_harvest_prediction_service()
try:
result = service.get_harvest_prediction(**serializer.validated_data)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در پیش بینی زمان برداشت: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response(
{"code": 200, "msg": "موفق", "data": result},
status=status.HTTP_200_OK,
)
class YieldPredictionView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="پیش بینی عملکرد مزرعه",
description="با دریافت farm_uuid، خروجی شبیه ساز رشد را به برآورد عملکرد قابل استفاده در KPI تبدیل می کند.",
request=YieldPredictionRequestSerializer,
responses={
200: build_response(YieldPredictionEnvelopeSerializer, "خروجی پیش بینی عملکرد مزرعه."),
400: build_response(GrowthSimulationErrorSerializer, "داده ورودی نامعتبر است."),
500: build_response(GrowthSimulationErrorSerializer, "خطا در پیش بینی عملکرد."),
},
examples=[
OpenApiExample(
"نمونه درخواست yield prediction",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"plant_name": "گوجه‌فرنگی",
"irrigation_recommendation": {"events": [{"date": "2026-04-25", "amount": 2.5}]},
"fertilization_recommendation": {
"events": [{"date": "2026-04-20", "N_amount": 45, "N_recovery": 0.7}]
},
},
request_only=True,
),
],
)
def post(self, request):
serializer = YieldPredictionRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
service = apps.get_app_config("crop_simulation").get_yield_prediction_service()
try:
result = service.get_yield_prediction(**serializer.validated_data)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در پیش بینی عملکرد: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response({"code": 200, "msg": "موفق", "data": result}, status=status.HTTP_200_OK)
class YieldHarvestSummaryView(APIView):
@extend_schema(
tags=["Crop Simulation"],
summary="خلاصه عملکرد و برداشت",
description=(
"خروجی داشبورد Yield & Harvest Summary را با اتکا به داده های قطعی شبیه سازی برمی گرداند. "
"فعلا پاسخ به صورت mock با کارت های خالی بازگردانده می شود."
),
parameters=[
OpenApiParameter(
name="farm_uuid",
type=str,
location=OpenApiParameter.QUERY,
required=True,
description="شناسه یکتای مزرعه",
),
OpenApiParameter(
name="season_year",
type=int,
location=OpenApiParameter.QUERY,
required=False,
description="سال زراعی",
),
OpenApiParameter(
name="crop_name",
type=str,
location=OpenApiParameter.QUERY,
required=False,
description="نام محصول",
),
OpenApiParameter(
name="include_narrative",
type=bool,
location=OpenApiParameter.QUERY,
required=False,
description="در آینده روایت متنی را نیز اضافه می کند.",
),
OpenApiParameter(
name="irrigation_recommendation",
type=str,
location=OpenApiParameter.QUERY,
required=False,
description="JSON برنامه آبیاری برای تزریق به شبیه سازی PCSE.",
),
OpenApiParameter(
name="fertilization_recommendation",
type=str,
location=OpenApiParameter.QUERY,
required=False,
description="JSON برنامه کودهی برای تزریق به شبیه سازی PCSE.",
),
],
responses={
200: build_response(
YieldHarvestSummaryEnvelopeSerializer,
"خروجی خلاصه عملکرد و برداشت مزرعه.",
),
400: build_response(
GrowthSimulationErrorSerializer,
"پارامترهای query نامعتبر است.",
),
},
examples=[
OpenApiExample(
"نمونه پاسخ yield harvest summary",
value={
"code": 200,
"msg": "success",
"data": {
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"season_highlights_card": {},
"yield_prediction": {},
"harvest_prediction_card": {},
"harvest_readiness_zones": {},
"yield_quality_bands": {},
"harvest_operations_card": {},
"yield_prediction_chart": {},
},
},
response_only=True,
),
],
)
def get(self, request):
serializer = YieldHarvestSummaryQuerySerializer(data=request.query_params)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
validated = serializer.validated_data
service = YieldHarvestSummaryService()
payload = service.get_summary(
farm_uuid=str(validated["farm_uuid"]),
season_year=str(validated.get("season_year") or ""),
crop_name=validated.get("crop_name") or "",
include_narrative=validated.get("include_narrative", False),
irrigation_recommendation=validated.get("irrigation_recommendation"),
fertilization_recommendation=validated.get("fertilization_recommendation"),
)
return Response({"code": 200, "msg": "موفق", "data": payload}, status=status.HTTP_200_OK)