UPDATE
This commit is contained in:
+13
-10
@@ -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
@@ -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")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Keep build-isolated dependency resolution compatible with Python 3.10.
|
||||||
|
numpy>=1.23,<1.27
|
||||||
@@ -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": [...],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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']}",
|
||||||
)
|
)
|
||||||
|
|||||||
+518
-72
@@ -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,10 +1242,11 @@ class CropSimulationService:
|
|||||||
"runs": run_metrics,
|
"runs": run_metrics,
|
||||||
}
|
}
|
||||||
if scenario.scenario_type == SimulationScenario.ScenarioType.CROP_COMPARISON:
|
if scenario.scenario_type == SimulationScenario.ScenarioType.CROP_COMPARISON:
|
||||||
payload["comparison"]["yield_gap"] = round(
|
if len(run_metrics) >= 2:
|
||||||
abs(run_metrics[0]["yield_estimate"] - run_metrics[1]["yield_estimate"]),
|
payload["comparison"]["yield_gap"] = round(
|
||||||
3,
|
abs(run_metrics[0]["yield_estimate"] - run_metrics[1]["yield_estimate"]),
|
||||||
)
|
3,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
scenario.scenario_type
|
scenario.scenario_type
|
||||||
== SimulationScenario.ScenarioType.FERTILIZATION_COMPARISON
|
== SimulationScenario.ScenarioType.FERTILIZATION_COMPARISON
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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):
|
||||||
"""سریالایزر خروجی برای پلن توصیه کودهی."""
|
"""سریالایزر خروجی برای پلن توصیه کودهی."""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 ===
|
||||||
|
|||||||
Reference in New Issue
Block a user