This commit is contained in:
2026-04-24 22:20:15 +03:30
parent f7dc05dc9e
commit 569d520a5c
24 changed files with 687 additions and 152 deletions
+13 -10
View File
@@ -21,17 +21,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \ pkg-config \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY requirements.txt . COPY requirements.txt constraints.txt .
# Python mirrors RUN PIP_CONSTRAINT=/app/constraints.txt \
RUN pip config --user set global.index-url https://package-mirror.liara.ir/repository/pypi/simple && \ pip install \
pip config --user set global.extra-index-url https://mirror.cdn.ir/repository/pypi/simple && \ --prefer-binary \
pip config --user set global.extra-index-url https://mirror2.chabokan.net/pypi/simple && \ --index-url https://mirror-pypi.runflare.com/simple \
pip config --user set global.trusted-host package-mirror.liara.ir && \ --extra-index-url https://package-mirror.liara.ir/repository/pypi/simple \
pip config --user set global.trusted-host mirror.cdn.ir && \ --extra-index-url https://mirror.cdn.ir/repository/pypi/simple \
pip config --user set global.trusted-host mirror-pypi.runflare.com --extra-index-url https://mirror2.chabokan.net/pypi/simple \
--trusted-host mirror-pypi.runflare.com \
RUN pip install -r requirements.txt --trusted-host package-mirror.liara.ir \
--trusted-host mirror.cdn.ir \
--trusted-host mirror2.chabokan.net \
-r requirements.txt
COPY entrypoint.sh /app/entrypoint.sh COPY entrypoint.sh /app/entrypoint.sh
COPY . . COPY . .
+2 -1
View File
@@ -1,5 +1,5 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import path from django.urls import include, path
def test_view(_request): def test_view(_request):
@@ -8,4 +8,5 @@ def test_view(_request):
urlpatterns = [ urlpatterns = [
path("__test__/", test_view), path("__test__/", test_view),
path("api/rag/", include("rag.urls")),
] ]
+2
View File
@@ -0,0 +1,2 @@
# Keep build-isolated dependency resolution compatible with Python 3.10.
numpy>=1.23,<1.27
+4 -4
View File
@@ -204,7 +204,7 @@
اگر `pcse` نصب نباشد، `None` برمی‌گرداند. اگر `pcse` نصب نباشد، `None` برمی‌گرداند.
### `_resolve_model_class(bindings, model_name)` ### `_resolve_model_class(bindings, model_name)`
کلاس مدل PCSE را با نامی مثل `Wofost72_WLP_CWB` پیدا می‌کند. کلاس مدل PCSE را با نامی مثل `Wofost81_NWLP_CWB_CNB` پیدا می‌کند.
--- ---
@@ -226,13 +226,13 @@
این کلاس فقط مسئول اجرای موتور شبیه‌سازی است و وارد منطق ذخیره سناریوها نمی‌شود. این کلاس فقط مسئول اجرای موتور شبیه‌سازی است و وارد منطق ذخیره سناریوها نمی‌شود.
### `__init__(model_name="Wofost72_WLP_CWB")` ### `__init__(model_name="Wofost81_NWLP_CWB_CNB")`
مدل PCSE مورد استفاده را مشخص می‌کند. مدل PCSE مورد استفاده را مشخص می‌کند.
مدل پیش‌فرض: مدل پیش‌فرض:
```python ```python
Wofost72_WLP_CWB Wofost81_NWLP_CWB_CNB
``` ```
### `run_simulation(...)` ### `run_simulation(...)`
@@ -251,7 +251,7 @@ Wofost72_WLP_CWB
```python ```python
{ {
"engine": "pcse", "engine": "pcse",
"model_name": "Wofost72_WLP_CWB", "model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": {...}, "metrics": {...},
"daily_output": [...], "daily_output": [...],
"summary_output": [...], "summary_output": [...],
+35 -1
View File
@@ -12,7 +12,7 @@ from farm_data.models import SensorData
from plant.gdd import calculate_daily_gdd, resolve_growth_profile from plant.gdd import calculate_daily_gdd, resolve_growth_profile
from weather.models import WeatherForecast from weather.models import WeatherForecast
from .services import CropSimulationService from .services import CropSimulationService, build_simulation_payload_from_farm
DEFAULT_DYNAMIC_PARAMETERS = ["DVS", "LAI", "TAGP", "TWSO", "SM"] DEFAULT_DYNAMIC_PARAMETERS = ["DVS", "LAI", "TAGP", "TWSO", "SM"]
@@ -35,6 +35,7 @@ class GrowthSimulationError(Exception):
@dataclass @dataclass
class GrowthSimulationContext: class GrowthSimulationContext:
farm_uuid: str | None
plant_name: str plant_name: str
plant: Any plant: Any
dynamic_parameters: list[str] dynamic_parameters: list[str]
@@ -223,6 +224,7 @@ def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
page_size = min(max(int(payload.get("page_size") or DEFAULT_PAGE_SIZE), 1), MAX_PAGE_SIZE) page_size = min(max(int(payload.get("page_size") or DEFAULT_PAGE_SIZE), 1), MAX_PAGE_SIZE)
sensor = None sensor = None
resolved_farm_uuid = str(payload["farm_uuid"]) if payload.get("farm_uuid") else None
if payload.get("farm_uuid"): if payload.get("farm_uuid"):
sensor = ( sensor = (
SensorData.objects.select_related("center_location") SensorData.objects.select_related("center_location")
@@ -233,6 +235,35 @@ def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
if sensor is None: if sensor is None:
raise GrowthSimulationError("Farm not found.") raise GrowthSimulationError("Farm not found.")
if resolved_farm_uuid:
farm_payload = build_simulation_payload_from_farm(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name,
weather=payload.get("weather"),
soil=payload.get("soil_parameters"),
crop_parameters=payload.get("crop_parameters"),
agromanagement=payload.get("agromanagement"),
site_parameters=payload.get("site_parameters"),
)
weather = farm_payload["weather"]
crop_parameters = farm_payload["crop_parameters"]
soil_parameters = farm_payload["soil"]
site_parameters = farm_payload["site_parameters"]
agromanagement = farm_payload["agromanagement"]
plant = farm_payload["plant"] or plant
return GrowthSimulationContext(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name,
plant=plant,
dynamic_parameters=dynamic_parameters,
weather=weather,
crop_parameters=crop_parameters,
soil_parameters=soil_parameters,
site_parameters=site_parameters,
agromanagement=agromanagement,
page_size=page_size,
)
weather = ( weather = (
_normalize_weather_records(payload["weather"]) _normalize_weather_records(payload["weather"])
if payload.get("weather") if payload.get("weather")
@@ -265,6 +296,7 @@ def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
) )
return GrowthSimulationContext( return GrowthSimulationContext(
farm_uuid=resolved_farm_uuid,
plant_name=plant_name, plant_name=plant_name,
plant=plant, plant=plant,
dynamic_parameters=dynamic_parameters, dynamic_parameters=dynamic_parameters,
@@ -359,6 +391,8 @@ def _run_projection_engine(context: GrowthSimulationContext) -> dict[str, Any]:
def _run_simulation(context: GrowthSimulationContext) -> tuple[dict[str, Any], int | None, str | None]: def _run_simulation(context: GrowthSimulationContext) -> tuple[dict[str, Any], int | None, str | None]:
try: try:
response = CropSimulationService().run_single_simulation( response = CropSimulationService().run_single_simulation(
farm_uuid=context.farm_uuid,
plant_name=context.plant_name,
weather=context.weather, weather=context.weather,
soil=context.soil_parameters, soil=context.soil_parameters,
crop_parameters=context.crop_parameters, crop_parameters=context.crop_parameters,
@@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("crop_simulation", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="simulationscenario",
name="model_name",
field=models.CharField(default="Wofost81_NWLP_CWB_CNB", max_length=128),
),
]
+1 -1
View File
@@ -23,7 +23,7 @@ class SimulationScenario(models.Model):
default=ScenarioType.SINGLE, default=ScenarioType.SINGLE,
db_index=True, db_index=True,
) )
model_name = models.CharField(max_length=128, default="Wofost72_WLP_CWB") model_name = models.CharField(max_length=128, default="Wofost81_NWLP_CWB_CNB")
status = models.CharField( status = models.CharField(
max_length=32, max_length=32,
choices=Status.choices, choices=Status.choices,
+6 -2
View File
@@ -368,10 +368,12 @@ class SimulationRecommendationOptimizer:
irrigation_events.append({"date": day, "amount": amount_per_event}) irrigation_events.append({"date": day, "amount": amount_per_event})
try: try:
result = self.simulation_service.run_single_simulation( result = self.simulation_service.run_single_simulation(
farm_uuid=str(sensor.farm_uuid),
plant_name=getattr(plant, "name", None),
weather=weather, weather=weather,
soil=soil,
crop_parameters=crop_parameters, crop_parameters=crop_parameters,
agromanagement=agromanagement, agromanagement=agromanagement,
soil=soil,
site_parameters=site, site_parameters=site,
irrigation_recommendation={"events": irrigation_events}, irrigation_recommendation={"events": irrigation_events},
name=f"irrigation-{spec['code']}", name=f"irrigation-{spec['code']}",
@@ -605,10 +607,12 @@ class SimulationRecommendationOptimizer:
try: try:
result = self.simulation_service.run_single_simulation( result = self.simulation_service.run_single_simulation(
farm_uuid=str(sensor.farm_uuid),
plant_name=getattr(plant, "name", None),
weather=weather, weather=weather,
soil=soil,
crop_parameters=crop_parameters, crop_parameters=crop_parameters,
agromanagement=strategy_agromanagement, agromanagement=strategy_agromanagement,
soil=soil,
site_parameters=site, site_parameters=site,
name=f"fertilization-{spec['code']}", name=f"fertilization-{spec['code']}",
) )
+514 -68
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import importlib import importlib
from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime from datetime import date, datetime
from typing import Any from typing import Any
@@ -13,6 +14,9 @@ from .models import SimulationRun, SimulationScenario
DEFAULT_OUTPUT_VARS = ["DVS", "LAI", "TAGP", "TWSO", "SM"] DEFAULT_OUTPUT_VARS = ["DVS", "LAI", "TAGP", "TWSO", "SM"]
DEFAULT_SUMMARY_VARS = ["TAGP", "TWSO", "CTRAT", "RD"] DEFAULT_SUMMARY_VARS = ["TAGP", "TWSO", "CTRAT", "RD"]
DEFAULT_TERMINAL_VARS = ["TAGP", "TWSO", "LAI", "DVS"] DEFAULT_TERMINAL_VARS = ["TAGP", "TWSO", "LAI", "DVS"]
DEFAULT_PCSE_MODEL_NAME = "Wofost81_NWLP_CWB_CNB"
DEFAULT_NAVAILI = 35.0
DEFAULT_WAV = 40.0
class CropSimulationError(Exception): class CropSimulationError(Exception):
@@ -229,6 +233,265 @@ def _pick_first_not_none(*values: Any) -> Any:
return None return None
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 _clamp(value: float, lower: float, upper: float) -> float:
return max(lower, min(value, upper))
def _sensor_metric(sensor: Any, metric_name: str) -> float | None:
if sensor is None:
return None
if hasattr(sensor, metric_name):
value = getattr(sensor, metric_name)
if value is not None:
return _safe_float(value)
payload = getattr(sensor, "sensor_payload", None) or {}
if not isinstance(payload, dict):
return None
for block in payload.values():
if isinstance(block, dict) and block.get(metric_name) is not None:
return _safe_float(block.get(metric_name))
return None
def _extract_plant_simulation_profile(plant: Any | None) -> dict[str, Any] | None:
if plant is None:
return None
for attr in ("growth_profile", "irrigation_profile", "health_profile"):
profile = getattr(plant, attr, None) or {}
if not isinstance(profile, dict):
continue
simulation = profile.get("simulation")
if isinstance(simulation, dict):
return simulation
return None
def _build_default_crop_parameters(plant: Any | None, crop_name: str) -> dict[str, Any]:
profile = getattr(plant, "growth_profile", None) or {}
required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
return {
"crop_name": crop_name,
"TSUM1": round(required_gdd * 0.45, 3),
"TSUM2": round(required_gdd * 0.55, 3),
"YIELD_SCALE": 1.0,
"MAX_LAI": 5.0,
"MAX_BIOMASS": 12000.0,
}
def _build_default_agromanagement(crop_name: str, weather: list[dict[str, Any]]) -> list[dict[str, Any]]:
first_day = weather[0]["DAY"]
last_day = weather[-1]["DAY"]
crop_end = max(last_day, first_day + (last_day - first_day))
return [
{
first_day: {
"CropCalendar": {
"crop_name": crop_name,
"variety_name": "default",
"crop_start_date": first_day,
"crop_start_type": "sowing",
"crop_end_date": crop_end,
"crop_end_type": "harvest",
"max_duration": max((crop_end - first_day).days, 1),
},
"TimedEvents": [],
"StateEvents": [],
}
}
]
def _build_weather_from_forecasts(forecasts: list[Any], *, latitude: float, longitude: float) -> list[dict[str, Any]]:
return [
{
"DAY": forecast.forecast_date,
"LAT": latitude,
"LON": longitude,
"ELEV": 1200.0,
"IRRAD": 16_000_000.0,
"TMIN": _safe_float(
_pick_first_not_none(forecast.temperature_min, forecast.temperature_mean),
12.0,
),
"TMAX": _safe_float(
_pick_first_not_none(forecast.temperature_max, forecast.temperature_mean),
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),
}
for forecast in forecasts
]
def _normalize_site_parameters_for_model(
model_name: str,
site_parameters: dict[str, Any] | None,
*,
soil_parameters: dict[str, Any] | None = None,
) -> dict[str, Any]:
site = dict(site_parameters or {})
soil = soil_parameters or {}
site.setdefault("WAV", _safe_float(site.get("WAV"), DEFAULT_WAV))
if model_name.startswith("Wofost81_NWLP"):
navaili = _pick_first_not_none(
site.get("NAVAILI"),
site.get("navaili"),
site.get("nitrogen"),
soil.get("NAVAILI"),
soil.get("nitrogen"),
)
site["NAVAILI"] = _safe_float(navaili, DEFAULT_NAVAILI)
site.setdefault("BG_N_SUPPLY", 0.05)
site.setdefault("NSOILBASE", max(site["NAVAILI"] * 0.35, 5.0))
site.setdefault("NSOILBASE_FR", 0.02)
return site
def build_simulation_payload_from_farm(
*,
farm_uuid: str,
plant_name: str | None = None,
weather: Any | None = None,
soil: dict[str, Any] | None = None,
crop_parameters: dict[str, Any] | None = None,
agromanagement: Any | None = None,
site_parameters: dict[str, Any] | None = None,
) -> dict[str, Any]:
from farm_data.models import SensorData
from weather.models import WeatherForecast
farm = (
SensorData.objects.select_related("center_location", "irrigation_method")
.prefetch_related("plants", "center_location__depths")
.filter(farm_uuid=farm_uuid)
.first()
)
if farm is None:
raise CropSimulationError(f"Farm with uuid={farm_uuid} not found.")
plant = None
if plant_name:
plant = farm.plants.filter(name=plant_name).first()
if plant is None:
plant = farm.plants.first()
if weather is not None:
resolved_weather = _normalize_weather_records(weather)
else:
forecasts = list(
WeatherForecast.objects.filter(location=farm.center_location)
.order_by("forecast_date")[:14]
)
if not forecasts:
raise CropSimulationError(
"Weather data for the selected farm is missing."
)
resolved_weather = _build_weather_from_forecasts(
forecasts,
latitude=float(farm.center_location.latitude),
longitude=float(farm.center_location.longitude),
)
depths = list(farm.center_location.depths.all())
top_depth = depths[0] if depths else None
smfcf = _clamp(_safe_float(getattr(top_depth, "wv0033", None), 0.34), 0.2, 0.55)
smw = _clamp(_safe_float(getattr(top_depth, "wv1500", None), 0.14), 0.05, max(smfcf - 0.02, 0.06))
soil_moisture = _sensor_metric(farm, "soil_moisture")
wav = (
round(_clamp((soil_moisture or DEFAULT_WAV) / 100.0, smw, smfcf) * 100.0, 3)
if soil_moisture is not None
else DEFAULT_WAV
)
nitrogen = _pick_first_not_none(_sensor_metric(farm, "nitrogen"), getattr(top_depth, "nitrogen", None))
phosphorus = _sensor_metric(farm, "phosphorus")
potassium = _sensor_metric(farm, "potassium")
soil_ph = _pick_first_not_none(_sensor_metric(farm, "soil_ph"), getattr(top_depth, "phh2o", None))
ec = _sensor_metric(farm, "electrical_conductivity")
resolved_soil = {
"SMFCF": round(smfcf, 3),
"SMW": round(smw, 3),
"RDMSOL": 120.0,
"soil_moisture": soil_moisture,
"nitrogen": _safe_float(nitrogen, DEFAULT_NAVAILI),
"phosphorus": _safe_float(phosphorus, 0.0),
"potassium": _safe_float(potassium, 0.0),
"soil_ph": _safe_float(soil_ph, 7.0),
"electrical_conductivity": _safe_float(ec, 0.0),
"clay": _safe_float(getattr(top_depth, "clay", None), 0.0),
"sand": _safe_float(getattr(top_depth, "sand", None), 0.0),
"silt": _safe_float(getattr(top_depth, "silt", None), 0.0),
"cec": _safe_float(getattr(top_depth, "cec", None), 0.0),
"soc": _safe_float(getattr(top_depth, "soc", None), 0.0),
}
if soil:
resolved_soil.update(soil)
resolved_site = {
"WAV": wav,
"NAVAILI": _safe_float(nitrogen, DEFAULT_NAVAILI),
"P_STATUS": _safe_float(phosphorus, 0.0),
"K_STATUS": _safe_float(potassium, 0.0),
"SOIL_PH": _safe_float(soil_ph, 7.0),
"EC": _safe_float(ec, 0.0),
}
if site_parameters:
resolved_site.update(site_parameters)
simulation_profile = _extract_plant_simulation_profile(plant)
default_crop = (
deepcopy(simulation_profile.get("crop_parameters"))
if simulation_profile and isinstance(simulation_profile.get("crop_parameters"), dict)
else _build_default_crop_parameters(plant, plant_name or getattr(plant, "name", "crop"))
)
resolved_crop = default_crop
if crop_parameters:
resolved_crop.update(crop_parameters)
resolved_crop.setdefault("crop_name", plant_name or getattr(plant, "name", "crop"))
resolved_crop.setdefault("farm_uuid", str(farm_uuid))
resolved_crop.setdefault("soil_ph", _safe_float(soil_ph, 7.0))
resolved_crop.setdefault("soil_nitrogen", _safe_float(nitrogen, DEFAULT_NAVAILI))
resolved_crop.setdefault("soil_phosphorus", _safe_float(phosphorus, 0.0))
resolved_crop.setdefault("soil_potassium", _safe_float(potassium, 0.0))
default_agromanagement = (
deepcopy(simulation_profile.get("agromanagement"))
if simulation_profile and simulation_profile.get("agromanagement")
else _build_default_agromanagement(resolved_crop["crop_name"], resolved_weather)
)
resolved_agromanagement = agromanagement if agromanagement is not None else default_agromanagement
return {
"farm": farm,
"plant": plant,
"weather": resolved_weather,
"soil": resolved_soil,
"site_parameters": resolved_site,
"crop_parameters": resolved_crop,
"agromanagement": resolved_agromanagement,
}
def _extract_total_n(agromanagement: list[dict[str, Any]]) -> float: def _extract_total_n(agromanagement: list[dict[str, Any]]) -> float:
total_n = 0.0 total_n = 0.0
for campaign in agromanagement: for campaign in agromanagement:
@@ -250,6 +513,73 @@ def _extract_total_n(agromanagement: list[dict[str, Any]]) -> float:
return total_n return total_n
def _estimate_pk_stress_factor(
*,
soil: dict[str, Any],
site: dict[str, Any],
crop: dict[str, Any],
) -> dict[str, float]:
phosphorus = _safe_float(
_pick_first_not_none(site.get("P_STATUS"), soil.get("phosphorus"), crop.get("soil_phosphorus")),
0.0,
)
potassium = _safe_float(
_pick_first_not_none(site.get("K_STATUS"), soil.get("potassium"), crop.get("soil_potassium")),
0.0,
)
soil_ph = _safe_float(
_pick_first_not_none(site.get("SOIL_PH"), soil.get("soil_ph"), crop.get("soil_ph")),
7.0,
)
ec = _safe_float(_pick_first_not_none(site.get("EC"), soil.get("electrical_conductivity")), 0.0)
phosphorus_target = _safe_float(crop.get("P_OPTIMAL"), 30.0)
potassium_target = _safe_float(crop.get("K_OPTIMAL"), 45.0)
p_factor = _clamp(phosphorus / max(phosphorus_target, 1.0), 0.45, 1.0)
k_factor = _clamp(potassium / max(potassium_target, 1.0), 0.45, 1.0)
ph_penalty = 1.0
if soil_ph < 5.8:
ph_penalty = _clamp(1.0 - ((5.8 - soil_ph) * 0.08), 0.65, 1.0)
elif soil_ph > 7.8:
ph_penalty = _clamp(1.0 - ((soil_ph - 7.8) * 0.06), 0.7, 1.0)
ec_penalty = 1.0
if ec > 2.5:
ec_penalty = _clamp(1.0 - ((ec - 2.5) * 0.07), 0.72, 1.0)
combined_factor = round(_clamp(p_factor * k_factor * ph_penalty * ec_penalty, 0.35, 1.0), 4)
return {
"phosphorus_factor": round(p_factor, 4),
"potassium_factor": round(k_factor, 4),
"ph_penalty": round(ph_penalty, 4),
"ec_penalty": round(ec_penalty, 4),
"combined_factor": combined_factor,
}
def _apply_pk_adjustment(
result: dict[str, Any],
*,
soil: dict[str, Any],
site: dict[str, Any],
crop: dict[str, Any],
) -> dict[str, Any]:
adjustment = _estimate_pk_stress_factor(soil=soil, site=site, crop=crop)
factor = adjustment["combined_factor"]
if factor >= 0.995:
result["nutrient_adjustment"] = adjustment
return result
metrics = dict(result.get("metrics", {}))
for key, scale in {"yield_estimate": factor, "biomass": factor, "max_lai": max(factor, 0.6)}.items():
if metrics.get(key) is not None:
metrics[key] = round(_safe_float(metrics[key]) * scale, 4)
result["metrics"] = metrics
result["nutrient_adjustment"] = adjustment
return result
def _load_pcse_bindings() -> dict[str, Any] | None: def _load_pcse_bindings() -> dict[str, Any] | None:
try: try:
base_module = importlib.import_module("pcse.base") base_module = importlib.import_module("pcse.base")
@@ -288,7 +618,7 @@ class PreparedSimulationInput:
class PcseSimulationManager: class PcseSimulationManager:
def __init__(self, model_name: str = "Wofost72_WLP_CWB"): def __init__(self, model_name: str = DEFAULT_PCSE_MODEL_NAME):
self.model_name = model_name self.model_name = model_name
def run_simulation( def run_simulation(
@@ -304,7 +634,11 @@ class PcseSimulationManager:
weather=_normalize_weather_records(weather), weather=_normalize_weather_records(weather),
soil=soil or {}, soil=soil or {},
crop=crop_parameters or {}, crop=crop_parameters or {},
site=site_parameters or {}, site=_normalize_site_parameters_for_model(
self.model_name,
site_parameters or {},
soil_parameters=soil or {},
),
agromanagement=_normalize_agromanagement(agromanagement), agromanagement=_normalize_agromanagement(agromanagement),
) )
bindings = _load_pcse_bindings() bindings = _load_pcse_bindings()
@@ -312,7 +646,15 @@ class PcseSimulationManager:
raise CropSimulationError( raise CropSimulationError(
"PCSE is not installed or required PCSE classes could not be loaded." "PCSE is not installed or required PCSE classes could not be loaded."
) )
return self._run_with_pcse(prepared, bindings) result = self._run_with_pcse(prepared, bindings)
if self.model_name.startswith("Wofost81_NWLP"):
result = _apply_pk_adjustment(
result,
soil=prepared.soil,
site=prepared.site,
crop=prepared.crop,
)
return result
def _run_with_pcse( def _run_with_pcse(
self, self,
@@ -407,20 +749,73 @@ class CropSimulationService:
def __init__(self, manager: PcseSimulationManager | None = None): def __init__(self, manager: PcseSimulationManager | None = None):
self.manager = manager or PcseSimulationManager() self.manager = manager or PcseSimulationManager()
def _resolve_common_inputs(
self,
*,
farm_uuid: str | None = None,
plant_name: str | None = None,
weather: Any | None = None,
soil: dict[str, Any] | None = None,
crop_parameters: dict[str, Any] | None = None,
agromanagement: Any | None = None,
site_parameters: dict[str, Any] | None = None,
) -> dict[str, Any]:
if not farm_uuid:
return {
"weather": weather,
"soil": soil or {},
"crop_parameters": crop_parameters or {},
"agromanagement": agromanagement,
"site_parameters": _normalize_site_parameters_for_model(
self.manager.model_name,
site_parameters or {},
soil_parameters=soil or {},
),
"farm": None,
"plant": None,
}
base = build_simulation_payload_from_farm(
farm_uuid=str(farm_uuid),
plant_name=plant_name or (crop_parameters or {}).get("crop_name"),
weather=weather,
soil=soil,
crop_parameters=crop_parameters,
agromanagement=agromanagement,
site_parameters=site_parameters,
)
base["site_parameters"] = _normalize_site_parameters_for_model(
self.manager.model_name,
base.get("site_parameters"),
soil_parameters=base.get("soil"),
)
return base
def run_single_simulation( def run_single_simulation(
self, self,
*, *,
weather: Any, weather: Any | None = None,
soil: dict[str, Any], soil: dict[str, Any] | None = None,
crop_parameters: dict[str, Any], crop_parameters: dict[str, Any] | None = None,
agromanagement: Any, agromanagement: Any | None = None,
site_parameters: dict[str, Any] | None = None, site_parameters: dict[str, Any] | None = None,
irrigation_recommendation: dict[str, Any] | None = None, irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None, fertilization_recommendation: dict[str, Any] | None = None,
name: str = "", name: str = "",
farm_uuid: str | None = None,
plant_name: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
resolved = self._resolve_common_inputs(
farm_uuid=farm_uuid,
plant_name=plant_name,
weather=weather,
soil=soil,
crop_parameters=crop_parameters,
agromanagement=agromanagement,
site_parameters=site_parameters,
)
merged_agromanagement = _merge_management_recommendations( merged_agromanagement = _merge_management_recommendations(
agromanagement, resolved["agromanagement"],
irrigation_recommendation=irrigation_recommendation, irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation, fertilization_recommendation=fertilization_recommendation,
) )
@@ -430,13 +825,15 @@ class CropSimulationService:
model_name=self.manager.model_name, model_name=self.manager.model_name,
input_payload=_json_ready( input_payload=_json_ready(
{ {
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_parameters, "crop_parameters": resolved["crop_parameters"],
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
"irrigation_recommendation": irrigation_recommendation or {}, "irrigation_recommendation": irrigation_recommendation or {},
"fertilization_recommendation": fertilization_recommendation or {}, "fertilization_recommendation": fertilization_recommendation or {},
"farm_uuid": farm_uuid,
"plant_name": plant_name,
} }
), ),
) )
@@ -444,10 +841,10 @@ class CropSimulationService:
scenario=scenario, scenario=scenario,
run_key="single", run_key="single",
label=name or "single", label=name or "single",
weather_payload=_json_ready(weather), weather_payload=_json_ready(resolved["weather"]),
soil_payload=_json_ready(soil), soil_payload=_json_ready(resolved["soil"]),
crop_payload=_json_ready(crop_parameters), crop_payload=_json_ready(resolved["crop_parameters"]),
site_payload=_json_ready(site_parameters or {}), site_payload=_json_ready(resolved["site_parameters"]),
agromanagement_payload=_json_ready(merged_agromanagement), agromanagement_payload=_json_ready(merged_agromanagement),
) )
return self._execute_scenario( return self._execute_scenario(
@@ -455,10 +852,10 @@ class CropSimulationService:
run_specs=[ run_specs=[
{ {
"instance": run, "instance": run,
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_parameters, "crop_parameters": resolved["crop_parameters"],
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
} }
], ],
@@ -467,18 +864,27 @@ class CropSimulationService:
def compare_crops( def compare_crops(
self, self,
*, *,
weather: Any, weather: Any | None = None,
soil: dict[str, Any], soil: dict[str, Any] | None = None,
crop_a: dict[str, Any], crop_a: dict[str, Any],
crop_b: dict[str, Any], crop_b: dict[str, Any],
agromanagement: Any, agromanagement: Any | None = None,
site_parameters: dict[str, Any] | None = None, site_parameters: dict[str, Any] | None = None,
irrigation_recommendation: dict[str, Any] | None = None, irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None, fertilization_recommendation: dict[str, Any] | None = None,
name: str = "", name: str = "",
farm_uuid: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
resolved = self._resolve_common_inputs(
farm_uuid=farm_uuid,
weather=weather,
soil=soil,
crop_parameters=None,
agromanagement=agromanagement,
site_parameters=site_parameters,
)
merged_agromanagement = _merge_management_recommendations( merged_agromanagement = _merge_management_recommendations(
agromanagement, resolved["agromanagement"],
irrigation_recommendation=irrigation_recommendation, irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation, fertilization_recommendation=fertilization_recommendation,
) )
@@ -488,14 +894,15 @@ class CropSimulationService:
model_name=self.manager.model_name, model_name=self.manager.model_name,
input_payload=_json_ready( input_payload=_json_ready(
{ {
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_a": crop_a, "crop_a": crop_a,
"crop_b": crop_b, "crop_b": crop_b,
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
"irrigation_recommendation": irrigation_recommendation or {}, "irrigation_recommendation": irrigation_recommendation or {},
"fertilization_recommendation": fertilization_recommendation or {}, "fertilization_recommendation": fertilization_recommendation or {},
"farm_uuid": farm_uuid,
} }
), ),
) )
@@ -504,20 +911,20 @@ class CropSimulationService:
scenario=scenario, scenario=scenario,
run_key="crop_a", run_key="crop_a",
label=crop_a.get("crop_name", "crop_a"), label=crop_a.get("crop_name", "crop_a"),
weather_payload=_json_ready(weather), weather_payload=_json_ready(resolved["weather"]),
soil_payload=_json_ready(soil), soil_payload=_json_ready(resolved["soil"]),
crop_payload=_json_ready(crop_a), crop_payload=_json_ready(crop_a),
site_payload=_json_ready(site_parameters or {}), site_payload=_json_ready(resolved["site_parameters"]),
agromanagement_payload=_json_ready(merged_agromanagement), agromanagement_payload=_json_ready(merged_agromanagement),
), ),
SimulationRun.objects.create( SimulationRun.objects.create(
scenario=scenario, scenario=scenario,
run_key="crop_b", run_key="crop_b",
label=crop_b.get("crop_name", "crop_b"), label=crop_b.get("crop_name", "crop_b"),
weather_payload=_json_ready(weather), weather_payload=_json_ready(resolved["weather"]),
soil_payload=_json_ready(soil), soil_payload=_json_ready(resolved["soil"]),
crop_payload=_json_ready(crop_b), crop_payload=_json_ready(crop_b),
site_payload=_json_ready(site_parameters or {}), site_payload=_json_ready(resolved["site_parameters"]),
agromanagement_payload=_json_ready(merged_agromanagement), agromanagement_payload=_json_ready(merged_agromanagement),
), ),
] ]
@@ -526,18 +933,18 @@ class CropSimulationService:
run_specs=[ run_specs=[
{ {
"instance": runs[0], "instance": runs[0],
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_a, "crop_parameters": crop_a,
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
}, },
{ {
"instance": runs[1], "instance": runs[1],
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_b, "crop_parameters": crop_b,
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
}, },
], ],
@@ -546,20 +953,44 @@ class CropSimulationService:
def recommend_best_crop( def recommend_best_crop(
self, self,
*, *,
weather: Any, weather: Any | None = None,
soil: dict[str, Any], soil: dict[str, Any] | None = None,
crops: list[dict[str, Any]], crops: list[dict[str, Any]] | None = None,
agromanagement: Any, agromanagement: Any | None = None,
site_parameters: dict[str, Any] | None = None, site_parameters: dict[str, Any] | None = None,
irrigation_recommendation: dict[str, Any] | None = None, irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None, fertilization_recommendation: dict[str, Any] | None = None,
name: str = "", name: str = "",
farm_uuid: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
if not crops and farm_uuid:
base = build_simulation_payload_from_farm(farm_uuid=str(farm_uuid))
crops = []
for plant in base["farm"].plants.all():
simulation_profile = _extract_plant_simulation_profile(plant)
crop_payload = (
deepcopy(simulation_profile.get("crop_parameters"))
if simulation_profile and isinstance(simulation_profile.get("crop_parameters"), dict)
else _build_default_crop_parameters(plant, plant.name)
)
crop_payload.setdefault("crop_name", plant.name)
crop_payload.setdefault("label", plant.name)
crops.append(crop_payload)
crops = crops or []
if len(crops) < 2: if len(crops) < 2:
raise CropSimulationError("At least two crop options are required.") raise CropSimulationError("At least two crop options are required.")
resolved = self._resolve_common_inputs(
farm_uuid=farm_uuid,
weather=weather,
soil=soil,
crop_parameters=None,
agromanagement=agromanagement,
site_parameters=site_parameters,
)
merged_agromanagement = _merge_management_recommendations( merged_agromanagement = _merge_management_recommendations(
agromanagement, resolved["agromanagement"],
irrigation_recommendation=irrigation_recommendation, irrigation_recommendation=irrigation_recommendation,
fertilization_recommendation=fertilization_recommendation, fertilization_recommendation=fertilization_recommendation,
) )
@@ -570,13 +1001,14 @@ class CropSimulationService:
model_name=self.manager.model_name, model_name=self.manager.model_name,
input_payload=_json_ready( input_payload=_json_ready(
{ {
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crops": crops, "crops": crops,
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
"irrigation_recommendation": irrigation_recommendation or {}, "irrigation_recommendation": irrigation_recommendation or {},
"fertilization_recommendation": fertilization_recommendation or {}, "fertilization_recommendation": fertilization_recommendation or {},
"farm_uuid": farm_uuid,
} }
), ),
) )
@@ -593,19 +1025,19 @@ class CropSimulationService:
scenario=scenario, scenario=scenario,
run_key=f"crop_{index}", run_key=f"crop_{index}",
label=label, label=label,
weather_payload=_json_ready(weather), weather_payload=_json_ready(resolved["weather"]),
soil_payload=_json_ready(soil), soil_payload=_json_ready(resolved["soil"]),
crop_payload=_json_ready(crop), crop_payload=_json_ready(crop),
site_payload=_json_ready(site_parameters or {}), site_payload=_json_ready(resolved["site_parameters"]),
agromanagement_payload=_json_ready(merged_agromanagement), agromanagement_payload=_json_ready(merged_agromanagement),
) )
run_specs.append( run_specs.append(
{ {
"instance": run, "instance": run,
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop, "crop_parameters": crop,
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
} }
) )
@@ -627,31 +1059,44 @@ class CropSimulationService:
def compare_fertilization_strategies( def compare_fertilization_strategies(
self, self,
*, *,
weather: Any, weather: Any | None = None,
soil: dict[str, Any], soil: dict[str, Any] | None = None,
crop_parameters: dict[str, Any], crop_parameters: dict[str, Any] | None = None,
strategies: list[dict[str, Any]], strategies: list[dict[str, Any]],
site_parameters: dict[str, Any] | None = None, site_parameters: dict[str, Any] | None = None,
irrigation_recommendation: dict[str, Any] | None = None, irrigation_recommendation: dict[str, Any] | None = None,
fertilization_recommendation: dict[str, Any] | None = None, fertilization_recommendation: dict[str, Any] | None = None,
name: str = "", name: str = "",
farm_uuid: str | None = None,
plant_name: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
if len(strategies) < 2: if len(strategies) < 2:
raise CropSimulationError("At least two fertilization strategies are required.") raise CropSimulationError("At least two fertilization strategies are required.")
resolved = self._resolve_common_inputs(
farm_uuid=farm_uuid,
plant_name=plant_name,
weather=weather,
soil=soil,
crop_parameters=crop_parameters,
agromanagement=None,
site_parameters=site_parameters,
)
scenario = SimulationScenario.objects.create( scenario = SimulationScenario.objects.create(
name=name, name=name,
scenario_type=SimulationScenario.ScenarioType.FERTILIZATION_COMPARISON, scenario_type=SimulationScenario.ScenarioType.FERTILIZATION_COMPARISON,
model_name=self.manager.model_name, model_name=self.manager.model_name,
input_payload=_json_ready( input_payload=_json_ready(
{ {
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_parameters, "crop_parameters": resolved["crop_parameters"],
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"strategies": strategies, "strategies": strategies,
"irrigation_recommendation": irrigation_recommendation or {}, "irrigation_recommendation": irrigation_recommendation or {},
"fertilization_recommendation": fertilization_recommendation or {}, "fertilization_recommendation": fertilization_recommendation or {},
"farm_uuid": farm_uuid,
"plant_name": plant_name,
} }
), ),
) )
@@ -666,19 +1111,19 @@ class CropSimulationService:
scenario=scenario, scenario=scenario,
run_key=f"strategy_{index}", run_key=f"strategy_{index}",
label=strategy.get("label", f"strategy_{index}"), label=strategy.get("label", f"strategy_{index}"),
weather_payload=_json_ready(weather), weather_payload=_json_ready(resolved["weather"]),
soil_payload=_json_ready(soil), soil_payload=_json_ready(resolved["soil"]),
crop_payload=_json_ready(crop_parameters), crop_payload=_json_ready(resolved["crop_parameters"]),
site_payload=_json_ready(site_parameters or {}), site_payload=_json_ready(resolved["site_parameters"]),
agromanagement_payload=_json_ready(merged_agromanagement), agromanagement_payload=_json_ready(merged_agromanagement),
) )
run_specs.append( run_specs.append(
{ {
"instance": run, "instance": run,
"weather": weather, "weather": resolved["weather"],
"soil": soil, "soil": resolved["soil"],
"crop_parameters": crop_parameters, "crop_parameters": resolved["crop_parameters"],
"site_parameters": site_parameters or {}, "site_parameters": resolved["site_parameters"],
"agromanagement": merged_agromanagement, "agromanagement": merged_agromanagement,
} }
) )
@@ -797,6 +1242,7 @@ class CropSimulationService:
"runs": run_metrics, "runs": run_metrics,
} }
if scenario.scenario_type == SimulationScenario.ScenarioType.CROP_COMPARISON: if scenario.scenario_type == SimulationScenario.ScenarioType.CROP_COMPARISON:
if len(run_metrics) >= 2:
payload["comparison"]["yield_gap"] = round( payload["comparison"]["yield_gap"] = round(
abs(run_metrics[0]["yield_estimate"] - run_metrics[1]["yield_estimate"]), abs(run_metrics[0]["yield_estimate"] - run_metrics[1]["yield_estimate"]),
3, 3,
+3 -3
View File
@@ -117,7 +117,7 @@ class CropSimulationServiceTests(TestCase):
side_effect=[ side_effect=[
{ {
"engine": "pcse", "engine": "pcse",
"model_name": "Wofost72_WLP_CWB", "model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": { "metrics": {
"yield_estimate": 5200.0, "yield_estimate": 5200.0,
"biomass": 9800.0, "biomass": 9800.0,
@@ -129,7 +129,7 @@ class CropSimulationServiceTests(TestCase):
}, },
{ {
"engine": "pcse", "engine": "pcse",
"model_name": "Wofost72_WLP_CWB", "model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": { "metrics": {
"yield_estimate": 6100.0, "yield_estimate": 6100.0,
"biomass": 11000.0, "biomass": 11000.0,
@@ -174,7 +174,7 @@ class CropSimulationServiceTests(TestCase):
captured.update(kwargs) captured.update(kwargs)
return { return {
"engine": "pcse", "engine": "pcse",
"model_name": "Wofost72_WLP_CWB", "model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": { "metrics": {
"yield_estimate": 5400.0, "yield_estimate": 5400.0,
"biomass": 9800.0, "biomass": 9800.0,
+1
View File
@@ -12,6 +12,7 @@ class FertilizationConfig(AppConfig):
@cached_property @cached_property
def optimizer_defaults(self): def optimizer_defaults(self):
return { return {
"simulation_model": "Wofost81_NWLP_CWB_CNB",
"validity_days": 7, "validity_days": 7,
"rain_delay_threshold_mm": 3.0, "rain_delay_threshold_mm": 3.0,
"stage_targets": { "stage_targets": {
+9 -1
View File
@@ -4,11 +4,19 @@ from rest_framework import serializers
class FertilizationRecommendRequestSerializer(serializers.Serializer): class FertilizationRecommendRequestSerializer(serializers.Serializer):
"""سریالایزر ورودی برای درخواست توصیه کودهی.""" """سریالایزر ورودی برای درخواست توصیه کودهی."""
sensor_uuid = serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)") farm_uuid = serializers.CharField(required=False, help_text="شناسه یکتای مزرعه (اجباری)")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه") plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد گیاه") growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد گیاه")
query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری") query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری")
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
return attrs
class FertilizationPlanSerializer(serializers.Serializer): class FertilizationPlanSerializer(serializers.Serializer):
"""سریالایزر خروجی برای پلن توصیه کودهی.""" """سریالایزر خروجی برای پلن توصیه کودهی."""
+4 -4
View File
@@ -29,7 +29,7 @@ FertilizationResponseSerializer = build_envelope_serializer(
class FertilizationRecommendView(APIView): class FertilizationRecommendView(APIView):
""" """
توصیه کودهی به صورت مستقیم. توصیه کودهی به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage. POST با farm_uuid، plant_name، growth_stage.
اطلاعات گیاه از plant app دریافت میشود. اطلاعات گیاه از plant app دریافت میشود.
نیازی به دریافت نوع آبیاری نیست. نیازی به دریافت نوع آبیاری نیست.
""" """
@@ -62,7 +62,7 @@ class FertilizationRecommendView(APIView):
OpenApiExample( OpenApiExample(
"نمونه درخواست", "نمونه درخواست",
value={ value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی", "plant_name": "گوجه‌فرنگی",
"growth_stage": "گلدهی", "growth_stage": "گلدهی",
}, },
@@ -81,14 +81,14 @@ class FertilizationRecommendView(APIView):
) )
validated = serializer.validated_data validated = serializer.validated_data
sensor_uuid = validated["sensor_uuid"] farm_uuid = validated["farm_uuid"]
plant_name = validated.get("plant_name") plant_name = validated.get("plant_name")
growth_stage = validated.get("growth_stage") growth_stage = validated.get("growth_stage")
query = validated.get("query") query = validated.get("query")
try: try:
result = get_fertilization_recommendation( result = get_fertilization_recommendation(
sensor_uuid=sensor_uuid, farm_uuid=farm_uuid,
plant_name=plant_name, plant_name=plant_name,
growth_stage=growth_stage, growth_stage=growth_stage,
query=query, query=query,
+1
View File
@@ -12,6 +12,7 @@ class IrrigationConfig(AppConfig):
@cached_property @cached_property
def optimizer_defaults(self): def optimizer_defaults(self):
return { return {
"simulation_model": "Wofost81_NWLP_CWB_CNB",
"validity_days": 3, "validity_days": 3,
"minimum_event_mm": 4.0, "minimum_event_mm": 4.0,
"significant_rain_threshold_mm": 4.0, "significant_rain_threshold_mm": 4.0,
+9 -1
View File
@@ -28,7 +28,8 @@ class IrrigationMethodSerializer(serializers.ModelSerializer):
class IrrigationRecommendRequestSerializer(serializers.Serializer): class IrrigationRecommendRequestSerializer(serializers.Serializer):
"""سریالایزر ورودی برای درخواست توصیه آبیاری.""" """سریالایزر ورودی برای درخواست توصیه آبیاری."""
sensor_uuid = serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)") farm_uuid = serializers.CharField(required=False, help_text="شناسه یکتای مزرعه (اجباری)")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه") plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد گیاه") growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد گیاه")
irrigation_method_name = serializers.CharField( irrigation_method_name = serializers.CharField(
@@ -36,6 +37,13 @@ class IrrigationRecommendRequestSerializer(serializers.Serializer):
) )
query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری") query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری")
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
return attrs
class IrrigationPlanSerializer(serializers.Serializer): class IrrigationPlanSerializer(serializers.Serializer):
"""سریالایزر خروجی برای پلن توصیه آبیاری.""" """سریالایزر خروجی برای پلن توصیه آبیاری."""
+4 -4
View File
@@ -107,7 +107,7 @@ class IrrigationMethodListCreateView(APIView):
class IrrigationRecommendView(APIView): class IrrigationRecommendView(APIView):
""" """
توصیه آبیاری به صورت مستقیم. توصیه آبیاری به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name. POST با farm_uuid، plant_name، growth_stage، irrigation_method_name.
اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت میشود. اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت میشود.
""" """
@@ -139,7 +139,7 @@ class IrrigationRecommendView(APIView):
OpenApiExample( OpenApiExample(
"نمونه درخواست", "نمونه درخواست",
value={ value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی", "plant_name": "گوجه‌فرنگی",
"growth_stage": "گلدهی", "growth_stage": "گلدهی",
"irrigation_method_name": "آبیاری قطره‌ای", "irrigation_method_name": "آبیاری قطره‌ای",
@@ -159,7 +159,7 @@ class IrrigationRecommendView(APIView):
) )
validated = serializer.validated_data validated = serializer.validated_data
sensor_uuid = validated["sensor_uuid"] farm_uuid = validated["farm_uuid"]
plant_name = validated.get("plant_name") plant_name = validated.get("plant_name")
growth_stage = validated.get("growth_stage") growth_stage = validated.get("growth_stage")
irrigation_method_name = validated.get("irrigation_method_name") irrigation_method_name = validated.get("irrigation_method_name")
@@ -167,7 +167,7 @@ class IrrigationRecommendView(APIView):
try: try:
result = get_irrigation_recommendation( result = get_irrigation_recommendation(
sensor_uuid=sensor_uuid, farm_uuid=farm_uuid,
plant_name=plant_name, plant_name=plant_name,
growth_stage=growth_stage, growth_stage=growth_stage,
irrigation_method_name=irrigation_method_name, irrigation_method_name=irrigation_method_name,
+13 -8
View File
@@ -173,19 +173,20 @@ def _merge_fertilization_response(
def get_fertilization_recommendation( def get_fertilization_recommendation(
sensor_uuid: str, farm_uuid: str | None = None,
plant_name: str | None = None, plant_name: str | None = None,
growth_stage: str | None = None, growth_stage: str | None = None,
query: str | None = None, query: str | None = None,
config: RAGConfig | None = None, config: RAGConfig | None = None,
limit: int = 8, limit: int = 8,
sensor_uuid: str | None = None,
) -> dict: ) -> dict:
""" """
توصیه کودهی برای یک سنسور (کاربر). توصیه کودهی برای یک مزرعه.
از RAG با پایگاه دانش fertilization استفاده میکند. از RAG با پایگاه دانش fertilization استفاده میکند.
Args: Args:
sensor_uuid: شناسه سنسور کاربر farm_uuid: شناسه مزرعه
plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant) plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant)
growth_stage: مرحله رشد گیاه growth_stage: مرحله رشد گیاه
query: سوال اختیاری query: سوال اختیاری
@@ -193,7 +194,7 @@ def get_fertilization_recommendation(
limit: تعداد چانکهای بازیابیشده limit: تعداد چانکهای بازیابیشده
Returns: Returns:
dict با کلیدهای fertilizer_needed, fertilizer_type, amount_kg_per_hectare, reason, npk_status, raw_response dict ساختاریافته برای توصیه کودهی
""" """
cfg = config or load_rag_config() cfg = config or load_rag_config()
service = get_service_config(SERVICE_ID, cfg) service = get_service_config(SERVICE_ID, cfg)
@@ -209,12 +210,16 @@ def get_fertilization_recommendation(
client = get_chat_client(service_cfg) client = get_chat_client(service_cfg)
model = service.llm.model model = service.llm.model
resolved_farm_uuid = str(farm_uuid or sensor_uuid or "").strip()
if not resolved_farm_uuid:
raise ValueError("farm_uuid is required.")
user_query = query or "توصیه کودهی برای مزرعه من چیست؟" user_query = query or "توصیه کودهی برای مزرعه من چیست؟"
sensor = ( sensor = (
SensorData.objects.select_related("center_location") SensorData.objects.select_related("center_location")
.prefetch_related("plants") .prefetch_related("plants")
.filter(farm_uuid=sensor_uuid) .filter(farm_uuid=resolved_farm_uuid)
.first() .first()
) )
resolved_plant_name = plant_name resolved_plant_name = plant_name
@@ -246,7 +251,7 @@ def get_fertilization_recommendation(
) )
context = build_rag_context( context = build_rag_context(
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID, user_query, resolved_farm_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
) )
extra_parts: list[str] = [] extra_parts: list[str] = []
@@ -276,7 +281,7 @@ def get_fertilization_recommendation(
{"role": "user", "content": user_query}, {"role": "user", "content": user_query},
] ]
audit_log = _create_audit_log( audit_log = _create_audit_log(
farm_uuid=sensor_uuid, farm_uuid=resolved_farm_uuid,
service_id=SERVICE_ID, service_id=SERVICE_ID,
model=model, model=model,
query=user_query, query=user_query,
@@ -291,7 +296,7 @@ def get_fertilization_recommendation(
) )
raw = response.choices[0].message.content.strip() raw = response.choices[0].message.content.strip()
except Exception as exc: except Exception as exc:
logger.error("Fertilization recommendation error for %s: %s", sensor_uuid, exc) logger.error("Fertilization recommendation error for %s: %s", resolved_farm_uuid, exc)
result = _build_fertilization_fallback(optimized_result=optimized_result) result = _build_fertilization_fallback(optimized_result=optimized_result)
result["error"] = f"خطا در دریافت توصیه: {exc}" result["error"] = f"خطا در دریافت توصیه: {exc}"
result["raw_response"] = None result["raw_response"] = None
+13 -8
View File
@@ -219,20 +219,21 @@ def _persist_irrigation_method_on_farm(
def get_irrigation_recommendation( def get_irrigation_recommendation(
sensor_uuid: str, farm_uuid: str | None = None,
plant_name: str | None = None, plant_name: str | None = None,
growth_stage: str | None = None, growth_stage: str | None = None,
irrigation_method_name: str | None = None, irrigation_method_name: str | None = None,
query: str | None = None, query: str | None = None,
config: RAGConfig | None = None, config: RAGConfig | None = None,
limit: int = 8, limit: int = 8,
sensor_uuid: str | None = None,
) -> dict: ) -> dict:
""" """
توصیه آبیاری برای یک سنسور (کاربر). توصیه آبیاری برای یک مزرعه.
از RAG با پایگاه دانش irrigation استفاده میکند. از RAG با پایگاه دانش irrigation استفاده میکند.
Args: Args:
sensor_uuid: شناسه سنسور کاربر farm_uuid: شناسه مزرعه
plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant) plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant)
growth_stage: مرحله رشد گیاه growth_stage: مرحله رشد گیاه
irrigation_method_name: نام روش آبیاری (برای بارگذاری از جدول IrrigationMethod) irrigation_method_name: نام روش آبیاری (برای بارگذاری از جدول IrrigationMethod)
@@ -241,7 +242,7 @@ def get_irrigation_recommendation(
limit: تعداد چانکهای بازیابیشده limit: تعداد چانکهای بازیابیشده
Returns: Returns:
dict با کلیدهای irrigation_needed, amount_mm, reason, next_check_date, raw_response dict ساختاریافته برای توصیه آبیاری
""" """
cfg = config or load_rag_config() cfg = config or load_rag_config()
service = get_service_config(SERVICE_ID, cfg) service = get_service_config(SERVICE_ID, cfg)
@@ -257,12 +258,16 @@ def get_irrigation_recommendation(
client = get_chat_client(service_cfg) client = get_chat_client(service_cfg)
model = service.llm.model model = service.llm.model
resolved_farm_uuid = str(farm_uuid or sensor_uuid or "").strip()
if not resolved_farm_uuid:
raise ValueError("farm_uuid is required.")
user_query = query or "توصیه آبیاری برای مزرعه من چیست؟" user_query = query or "توصیه آبیاری برای مزرعه من چیست؟"
sensor = ( sensor = (
SensorData.objects.select_related("center_location", "irrigation_method") SensorData.objects.select_related("center_location", "irrigation_method")
.prefetch_related("plants") .prefetch_related("plants")
.filter(farm_uuid=sensor_uuid) .filter(farm_uuid=resolved_farm_uuid)
.first() .first()
) )
irrigation_method = _resolve_irrigation_method(sensor, irrigation_method_name) irrigation_method = _resolve_irrigation_method(sensor, irrigation_method_name)
@@ -309,7 +314,7 @@ def get_irrigation_recommendation(
) )
context = build_rag_context( context = build_rag_context(
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID, user_query, resolved_farm_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
) )
extra_parts: list[str] = [] extra_parts: list[str] = []
@@ -360,7 +365,7 @@ def get_irrigation_recommendation(
{"role": "user", "content": user_query}, {"role": "user", "content": user_query},
] ]
audit_log = _create_audit_log( audit_log = _create_audit_log(
farm_uuid=sensor_uuid, farm_uuid=resolved_farm_uuid,
service_id=SERVICE_ID, service_id=SERVICE_ID,
model=model, model=model,
query=user_query, query=user_query,
@@ -375,7 +380,7 @@ def get_irrigation_recommendation(
) )
raw = response.choices[0].message.content.strip() raw = response.choices[0].message.content.strip()
except Exception as exc: except Exception as exc:
logger.error("Irrigation recommendation error for %s: %s", sensor_uuid, exc) logger.error("Irrigation recommendation error for %s: %s", resolved_farm_uuid, exc)
result = _build_irrigation_fallback( result = _build_irrigation_fallback(
optimized_result=optimized_result, optimized_result=optimized_result,
daily_water_needs=daily_water_needs, daily_water_needs=daily_water_needs,
+4 -4
View File
@@ -20,7 +20,7 @@ def rag_ingest_task(recreate: bool = True):
@app.task(bind=True) @app.task(bind=True)
def irrigation_recommendation_task( def irrigation_recommendation_task(
self, self,
sensor_uuid: str, farm_uuid: str,
plant_name: str | None = None, plant_name: str | None = None,
growth_stage: str | None = None, growth_stage: str | None = None,
irrigation_method_name: str | None = None, irrigation_method_name: str | None = None,
@@ -38,7 +38,7 @@ def irrigation_recommendation_task(
meta={"message": "در حال پردازش توصیه آبیاری..."}, meta={"message": "در حال پردازش توصیه آبیاری..."},
) )
result = get_irrigation_recommendation( result = get_irrigation_recommendation(
sensor_uuid=sensor_uuid, farm_uuid=farm_uuid,
plant_name=plant_name, plant_name=plant_name,
growth_stage=growth_stage, growth_stage=growth_stage,
irrigation_method_name=irrigation_method_name, irrigation_method_name=irrigation_method_name,
@@ -51,7 +51,7 @@ def irrigation_recommendation_task(
@app.task(bind=True) @app.task(bind=True)
def fertilization_recommendation_task( def fertilization_recommendation_task(
self, self,
sensor_uuid: str, farm_uuid: str,
plant_name: str | None = None, plant_name: str | None = None,
growth_stage: str | None = None, growth_stage: str | None = None,
query: str | None = None, query: str | None = None,
@@ -68,7 +68,7 @@ def fertilization_recommendation_task(
meta={"message": "در حال پردازش توصیه کودهی..."}, meta={"message": "در حال پردازش توصیه کودهی..."},
) )
result = get_fertilization_recommendation( result = get_fertilization_recommendation(
sensor_uuid=sensor_uuid, farm_uuid=farm_uuid,
plant_name=plant_name, plant_name=plant_name,
growth_stage=growth_stage, growth_stage=growth_stage,
query=query, query=query,
+6 -5
View File
@@ -1,9 +1,10 @@
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase, override_settings
from rest_framework.test import APIClient from rest_framework.test import APIClient
@override_settings(ROOT_URLCONF="config.test_urls")
class RagRecommendationApiTests(TestCase): class RagRecommendationApiTests(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
@@ -21,7 +22,7 @@ class RagRecommendationApiTests(TestCase):
response = self.client.post( response = self.client.post(
"/api/rag/recommend/irrigation/", "/api/rag/recommend/irrigation/",
data={ data={
"sensor_uuid": "sensor-123", "farm_uuid": "sensor-123",
"plant_name": "گوجه‌فرنگی", "plant_name": "گوجه‌فرنگی",
"growth_stage": "میوه‌دهی", "growth_stage": "میوه‌دهی",
"irrigation_method_name": "قطره‌ای", "irrigation_method_name": "قطره‌ای",
@@ -32,7 +33,7 @@ class RagRecommendationApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["plan"]["frequencyPerWeek"], 3) self.assertEqual(response.json()["data"]["plan"]["frequencyPerWeek"], 3)
mock_get_irrigation_recommendation.assert_called_once_with( mock_get_irrigation_recommendation.assert_called_once_with(
sensor_uuid="sensor-123", farm_uuid="sensor-123",
plant_name="گوجه‌فرنگی", plant_name="گوجه‌فرنگی",
growth_stage="میوه‌دهی", growth_stage="میوه‌دهی",
irrigation_method_name="قطره‌ای", irrigation_method_name="قطره‌ای",
@@ -52,7 +53,7 @@ class RagRecommendationApiTests(TestCase):
response = self.client.post( response = self.client.post(
"/api/rag/recommend/fertilization/", "/api/rag/recommend/fertilization/",
data={ data={
"sensor_uuid": "sensor-456", "farm_uuid": "sensor-456",
"plant_name": "گندم", "plant_name": "گندم",
"growth_stage": "رویشی", "growth_stage": "رویشی",
}, },
@@ -62,7 +63,7 @@ class RagRecommendationApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["plan"]["npkRatio"], "20-20-20") self.assertEqual(response.json()["data"]["plan"]["npkRatio"], "20-20-20")
mock_get_fertilization_recommendation.assert_called_once_with( mock_get_fertilization_recommendation.assert_called_once_with(
sensor_uuid="sensor-456", farm_uuid="sensor-456",
plant_name="گندم", plant_name="گندم",
growth_stage="رویشی", growth_stage="رویشی",
query=None, query=None,
+4 -4
View File
@@ -130,7 +130,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_irrigation_recommendation( result = get_irrigation_recommendation(
sensor_uuid=str(self.farm_uuid), farm_uuid=str(self.farm_uuid),
growth_stage="میوه‌دهی", growth_stage="میوه‌دهی",
) )
@@ -176,7 +176,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_irrigation_recommendation( result = get_irrigation_recommendation(
sensor_uuid=str(self.farm_uuid), farm_uuid=str(self.farm_uuid),
growth_stage="میوه‌دهی", growth_stage="میوه‌دهی",
irrigation_method_name="بارانی", irrigation_method_name="بارانی",
) )
@@ -205,7 +205,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_fertilization_recommendation( result = get_fertilization_recommendation(
sensor_uuid=str(self.farm_uuid), farm_uuid=str(self.farm_uuid),
growth_stage="رویشی", growth_stage="رویشی",
) )
@@ -232,7 +232,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_fertilization_recommendation( result = get_fertilization_recommendation(
sensor_uuid=str(self.farm_uuid), farm_uuid=str(self.farm_uuid),
growth_stage="رویشی", growth_stage="رویشی",
) )
+2 -2
View File
@@ -8,6 +8,6 @@ from .views import (
urlpatterns = [ urlpatterns = [
path("chat/", ChatView.as_view()), path("chat/", ChatView.as_view()),
# path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"), path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
# path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"), path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
] ]
+18 -16
View File
@@ -148,7 +148,7 @@ class ChatView(APIView):
class IrrigationRecommendationView(APIView): class IrrigationRecommendationView(APIView):
""" """
توصیه آبیاری به صورت مستقیم. توصیه آبیاری به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name. POST با farm_uuid، plant_name، growth_stage، irrigation_method_name.
نتیجه همان لحظه برگشت داده میشود. نتیجه همان لحظه برگشت داده میشود.
""" """
@@ -162,7 +162,8 @@ class IrrigationRecommendationView(APIView):
request=inline_serializer( request=inline_serializer(
name="IrrigationRecommendationRequest", name="IrrigationRecommendationRequest",
fields={ fields={
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"), "farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"), "plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"), "growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"irrigation_method_name": drf_serializers.CharField(required=False, help_text="نام روش آبیاری"), "irrigation_method_name": drf_serializers.CharField(required=False, help_text="نام روش آبیاری"),
@@ -187,7 +188,7 @@ class IrrigationRecommendationView(APIView):
OpenApiExample( OpenApiExample(
"نمونه درخواست", "نمونه درخواست",
value={ value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی", "plant_name": "گوجه‌فرنگی",
"growth_stage": "میوه‌دهی", "growth_stage": "میوه‌دهی",
"irrigation_method_name": "آبیاری قطره‌ای", "irrigation_method_name": "آبیاری قطره‌ای",
@@ -199,23 +200,23 @@ class IrrigationRecommendationView(APIView):
def post(self, request: Request): def post(self, request: Request):
from rag.services.irrigation import get_irrigation_recommendation from rag.services.irrigation import get_irrigation_recommendation
sensor_uuid = request.data.get("sensor_uuid") farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not sensor_uuid: if not farm_uuid:
return Response( return Response(
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None}, {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
try: try:
result = get_irrigation_recommendation( result = get_irrigation_recommendation(
sensor_uuid=str(sensor_uuid), farm_uuid=str(farm_uuid),
plant_name=request.data.get("plant_name"), plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"), growth_stage=request.data.get("growth_stage"),
irrigation_method_name=request.data.get("irrigation_method_name"), irrigation_method_name=request.data.get("irrigation_method_name"),
query=request.data.get("query"), query=request.data.get("query"),
) )
except Exception: except Exception:
logger.exception("Direct irrigation recommendation failed for sensor %s", sensor_uuid) logger.exception("Direct irrigation recommendation failed for farm %s", farm_uuid)
return Response( return Response(
{"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None}, {"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR, status=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -230,7 +231,7 @@ class IrrigationRecommendationView(APIView):
class FertilizationRecommendationView(APIView): class FertilizationRecommendationView(APIView):
""" """
توصیه کودهی به صورت مستقیم. توصیه کودهی به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage. POST با farm_uuid، plant_name، growth_stage.
نتیجه همان لحظه برگشت داده میشود. نتیجه همان لحظه برگشت داده میشود.
""" """
@@ -244,7 +245,8 @@ class FertilizationRecommendationView(APIView):
request=inline_serializer( request=inline_serializer(
name="FertilizationRecommendationRequest", name="FertilizationRecommendationRequest",
fields={ fields={
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"), "farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"), "plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"), "growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"), "query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"),
@@ -268,7 +270,7 @@ class FertilizationRecommendationView(APIView):
OpenApiExample( OpenApiExample(
"نمونه درخواست", "نمونه درخواست",
value={ value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی", "plant_name": "گوجه‌فرنگی",
"growth_stage": "رویشی", "growth_stage": "رویشی",
}, },
@@ -279,22 +281,22 @@ class FertilizationRecommendationView(APIView):
def post(self, request: Request): def post(self, request: Request):
from rag.services.fertilization import get_fertilization_recommendation from rag.services.fertilization import get_fertilization_recommendation
sensor_uuid = request.data.get("sensor_uuid") farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not sensor_uuid: if not farm_uuid:
return Response( return Response(
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None}, {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
try: try:
result = get_fertilization_recommendation( result = get_fertilization_recommendation(
sensor_uuid=str(sensor_uuid), farm_uuid=str(farm_uuid),
plant_name=request.data.get("plant_name"), plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"), growth_stage=request.data.get("growth_stage"),
query=request.data.get("query"), query=request.data.get("query"),
) )
except Exception: except Exception:
logger.exception("Direct fertilization recommendation failed for sensor %s", sensor_uuid) logger.exception("Direct fertilization recommendation failed for farm %s", farm_uuid)
return Response( return Response(
{"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None}, {"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR, status=status.HTTP_500_INTERNAL_SERVER_ERROR,
-1
View File
@@ -31,7 +31,6 @@ numpy>=1.23,<1.27
pcse pcse
# === Vector Databases === # === Vector Databases ===
chromadb>=0.4.24,<0.5
qdrant-client>=1.7,<1.9 qdrant-client>=1.7,<1.9
# === Utils === # === Utils ===