AI UPDATE

This commit is contained in:
2026-03-22 03:08:27 +03:30
parent 3ee14ca977
commit d977a583c6
37 changed files with 3525 additions and 263 deletions
+107
View File
@@ -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}'
),
),
),
]
+26
View File
@@ -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)