diff --git a/crop_simulation/growth_simulation.py b/crop_simulation/growth_simulation.py index 8bd139d..100e5b8 100644 --- a/crop_simulation/growth_simulation.py +++ b/crop_simulation/growth_simulation.py @@ -30,11 +30,33 @@ DEFAULT_STAGE_LABELS = { "maturity": "رسیدگی", } +ENGINE_LABELS = { + "pcse": "موتور شبیه سازی PCSE", + "growth_projection": "موتور برآورد رشد", +} + +MODEL_LABELS = { + "growth_projection_v1": "مدل برآورد رشد نسخه ۱", + "wofost": "مدل ووفوست", +} + class GrowthSimulationError(Exception): pass +def _fa_engine_name(name: str | None) -> str | None: + if not name: + return name + return ENGINE_LABELS.get(name, name) + + +def _fa_model_name(name: str | None) -> str | None: + if not name: + return name + return MODEL_LABELS.get(name, name) + + @dataclass class GrowthSimulationContext: farm_uuid: str | None @@ -469,7 +491,7 @@ def _run_simulation(context: GrowthSimulationContext) -> tuple[dict[str, Any], i exc, ) fallback_result = _run_projection_engine(context) - warning = f"Simulation engine failed, fallback projection used: {exc}" + warning = f"موتور شبیه سازی با خطا مواجه شد و برآورد جایگزین استفاده شد: {exc}" return fallback_result, None, warning @@ -622,8 +644,8 @@ def run_growth_simulation(payload: dict[str, Any], progress_callback=None) -> di return { "plant_name": context.plant_name, "dynamic_parameters": context.dynamic_parameters, - "engine": simulation_result.get("engine"), - "model_name": simulation_result.get("model_name"), + "engine": _fa_engine_name(simulation_result.get("engine")), + "model_name": _fa_model_name(simulation_result.get("model_name")), "scenario_id": scenario_id, "simulation_warning": simulation_error, "summary_metrics": simulation_result.get("metrics", {}), @@ -665,7 +687,7 @@ def _build_current_farm_chart_payload( "title": "تعداد برگ تخمینی", "subtitle": "وضعیت فعلی", "amount": round(_estimate_leaf_count(latest_lai), 2), - "unit": "leaf", + "unit": "برگ", "avatarColor": "success", "avatarIcon": "tabler-leaf", }, @@ -673,7 +695,7 @@ def _build_current_farm_chart_payload( "title": "وزن بیوماس", "subtitle": "برآورد فعلی", "amount": round(latest_biomass, 2), - "unit": "kg/ha", + "unit": "کیلوگرم در هکتار", "avatarColor": "primary", "avatarIcon": "tabler-chart-bar", }, @@ -681,7 +703,7 @@ def _build_current_farm_chart_payload( "title": "وزن محصول", "subtitle": "برآورد فعلی", "amount": round(latest_storage, 2), - "unit": "kg/ha", + "unit": "کیلوگرم در هکتار", "avatarColor": "warning", "avatarIcon": "tabler-scale", }, @@ -698,8 +720,8 @@ def _build_current_farm_chart_payload( return { "farm_uuid": context.farm_uuid, "plant_name": context.plant_name, - "engine": simulation_result.get("engine"), - "model_name": simulation_result.get("model_name"), + "engine": _fa_engine_name(simulation_result.get("engine")), + "model_name": _fa_model_name(simulation_result.get("model_name")), "scenario_id": scenario_id, "simulation_warning": simulation_warning, "categories": categories, @@ -731,7 +753,7 @@ class CurrentFarmChartSimulator: def simulate(self, *, farm_uuid: str, plant_name: str | None = None) -> dict[str, Any]: if not farm_uuid: - raise GrowthSimulationError("farm_uuid is required.") + raise GrowthSimulationError("ارسال farm_uuid الزامی است.") resolved_plant_name = plant_name if not resolved_plant_name: @@ -741,10 +763,10 @@ class CurrentFarmChartSimulator: .first() ) if sensor is None: - raise GrowthSimulationError("Farm not found.") + raise GrowthSimulationError("مزرعه پیدا نشد.") plant = sensor.plants.first() if plant is None: - raise GrowthSimulationError("Plant not found for the selected farm.") + raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.") resolved_plant_name = plant.name context = build_growth_context( diff --git a/crop_simulation/harvest_prediction.py b/crop_simulation/harvest_prediction.py index 7362abb..105327e 100644 --- a/crop_simulation/harvest_prediction.py +++ b/crop_simulation/harvest_prediction.py @@ -51,10 +51,10 @@ def build_harvest_prediction_payload(*, farm_uuid: str, plant_name: str | None = if not resolved_plant_name: sensor = SensorData.objects.prefetch_related("plants").filter(farm_uuid=farm_uuid).first() if sensor is None: - raise GrowthSimulationError("Farm not found.") + raise GrowthSimulationError("مزرعه پیدا نشد.") plant = sensor.plants.first() if plant is None: - raise GrowthSimulationError("Plant not found for the selected farm.") + raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.") resolved_plant_name = plant.name context = build_growth_context( @@ -68,7 +68,7 @@ def build_harvest_prediction_payload(*, farm_uuid: str, plant_name: str | None = simulation_result, scenario_id, simulation_warning = _run_simulation(context) daily_output = simulation_result.get("daily_output") or [] if not daily_output: - raise GrowthSimulationError("No simulation output available.") + raise GrowthSimulationError("هیچ خروجی شبیه سازی در دسترس نیست.") profile = resolve_growth_profile(context.plant) required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0) diff --git a/crop_simulation/views.py b/crop_simulation/views.py index 3ade56f..f3931b9 100644 --- a/crop_simulation/views.py +++ b/crop_simulation/views.py @@ -63,6 +63,15 @@ def _coerce_positive_int(value, default: int) -> int: 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"], @@ -153,7 +162,11 @@ class PlantGrowthSimulationStatusView(APIView): ) def get(self, request, task_id: str): result = _get_async_result(task_id) - payload = {"task_id": task_id, "status": result.state} + payload = { + "task_id": task_id, + "status": result.state, + "status_fa": _fa_task_status(result.state), + } if result.state == "PENDING": payload["message"] = "تسک در صف یا یافت نشد." @@ -181,7 +194,7 @@ class PlantGrowthSimulationStatusView(APIView): payload["error"] = str(result.result) return Response( - {"code": 200, "msg": "success", "data": payload}, + {"code": 200, "msg": "موفق", "data": payload}, status=status.HTTP_200_OK, ) @@ -256,7 +269,7 @@ class CurrentFarmSimulationChartView(APIView): ) return Response( - {"code": 200, "msg": "success", "data": result}, + {"code": 200, "msg": "موفق", "data": result}, status=status.HTTP_200_OK, ) @@ -313,7 +326,7 @@ class HarvestPredictionView(APIView): ) return Response( - {"code": 200, "msg": "success", "data": result}, + {"code": 200, "msg": "موفق", "data": result}, status=status.HTTP_200_OK, ) @@ -355,7 +368,7 @@ class YieldPredictionView(APIView): {"code": 500, "msg": f"خطا در پیش بینی عملکرد: {exc}", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK) + return Response({"code": 200, "msg": "موفق", "data": result}, status=status.HTTP_200_OK) class YieldHarvestSummaryView(APIView): @@ -443,4 +456,4 @@ class YieldHarvestSummaryView(APIView): crop_name=validated.get("crop_name") or "", include_narrative=validated.get("include_narrative", False), ) - return Response({"code": 200, "msg": "success", "data": payload}, status=status.HTTP_200_OK) + return Response({"code": 200, "msg": "موفق", "data": payload}, status=status.HTTP_200_OK) diff --git a/crop_simulation/yield_harvest_summary.py b/crop_simulation/yield_harvest_summary.py index eba2eac..17ed669 100644 --- a/crop_simulation/yield_harvest_summary.py +++ b/crop_simulation/yield_harvest_summary.py @@ -17,6 +17,13 @@ from rag.services.yield_harvest import YieldHarvestRAGService logger = logging.getLogger(__name__) +READINESS_STATUS_FA = { + "ready": "آماده", + "approaching": "نزدیک به آمادگی", + "monitoring": "نیازمند پایش", + "not_ready": "آماده نیست", +} + class YieldHarvestSummaryService: def get_summary( @@ -174,8 +181,8 @@ class YieldHarvestSummaryService: ) fallback_description = ( - f"Deterministic harvest forecast for {crop_name or 'the selected crop'} " - f"in season {season_year}." + f"پیش بینی قطعی برداشت برای {crop_name or 'محصول انتخاب شده'} " + f"در فصل زراعی {season_year}." ) return { @@ -188,7 +195,7 @@ class YieldHarvestSummaryService: "optimal_window_start": result.get("optimalWindowStart"), "optimal_window_end": result.get("optimalWindowEnd"), "description": result.get("description") or fallback_description, - "descriptionSource": "deterministic", + "descriptionSource": "قطعی", "field_conditions": { "soil_moisture": farm_context.get("recent_sensor_averages", {}).get("soil_moisture"), "soil_temperature": farm_context.get("recent_sensor_averages", {}).get("soil_temperature"), @@ -233,19 +240,19 @@ class YieldHarvestSummaryService: "season_year": season_year, "series": [ { - "name": "Predicted Yield", + "name": "عملکرد پیش بینی شده", "type": "line", "data": yield_series, }, { - "name": "Biomass", + "name": "بیوماس", "type": "area", "data": biomass_series, }, ], - "xAxis": {"type": "datetime"}, + "xAxis": {"type": "datetime", "label": "تاریخ"}, "meta": { - "unit": "kg/ha", + "unit": "کیلوگرم در هکتار", "simulation_engine": result.get("engine"), "simulation_model": result.get("model_name"), "scenario_id": result.get("scenario_id"), @@ -281,10 +288,10 @@ class YieldHarvestSummaryService: "days_until_harvest": days_until, "current_dvs": round(pcse_dvs_stage, 4), "summary": ( - f"Operations are prioritized for {farm_context.get('crop_name') or 'the selected crop'} " - f"with {days_until} days remaining until the predicted harvest window." + f"عملیات برداشت برای {farm_context.get('crop_name') or 'محصول انتخاب شده'} " + f"با توجه به {days_until} روز باقی مانده تا بازه پیش بینی شده برداشت اولویت بندی شده است." ), - "rules_source": "deterministic_dvs_rules", + "rules_source": "قواعد_قطعی_DVS", "field_context": { "soil_type": farm_context.get("soil", {}).get("soil_type"), "soil_moisture": farm_context.get("recent_sensor_averages", {}).get("soil_moisture"), @@ -325,7 +332,7 @@ class YieldHarvestSummaryService: "farm_uuid": farm_uuid, "crop_name": crop_name, "season_year": season_year, - "title": "Season highlights", + "title": "خلاصه فصل", # Left blank for narrative merge unless a non-LLM fallback is needed later. "subtitle": "", "total_predicted_yield": total_predicted_yield, @@ -357,7 +364,7 @@ class YieldHarvestSummaryService: "farm_uuid": farm_uuid, "averageReadiness": None, "zones": [], - "source": "ndvi_health_service", + "source": "سرویس_سلامت_NDVI", } location = sensor.center_location @@ -398,7 +405,7 @@ class YieldHarvestSummaryService: zones.append( { "zoneId": f"zone-{zone_index}", - "zoneLabel": f"Zone {zone_index}", + "zoneLabel": f"ناحیه {zone_index}", "gridPosition": {"row": row_index, "col": col_index}, "meanNdvi": cell_ndvi, "readiness": readiness, @@ -413,7 +420,7 @@ class YieldHarvestSummaryService: zones.append( { "zoneId": "zone-center", - "zoneLabel": "Center field zone", + "zoneLabel": "ناحیه مرکزی مزرعه", "gridPosition": None, "meanNdvi": latest_ndvi, "readiness": readiness, @@ -441,7 +448,7 @@ class YieldHarvestSummaryService: "ndviTrend": ndvi_trend, "averageReadiness": average_readiness, "zones": zones, - "source": "ndvi_health_service", + "source": "سرویس_سلامت_NDVI", } def _to_unix_timestamp(self, value: Any) -> int | None: @@ -480,12 +487,12 @@ class YieldHarvestSummaryService: def _readiness_status(self, readiness: int) -> str: if readiness >= 80: - return "ready" + return READINESS_STATUS_FA["ready"] if readiness >= 55: - return "approaching" + return READINESS_STATUS_FA["approaching"] if readiness >= 30: - return "monitoring" - return "not_ready" + return READINESS_STATUS_FA["monitoring"] + return READINESS_STATUS_FA["not_ready"] def _build_yield_quality_bands( self, @@ -555,7 +562,7 @@ class YieldHarvestSummaryService: "farm_uuid": farm_uuid, "crop_name": crop_name, "season_year": season_year, - "source": "deterministic_grading_rules", + "source": "قواعد_قطعی_درجه_بندی", "is_estimated": True, "protein_content": { "value": protein_content, @@ -568,7 +575,7 @@ class YieldHarvestSummaryService: "grade_distribution": grade_distribution, "primary_quality_grade": primary_quality_grade, "quality_score": quality_score, - "summary": f"Primary quality grade is {primary_quality_grade}.", + "summary": f"درجه کیفیت غالب محصول {primary_quality_grade} است.", } def _get_estimated_revenue( @@ -682,7 +689,7 @@ class YieldHarvestSummaryService: return { "farm_uuid": farm_uuid, "center_coordinates": None, - "soil": {"provider": getattr(settings, "SOIL_DATA_PROVIDER", "unknown")}, + "soil": {"provider": getattr(settings, "SOIL_DATA_PROVIDER", "نامشخص")}, "recent_sensor_averages": {}, } @@ -712,7 +719,7 @@ class YieldHarvestSummaryService: }, "farm_boundary": farm_details.get("center_location", {}).get("farm_boundary"), "soil": { - "provider": getattr(settings, "SOIL_DATA_PROVIDER", "unknown"), + "provider": getattr(settings, "SOIL_DATA_PROVIDER", "نامشخص"), "soil_type": self._infer_soil_type(soil_details), "resolved_metrics": soil_details, }, @@ -735,14 +742,14 @@ class YieldHarvestSummaryService: def _map_dvs_to_phase(self, dvs: float) -> tuple[str, str]: if dvs >= 2.0: - return "ready", "maturity" + return "آماده", "رسیدگی" if dvs >= 1.7: - return "final_pre_harvest", "late_reproductive" + return "پیش_برداشت_نهایی", "زایشی_پایانی" if dvs >= 1.2: - return "mid_pre_harvest", "grain_fill" + return "پیش_برداشت_میانی", "پرشدن_دانه" if dvs >= 0.8: - return "monitoring", "reproductive_transition" - return "early_pre_harvest", "vegetative" + return "پایش", "گذار_زایشی" + return "پیش_برداشت_ابتدایی", "رشد_رویشی" def _build_operations_steps( self, @@ -753,74 +760,74 @@ class YieldHarvestSummaryService: ) -> list[dict[str, Any]]: field_ready = soil_moisture is None or soil_moisture <= 35.0 - if phase_name == "maturity": + if phase_name == "رسیدگی": return [ { "key": "desiccation", - "title": "Desiccation check", - "status": "ready", + "title": "بررسی خشک شدن محصول", + "status": "آماده", "is_completed": False, "estimated_days": 0, }, { "key": "harvesting", - "title": "Harvesting", - "status": "ready" if field_ready else "watch_field_conditions", + "title": "برداشت", + "status": "آماده" if field_ready else "نیازمند بررسی شرایط مزرعه", "is_completed": False, "estimated_days": max(min(days_until, 2), 0), }, { "key": "transportation", - "title": "Transportation", - "status": "ready", + "title": "انتقال محصول", + "status": "آماده", "is_completed": False, "estimated_days": max(min(days_until + 1, 3), 1), }, ] - if phase_name == "late_reproductive": + if phase_name == "زایشی_پایانی": return [ { "key": "equipment_check", - "title": "Inspect harvest equipment", - "status": "priority", + "title": "بازبینی تجهیزات برداشت", + "status": "اولویت بالا", "is_completed": False, "estimated_days": 1, }, { "key": "labor_plan", - "title": "Confirm labor and transport plan", - "status": "priority", + "title": "نهایی کردن برنامه نیروی کار و حمل", + "status": "اولویت بالا", "is_completed": False, "estimated_days": 2, }, { "key": "field_entry", - "title": "Verify field access and dry windows", - "status": "ready" if field_ready else "monitor", + "title": "بررسی امکان ورود به مزرعه و بازه های خشک", + "status": "آماده" if field_ready else "پایش", "is_completed": False, "estimated_days": max(min(days_until, 5), 1), }, ] - if phase_name == "grain_fill": + if phase_name == "پرشدن_دانه": return [ { "key": "monitor_maturity", - "title": "Track maturity and storage organ growth", - "status": "active", + "title": "پایش رسیدگی و رشد اندام ذخیره ای", + "status": "در حال انجام", "is_completed": False, "estimated_days": 7, }, { "key": "review_readiness", - "title": "Review zone readiness differences", - "status": "active", + "title": "بررسی اختلاف آمادگی بین ناحیه ها", + "status": "در حال انجام", "is_completed": False, "estimated_days": 10, }, { "key": "prepare_logistics", - "title": "Prepare harvest logistics plan", - "status": "upcoming", + "title": "آماده سازی برنامه لجستیک برداشت", + "status": "پیش رو", "is_completed": False, "estimated_days": 14, }, @@ -828,22 +835,22 @@ class YieldHarvestSummaryService: return [ { "key": "weekly_monitoring", - "title": "Run weekly crop maturity checks", - "status": "active", + "title": "پایش هفتگی رسیدگی محصول", + "status": "در حال انجام", "is_completed": False, "estimated_days": 14, }, { "key": "update_forecast", - "title": "Refresh harvest timing forecast", - "status": "active", + "title": "به روزرسانی پیش بینی زمان برداشت", + "status": "در حال انجام", "is_completed": False, "estimated_days": 10, }, { "key": "draft_operations", - "title": "Draft harvest operation checklist", - "status": "upcoming", + "title": "تهیه چک لیست عملیات برداشت", + "status": "پیش رو", "is_completed": False, "estimated_days": 21, }, @@ -856,12 +863,12 @@ class YieldHarvestSummaryService: if sand is None or clay is None or silt is None: return None if clay >= 40: - return "clay" + return "رسی" if sand >= 70 and clay <= 15: - return "sandy" + return "شنی" if silt >= 50 and clay < 27: - return "silty_loam" - return "loam" + return "سیلتی لوم" + return "لوم" def _safe_float(self, value: Any, default: float | None = 0.0) -> float | None: try: @@ -932,33 +939,33 @@ class YieldHarvestSummaryService: highlights = payload.get("season_highlights_card") or {} total_yield = highlights.get("total_predicted_yield") unit = highlights.get("yield_unit") or "" - harvest_date = highlights.get("target_harvest_date") or "the predicted harvest window" + harvest_date = highlights.get("target_harvest_date") or "بازه پیش بینی شده برداشت" if total_yield is None: - return f"Harvest is targeted for {harvest_date} based on the deterministic season outlook." - return f"Predicted yield is {total_yield} {unit} and harvest is targeted for {harvest_date}.".strip() + return f"بر اساس چشم انداز قطعی فصل، برداشت برای {harvest_date} هدف گذاری شده است." + return f"عملکرد پیش بینی شده {total_yield} {unit} است و برداشت برای {harvest_date} هدف گذاری شده است.".strip() def _default_yield_prediction_explanation(self, payload: dict[str, Any]) -> str: yield_card = payload.get("yield_prediction") or {} predicted = yield_card.get("predicted_yield_tons") unit = yield_card.get("unit") or "" if predicted is None: - return "Yield forecast is based on the deterministic crop simulation output." - return f"Yield forecast is based on the deterministic crop simulation and currently projects {predicted} {unit}.".strip() + return "پیش بینی عملکرد بر پایه خروجی قطعی شبیه سازی محصول محاسبه شده است." + return f"پیش بینی عملکرد بر پایه شبیه سازی قطعی محصول انجام شده و در حال حاضر مقدار {predicted} {unit} را نشان می دهد.".strip() def _default_harvest_readiness_summary(self, payload: dict[str, Any]) -> str: readiness = payload.get("harvest_readiness_zones") or {} average = readiness.get("averageReadiness") if average is None: - return "Harvest readiness is derived from the latest deterministic zone signals." - return f"Average harvest readiness is {average} based on the latest deterministic zone signals.".strip() + return "آمادگی برداشت از آخرین سیگنال های قطعی ناحیه ای استخراج شده است." + return f"میانگین آمادگی برداشت بر اساس آخرین سیگنال های قطعی ناحیه ای، {average} است.".strip() def _default_operation_note(self, step: dict[str, Any]) -> str: - title = step.get("title") or "This operation" - status = step.get("status") or "planned" + title = step.get("title") or "این عملیات" + status = step.get("status") or "برنامه ریزی شده" estimate = step.get("estimated_days") if estimate is None: - return f"{title} is currently marked as {status}." - return f"{title} is {status} with an estimated timing of {estimate} days.".strip() + return f"وضعیت {title} در حال حاضر «{status}» ثبت شده است." + return f"{title} با وضعیت «{status}» و زمان بندی تقریبی {estimate} روز ثبت شده است.".strip() def _resolve_service(self, *, getter_names: tuple[str, ...]) -> Any: app_config = apps.get_app_config("crop_simulation") diff --git a/crop_simulation/yield_prediction.py b/crop_simulation/yield_prediction.py index cb26b0c..326d483 100644 --- a/crop_simulation/yield_prediction.py +++ b/crop_simulation/yield_prediction.py @@ -2,7 +2,12 @@ from __future__ import annotations from typing import Any -from .growth_simulation import CurrentFarmChartSimulator, GrowthSimulationError +from .growth_simulation import ( + CurrentFarmChartSimulator, + GrowthSimulationError, + _fa_engine_name, + _fa_model_name, +) def build_yield_prediction_payload(*, farm_uuid: str, plant_name: str | None = None) -> dict[str, Any]: @@ -16,9 +21,9 @@ def build_yield_prediction_payload(*, farm_uuid: str, plant_name: str | None = N "predictedYieldTons": predicted_yield_tons, "predictedYieldRaw": round(yield_estimate, 2), "unit": "تن", - "sourceUnit": "kg/ha", - "simulationEngine": result.get("engine"), - "simulationModel": result.get("model_name"), + "sourceUnit": "کیلوگرم در هکتار", + "simulationEngine": _fa_engine_name(result.get("engine")), + "simulationModel": _fa_model_name(result.get("model_name")), "scenarioId": result.get("scenario_id"), "simulationWarning": result.get("simulation_warning"), "supportingMetrics": result.get("metrics") or {},