rendement_agricole / scripts /validate_runtime.py
stephmnt's picture
Sync from GitHub via hub-sync
23b1977 verified
"""Valide localement le runtime final a partir des artefacts deployables."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.deployment_payload import DEPLOYMENT_REQUIRED_ARTIFACTS
from scripts.pipeline_utils import ensure_paths_exist, relative_to_project
from scripts.prediction_adjustment import AdjustedYieldService
RUNTIME_REQUIRED_ARTIFACTS = DEPLOYMENT_REQUIRED_ARTIFACTS
def parse_args() -> argparse.Namespace:
"""Construit l'interface en ligne de commande du validateur runtime."""
parser = argparse.ArgumentParser(
description="Run a local smoke test against the final adjusted-yield service.",
)
parser.add_argument(
"--country",
help="Optional country used for the smoke test. Defaults to the first available area.",
)
parser.add_argument(
"--crop",
help="Optional crop used for the smoke test. Must belong to the selected country.",
)
parser.add_argument(
"--max-candidate-crops",
type=int,
default=3,
help="Maximum number of crops included in the recommendation smoke test.",
)
parser.add_argument(
"--json",
action="store_true",
help="Print the validation summary as JSON.",
)
return parser.parse_args()
def pick_area_and_crop(
service: AdjustedYieldService,
*,
country: str | None = None,
crop: str | None = None,
) -> tuple[str, str]:
"""Selectionne un couple pays/culture valide pour le smoke test.
Args:
service: Service metier deja initialise.
country: Pays optionnel impose.
crop: Culture optionnelle imposee.
Returns:
tuple[str, str]: Couple pays/culture compatible avec le moteur final.
"""
selected_country = country or service.available_areas[0]
if selected_country not in service.crops_by_area:
raise ValueError(f"Unknown country for runtime validation: {selected_country}")
available_crops = service.crops_by_area[selected_country]
selected_crop = crop or available_crops[0]
if selected_crop not in available_crops:
raise ValueError(
f"Crop {selected_crop!r} is not available for country {selected_country!r}."
)
return selected_country, selected_crop
def _pick_distinct_option(options: list[str], current_value: Any) -> Any:
"""Choisit une valeur differente de la reference si possible."""
for option in options:
if option != current_value:
return option
return current_value
def build_smoke_user_conditions(
service: AdjustedYieldService,
*,
reference_profile: dict[str, Any],
) -> dict[str, Any]:
"""Construit des conditions utilisateur legerement differentes de la reference.
Args:
service: Service metier expose par l'application finale.
reference_profile: Profil de reference retourne par le baseline.
Returns:
dict[str, Any]: Conditions candidates pour un smoke test realiste.
"""
return {
"region": _pick_distinct_option(service.simulation_options["regions"], reference_profile["region"]),
"soil_type": _pick_distinct_option(service.simulation_options["soil_types"], reference_profile["soil_type"]),
"rainfall_mm": float(reference_profile["rainfall_mm"]) + 25.0,
"temperature_celsius": float(reference_profile["temperature_celsius"]) + 1.5,
"fertilizer_used": not bool(reference_profile["fertilizer_used"]),
"irrigation_used": not bool(reference_profile["irrigation_used"]),
"weather_condition": _pick_distinct_option(
service.simulation_options["weather_conditions"],
reference_profile["weather_condition"],
),
"days_to_harvest": max(1.0, float(reference_profile["days_to_harvest"]) + 7.0),
}
def validate_runtime(
*,
country: str | None = None,
crop: str | None = None,
max_candidate_crops: int = 3,
) -> dict[str, Any]:
"""Execute un smoke test complet sur la pile metier finale.
Args:
country: Pays optionnel impose.
crop: Culture optionnelle imposee.
max_candidate_crops: Nombre maximum de cultures a comparer.
Returns:
dict[str, Any]: Resume du test et des artefacts verifies.
"""
ensure_paths_exist(RUNTIME_REQUIRED_ARTIFACTS, label="runtime artifacts")
service = AdjustedYieldService()
selected_country, selected_crop = pick_area_and_crop(service, country=country, crop=crop)
baseline = service.get_baseline(selected_country, selected_crop)
smoke_conditions = build_smoke_user_conditions(
service,
reference_profile=baseline["reference_profile"],
)
prediction = service.predict_adjusted_yield(selected_country, selected_crop, smoke_conditions)
candidate_crops = service.crops_by_area[selected_country][: max(1, max_candidate_crops)]
recommendations = service.recommend_crops(
selected_country,
smoke_conditions,
candidate_crops=candidate_crops,
)
if recommendations.empty:
raise RuntimeError("Runtime validation produced no recommendations.")
top_recommendation = recommendations.iloc[0]
return {
"country": selected_country,
"crop": selected_crop,
"target_year": baseline["target_year"],
"candidate_crop_count": int(len(candidate_crops)),
"baseline_prediction": float(baseline["p1_historical_prediction"]),
"final_prediction": float(prediction["final_prediction"]),
"top_recommendation": str(top_recommendation["crop"]),
"top_recommendation_prediction": float(top_recommendation["final_prediction"]),
"validated_artifacts": [relative_to_project(path) for path in RUNTIME_REQUIRED_ARTIFACTS],
}
def main() -> None:
"""Execute le validateur runtime depuis la CLI."""
args = parse_args()
summary = validate_runtime(
country=args.country,
crop=args.crop,
max_candidate_crops=args.max_candidate_crops,
)
if args.json:
print(json.dumps(summary, indent=2, ensure_ascii=True))
return
print(
"[runtime] Validation passed "
f"(country={summary['country']}, crop={summary['crop']}, "
f"final_prediction={summary['final_prediction']:.4f}, "
f"top_recommendation={summary['top_recommendation']})"
)
if __name__ == "__main__":
main()