Files
Ai/plant/gdd.py
T

108 lines
4.1 KiB
Python
Raw Normal View History

2026-03-22 03:08:27 +03:30
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,
)