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