AI UPDATE
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_GROWTH_PROFILE = {
|
||||
"base_temperature": 10.0,
|
||||
"required_gdd_for_maturity": 1200.0,
|
||||
"stage_thresholds": {
|
||||
"flowering": 500.0,
|
||||
"fruiting": 850.0,
|
||||
},
|
||||
"current_cumulative_gdd": 0.0,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class HarvestPrediction:
|
||||
current_cumulative_gdd: float
|
||||
required_gdd_for_maturity: float
|
||||
remaining_gdd: float
|
||||
estimated_days_to_harvest: int
|
||||
predicted_harvest_date: str
|
||||
predicted_harvest_window: dict[str, str]
|
||||
daily_gdd_forecast: list[dict[str, float | str]]
|
||||
active_stage: str | None
|
||||
|
||||
|
||||
def resolve_growth_profile(plant: Any | None) -> dict:
|
||||
profile = getattr(plant, "growth_profile", None) or {}
|
||||
stage_thresholds = {
|
||||
**DEFAULT_GROWTH_PROFILE["stage_thresholds"],
|
||||
**profile.get("stage_thresholds", {}),
|
||||
}
|
||||
return {
|
||||
**DEFAULT_GROWTH_PROFILE,
|
||||
**profile,
|
||||
"stage_thresholds": stage_thresholds,
|
||||
}
|
||||
|
||||
|
||||
def calculate_daily_gdd(tmax: float, tmin: float, tbase: float) -> float:
|
||||
mean_temp = (tmax + tmin) / 2.0
|
||||
return round(max(mean_temp - tbase, 0.0), 3)
|
||||
|
||||
|
||||
def determine_active_stage(current_cumulative_gdd: float, stage_thresholds: dict[str, float]) -> str | None:
|
||||
active_stage = None
|
||||
for stage, threshold in sorted(stage_thresholds.items(), key=lambda item: item[1]):
|
||||
if current_cumulative_gdd >= float(threshold):
|
||||
active_stage = stage
|
||||
return active_stage
|
||||
|
||||
|
||||
def predict_harvest_from_forecasts(
|
||||
forecasts: list[Any],
|
||||
plant: Any | None,
|
||||
) -> HarvestPrediction:
|
||||
profile = resolve_growth_profile(plant)
|
||||
base_temperature = float(profile.get("base_temperature", DEFAULT_GROWTH_PROFILE["base_temperature"]))
|
||||
required_gdd = float(profile.get("required_gdd_for_maturity", DEFAULT_GROWTH_PROFILE["required_gdd_for_maturity"]))
|
||||
current_cumulative_gdd = float(profile.get("current_cumulative_gdd", DEFAULT_GROWTH_PROFILE["current_cumulative_gdd"]))
|
||||
|
||||
cumulative_gdd = current_cumulative_gdd
|
||||
daily_forecast: list[dict[str, float | str]] = []
|
||||
estimated_date = forecasts[-1].forecast_date if forecasts else date.today()
|
||||
|
||||
for forecast in forecasts:
|
||||
tmax = float(getattr(forecast, "temperature_max", None) or getattr(forecast, "temperature_mean", 0.0))
|
||||
tmin = float(getattr(forecast, "temperature_min", None) or getattr(forecast, "temperature_mean", 0.0))
|
||||
daily_gdd = calculate_daily_gdd(tmax=tmax, tmin=tmin, tbase=base_temperature)
|
||||
cumulative_gdd += daily_gdd
|
||||
daily_forecast.append(
|
||||
{
|
||||
"date": forecast.forecast_date.isoformat(),
|
||||
"gdd": daily_gdd,
|
||||
"cumulative_gdd": round(cumulative_gdd, 3),
|
||||
}
|
||||
)
|
||||
if cumulative_gdd >= required_gdd:
|
||||
estimated_date = forecast.forecast_date
|
||||
break
|
||||
else:
|
||||
remaining_gdd_after_forecast = max(required_gdd - cumulative_gdd, 0.0)
|
||||
avg_gdd = sum(item["gdd"] for item in daily_forecast) / len(daily_forecast) if daily_forecast else 0.0
|
||||
extra_days = int(remaining_gdd_after_forecast / avg_gdd) + (1 if avg_gdd > 0 and remaining_gdd_after_forecast > 0 else 0)
|
||||
estimated_date = estimated_date + timedelta(days=max(extra_days, 0))
|
||||
|
||||
remaining_gdd = max(required_gdd - current_cumulative_gdd, 0.0)
|
||||
estimated_days = max((estimated_date - date.today()).days, 0)
|
||||
active_stage = determine_active_stage(current_cumulative_gdd, profile.get("stage_thresholds", {}))
|
||||
|
||||
return HarvestPrediction(
|
||||
current_cumulative_gdd=round(current_cumulative_gdd, 3),
|
||||
required_gdd_for_maturity=round(required_gdd, 3),
|
||||
remaining_gdd=round(remaining_gdd, 3),
|
||||
estimated_days_to_harvest=estimated_days,
|
||||
predicted_harvest_date=estimated_date.isoformat(),
|
||||
predicted_harvest_window={
|
||||
"start": (estimated_date - timedelta(days=3)).isoformat(),
|
||||
"end": (estimated_date + timedelta(days=3)).isoformat(),
|
||||
},
|
||||
daily_gdd_forecast=daily_forecast,
|
||||
active_stage=active_stage,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="health_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل سلامت گیاه برای KPIها. ساختار نمونه: "
|
||||
'{"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0002_plant_health_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="irrigation_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل آبیاری گیاه برای محاسبات ETc. "
|
||||
'نمونه: {"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, '
|
||||
'"growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0003_plant_irrigation_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="growth_profile",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text=(
|
||||
"پروفایل رشد گیاه برای مدل GDD. "
|
||||
'نمونه: {"base_temperature": 10, "required_gdd_for_maturity": 1200, '
|
||||
'"stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}'
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -52,6 +52,32 @@ class Plant(models.Model):
|
||||
blank=True,
|
||||
help_text="کود مناسب",
|
||||
)
|
||||
health_profile = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"پروفایل سلامت گیاه برای KPIها. ساختار نمونه: "
|
||||
'{"moisture": {"ideal_value": 65, "min_range": 45, "max_range": 75, "weight": 0.4}}'
|
||||
),
|
||||
)
|
||||
irrigation_profile = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"پروفایل آبیاری گیاه برای محاسبات ETc. "
|
||||
'نمونه: {"kc_initial": 0.6, "kc_mid": 1.15, "kc_end": 0.8, '
|
||||
'"growth_stage_duration": {"initial": 20, "mid": 30, "late": 25}}'
|
||||
),
|
||||
)
|
||||
growth_profile = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"پروفایل رشد گیاه برای مدل GDD. "
|
||||
'نمونه: {"base_temperature": 10, "required_gdd_for_maturity": 1200, '
|
||||
'"stage_thresholds": {"flowering": 500, "fruiting": 850}, "current_cumulative_gdd": 320}'
|
||||
),
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user