UPDATE
This commit is contained in:
@@ -32,7 +32,7 @@ def create_or_get_block_subdivision(
|
||||
اگر subdivision این بلوک قبلاً ساخته شده باشد همان را برمیگرداند؛
|
||||
در غیر این صورت الگوریتم grid + KMeans را اجرا و ذخیره میکند.
|
||||
"""
|
||||
from .models import BlockSubdivision
|
||||
from .models import BlockSubdivision, build_default_sub_block, ensure_block_layout_defaults
|
||||
|
||||
existing = BlockSubdivision.objects.filter(
|
||||
soil_location=location,
|
||||
@@ -244,7 +244,7 @@ def render_elbow_plot(
|
||||
|
||||
|
||||
def sync_block_layout_with_subdivision(location, subdivision) -> None:
|
||||
layout = location.block_layout or {}
|
||||
layout = ensure_block_layout_defaults(location.block_layout, block_count=location.input_block_count)
|
||||
blocks = list(layout.get("blocks") or [])
|
||||
target_block = None
|
||||
for block in blocks:
|
||||
@@ -263,7 +263,12 @@ def sync_block_layout_with_subdivision(location, subdivision) -> None:
|
||||
blocks.append(target_block)
|
||||
|
||||
target_block["needs_subdivision"] = subdivision.centroid_count > 1
|
||||
target_block["sub_blocks"] = list(subdivision.centroid_points or [])
|
||||
target_block["sub_blocks"] = list(subdivision.centroid_points or []) or [
|
||||
build_default_sub_block(
|
||||
str(target_block.get("block_code") or "block-1"),
|
||||
boundary=target_block.get("boundary") or {},
|
||||
)
|
||||
]
|
||||
target_block["subdivision_summary"] = {
|
||||
"chunk_size_sqm": subdivision.chunk_size_sqm,
|
||||
"grid_point_count": subdivision.grid_point_count,
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Avg
|
||||
|
||||
from crop_simulation.growth_simulation import GrowthSimulationContext, _run_projection_engine
|
||||
from crop_simulation.services import PcseSimulationManager, build_simulation_payload_from_farm
|
||||
from farm_data.services import get_canonical_farm_record, get_farm_plant_assignments
|
||||
from .models import AnalysisGridObservation, RemoteSensingClusterBlock
|
||||
from .satellite_snapshot import build_location_block_satellite_snapshots
|
||||
|
||||
|
||||
class ClusterRecommendationNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClusterRecommendationValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClusterPlantCandidate:
|
||||
plant_id: int | None
|
||||
plant_name: str
|
||||
position: int | None
|
||||
stage: str
|
||||
score: float
|
||||
predicted_yield: float | None
|
||||
predicted_yield_tons: float | None
|
||||
biomass: float | None
|
||||
max_lai: float | None
|
||||
simulation_engine: str | None
|
||||
simulation_model_name: str | None
|
||||
simulation_warning: str | None
|
||||
supporting_metrics: dict[str, Any]
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"plant_id": self.plant_id,
|
||||
"plant_name": self.plant_name,
|
||||
"position": self.position,
|
||||
"stage": self.stage,
|
||||
"score": self.score,
|
||||
"predicted_yield": self.predicted_yield,
|
||||
"predicted_yield_tons": self.predicted_yield_tons,
|
||||
"biomass": self.biomass,
|
||||
"max_lai": self.max_lai,
|
||||
"simulation_engine": self.simulation_engine,
|
||||
"simulation_model_name": self.simulation_model_name,
|
||||
"simulation_warning": self.simulation_warning,
|
||||
"supporting_metrics": self.supporting_metrics,
|
||||
}
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> float | None:
|
||||
try:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _clamp(value: float, minimum: float, maximum: float) -> float:
|
||||
if minimum > maximum:
|
||||
minimum, maximum = maximum, minimum
|
||||
return max(minimum, min(value, maximum))
|
||||
|
||||
|
||||
def _build_cluster_entries(
|
||||
snapshots: list[dict[str, Any]],
|
||||
*,
|
||||
cluster_blocks_by_uuid: dict[str, RemoteSensingClusterBlock],
|
||||
) -> list[dict[str, Any]]:
|
||||
entries_by_key: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for snapshot in snapshots:
|
||||
block_code = str(snapshot.get("block_code") or "").strip()
|
||||
temporal_extent = snapshot.get("temporal_extent")
|
||||
for satellite_sub_block in snapshot.get("satellite_sub_blocks") or []:
|
||||
cluster_uuid = str(satellite_sub_block.get("cluster_uuid") or "").strip()
|
||||
sub_block_code = str(satellite_sub_block.get("sub_block_code") or "").strip()
|
||||
cluster_label = satellite_sub_block.get("cluster_label")
|
||||
if cluster_uuid:
|
||||
entry_key = cluster_uuid
|
||||
elif sub_block_code:
|
||||
entry_key = f"{block_code}::{sub_block_code}"
|
||||
else:
|
||||
entry_key = f"{block_code}::cluster-{cluster_label}"
|
||||
entry = entries_by_key.setdefault(
|
||||
entry_key,
|
||||
{
|
||||
"block_code": block_code,
|
||||
"cluster_uuid": cluster_uuid or None,
|
||||
"sub_block_code": sub_block_code,
|
||||
"cluster_label": cluster_label,
|
||||
"temporal_extent": temporal_extent,
|
||||
"cluster_block": None,
|
||||
"satellite_metrics": {},
|
||||
"sensor_metrics": {},
|
||||
"resolved_metrics": {},
|
||||
"source_metadata": {
|
||||
"block_status": snapshot.get("status") or "missing",
|
||||
"aggregation_strategy": snapshot.get("aggregation_strategy") or "missing",
|
||||
"has_satellite_metrics": False,
|
||||
"has_sensor_metrics": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
entry["satellite_metrics"] = dict(satellite_sub_block.get("resolved_metrics") or {})
|
||||
entry["resolved_metrics"].update(entry["satellite_metrics"])
|
||||
entry["source_metadata"]["has_satellite_metrics"] = True
|
||||
if cluster_uuid and cluster_uuid in cluster_blocks_by_uuid:
|
||||
entry["cluster_block"] = cluster_blocks_by_uuid[cluster_uuid]
|
||||
|
||||
for sensor_sub_block in snapshot.get("sensor_sub_blocks") or []:
|
||||
cluster_uuid = str(sensor_sub_block.get("cluster_uuid") or "").strip()
|
||||
sub_block_code = str(sensor_sub_block.get("sub_block_code") or "").strip()
|
||||
cluster_label = sensor_sub_block.get("cluster_label")
|
||||
candidate_keys = [
|
||||
cluster_uuid,
|
||||
f"{block_code}::{sub_block_code}" if sub_block_code else "",
|
||||
f"{block_code}::cluster-{cluster_label}" if cluster_label is not None else "",
|
||||
]
|
||||
entry = None
|
||||
for candidate_key in candidate_keys:
|
||||
if candidate_key and candidate_key in entries_by_key:
|
||||
entry = entries_by_key[candidate_key]
|
||||
break
|
||||
if entry is None:
|
||||
continue
|
||||
entry["sensor_metrics"] = dict(sensor_sub_block.get("resolved_metrics") or {})
|
||||
entry["resolved_metrics"].update(entry["sensor_metrics"])
|
||||
entry["source_metadata"]["has_sensor_metrics"] = True
|
||||
|
||||
return list(entries_by_key.values())
|
||||
|
||||
|
||||
def _attach_missing_satellite_metrics(cluster_entries: list[dict[str, Any]]) -> None:
|
||||
for cluster_entry in cluster_entries:
|
||||
cluster_block = cluster_entry.get("cluster_block")
|
||||
if cluster_block is None:
|
||||
continue
|
||||
needs_soil_vv = "soil_vv" not in (cluster_entry.get("resolved_metrics") or {})
|
||||
if not needs_soil_vv:
|
||||
continue
|
||||
observation_summary = AnalysisGridObservation.objects.filter(
|
||||
cell__cell_code__in=list(cluster_block.cell_codes or []),
|
||||
temporal_start=cluster_block.result.temporal_start,
|
||||
temporal_end=cluster_block.result.temporal_end,
|
||||
).aggregate(soil_vv_mean=Avg("soil_vv"))
|
||||
soil_vv_mean = _safe_float(observation_summary.get("soil_vv_mean"))
|
||||
if soil_vv_mean is None:
|
||||
continue
|
||||
rounded_soil_vv = round(soil_vv_mean, 6)
|
||||
cluster_entry.setdefault("satellite_metrics", {})["soil_vv"] = rounded_soil_vv
|
||||
cluster_entry.setdefault("resolved_metrics", {})["soil_vv"] = rounded_soil_vv
|
||||
|
||||
|
||||
def _build_cluster_overrides(
|
||||
base_payload: dict[str, Any],
|
||||
*,
|
||||
cluster_metrics: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
soil_parameters = deepcopy(base_payload.get("soil") or {})
|
||||
site_parameters = deepcopy(base_payload.get("site_parameters") or {})
|
||||
|
||||
ndwi = _safe_float(cluster_metrics.get("ndwi"))
|
||||
if ndwi is not None:
|
||||
smfcf = _clamp(ndwi, 0.2, 0.55)
|
||||
smw = _clamp(smfcf * 0.45, 0.05, max(smfcf - 0.02, 0.06))
|
||||
sm0 = _clamp(
|
||||
min(max(smfcf + 0.08, smw + 0.12), 0.6),
|
||||
max(smfcf + 0.02, smw + 0.05),
|
||||
0.8,
|
||||
)
|
||||
soil_parameters["SMFCF"] = round(smfcf, 3)
|
||||
soil_parameters["SMW"] = round(smw, 3)
|
||||
soil_parameters["SM0"] = round(sm0, 3)
|
||||
site_parameters["SMLIM"] = round(_clamp(smfcf, smw, sm0), 3)
|
||||
|
||||
soil_moisture = _safe_float(cluster_metrics.get("soil_moisture"))
|
||||
if soil_moisture is not None:
|
||||
soil_parameters["soil_moisture"] = soil_moisture
|
||||
site_parameters["WAV"] = round(max(soil_moisture, 0.0), 3)
|
||||
|
||||
nutrient_mappings = (
|
||||
("nitrogen", "NAVAILI", "nitrogen"),
|
||||
("phosphorus", "P_STATUS", "phosphorus"),
|
||||
("potassium", "K_STATUS", "potassium"),
|
||||
("soil_ph", "SOIL_PH", "soil_ph"),
|
||||
("electrical_conductivity", "EC", "electrical_conductivity"),
|
||||
)
|
||||
for metric_name, site_key, soil_key in nutrient_mappings:
|
||||
value = _safe_float(cluster_metrics.get(metric_name))
|
||||
if value is None:
|
||||
continue
|
||||
soil_parameters[soil_key] = value
|
||||
site_parameters[site_key] = value
|
||||
|
||||
return soil_parameters, site_parameters
|
||||
|
||||
|
||||
def _serialize_cluster_block(cluster_block: RemoteSensingClusterBlock | None) -> dict[str, Any] | None:
|
||||
if cluster_block is None:
|
||||
return None
|
||||
return {
|
||||
"uuid": str(cluster_block.uuid),
|
||||
"sub_block_code": cluster_block.sub_block_code,
|
||||
"cluster_label": cluster_block.cluster_label,
|
||||
"chunk_size_sqm": cluster_block.chunk_size_sqm,
|
||||
"centroid_lat": cluster_block.centroid_lat,
|
||||
"centroid_lon": cluster_block.centroid_lon,
|
||||
"center_cell_code": cluster_block.center_cell_code,
|
||||
"center_cell_lat": cluster_block.center_cell_lat,
|
||||
"center_cell_lon": cluster_block.center_cell_lon,
|
||||
"cell_count": cluster_block.cell_count,
|
||||
"cell_codes": list(cluster_block.cell_codes or []),
|
||||
"geometry": cluster_block.geometry,
|
||||
"metadata": dict(cluster_block.metadata or {}),
|
||||
"created_at": cluster_block.created_at,
|
||||
"updated_at": cluster_block.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _simulate_candidate(
|
||||
*,
|
||||
base_payload: dict[str, Any],
|
||||
soil_parameters: dict[str, Any],
|
||||
site_parameters: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], str | None]:
|
||||
manager = PcseSimulationManager()
|
||||
try:
|
||||
return (
|
||||
manager.run_simulation(
|
||||
weather=base_payload.get("weather") or [],
|
||||
soil=soil_parameters,
|
||||
crop_parameters=base_payload.get("crop_parameters") or {},
|
||||
agromanagement=base_payload.get("agromanagement") or [],
|
||||
site_parameters=site_parameters,
|
||||
),
|
||||
None,
|
||||
)
|
||||
except Exception as exc:
|
||||
context = GrowthSimulationContext(
|
||||
farm_uuid=None,
|
||||
plant_name=str((base_payload.get("crop_parameters") or {}).get("crop_name") or ""),
|
||||
plant=base_payload.get("plant"),
|
||||
dynamic_parameters=[],
|
||||
weather=base_payload.get("weather") or [],
|
||||
crop_parameters=base_payload.get("crop_parameters") or {},
|
||||
soil_parameters=soil_parameters,
|
||||
site_parameters=site_parameters,
|
||||
agromanagement=base_payload.get("agromanagement") or [],
|
||||
page_size=10,
|
||||
)
|
||||
fallback_result = _run_projection_engine(context)
|
||||
return fallback_result, f"simulation_fallback:{exc}"
|
||||
|
||||
|
||||
def _rank_cluster_plants(
|
||||
cluster_entry: dict[str, Any],
|
||||
*,
|
||||
plant_assignments: list[Any],
|
||||
base_payloads: dict[str, dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
candidates: list[ClusterPlantCandidate] = []
|
||||
for assignment in plant_assignments:
|
||||
plant_name = str(getattr(assignment.plant, "name", "") or "").strip()
|
||||
if not plant_name:
|
||||
continue
|
||||
base_payload = base_payloads[plant_name]
|
||||
soil_parameters, site_parameters = _build_cluster_overrides(
|
||||
base_payload,
|
||||
cluster_metrics=dict(cluster_entry.get("resolved_metrics") or {}),
|
||||
)
|
||||
simulation_result, simulation_warning = _simulate_candidate(
|
||||
base_payload=base_payload,
|
||||
soil_parameters=soil_parameters,
|
||||
site_parameters=site_parameters,
|
||||
)
|
||||
metrics = dict(simulation_result.get("metrics") or {})
|
||||
predicted_yield = _safe_float(metrics.get("yield_estimate"))
|
||||
biomass = _safe_float(metrics.get("biomass"))
|
||||
max_lai = _safe_float(metrics.get("max_lai"))
|
||||
predicted_yield_tons = None if predicted_yield is None else round(max(predicted_yield, 0.0) / 1000.0, 4)
|
||||
score = round(predicted_yield if predicted_yield is not None else -1.0, 4)
|
||||
candidates.append(
|
||||
ClusterPlantCandidate(
|
||||
plant_id=getattr(assignment.plant, "backend_plant_id", None),
|
||||
plant_name=plant_name,
|
||||
position=getattr(assignment, "position", None),
|
||||
stage=str(getattr(assignment, "stage", "") or ""),
|
||||
score=score,
|
||||
predicted_yield=round(predicted_yield, 4) if predicted_yield is not None else None,
|
||||
predicted_yield_tons=predicted_yield_tons,
|
||||
biomass=round(biomass, 4) if biomass is not None else None,
|
||||
max_lai=round(max_lai, 4) if max_lai is not None else None,
|
||||
simulation_engine=simulation_result.get("engine"),
|
||||
simulation_model_name=simulation_result.get("model_name"),
|
||||
simulation_warning=simulation_warning,
|
||||
supporting_metrics=metrics,
|
||||
)
|
||||
)
|
||||
|
||||
ranked_candidates = sorted(
|
||||
candidates,
|
||||
key=lambda item: (
|
||||
item.score,
|
||||
item.biomass if item.biomass is not None else float("-inf"),
|
||||
-1 * (item.position if item.position is not None else 10_000),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
return [candidate.as_dict() for candidate in ranked_candidates]
|
||||
|
||||
|
||||
def build_cluster_crop_recommendations(farm_uuid: str) -> dict[str, Any]:
|
||||
farm = get_canonical_farm_record(farm_uuid)
|
||||
if farm is None:
|
||||
raise ClusterRecommendationNotFound("مزرعه پیدا نشد.")
|
||||
|
||||
plant_assignments = get_farm_plant_assignments(farm)
|
||||
if not plant_assignments:
|
||||
raise ClusterRecommendationValidationError("برای این مزرعه هنوز هیچ گیاهی در farm_data ثبت نشده است.")
|
||||
|
||||
location = farm.center_location
|
||||
snapshots = build_location_block_satellite_snapshots(
|
||||
location,
|
||||
sensor_payload=farm.sensor_payload,
|
||||
)
|
||||
cluster_uuids = {
|
||||
str(sub_block.get("cluster_uuid") or "").strip()
|
||||
for snapshot in snapshots
|
||||
for sub_block in (snapshot.get("satellite_sub_blocks") or [])
|
||||
if str(sub_block.get("cluster_uuid") or "").strip()
|
||||
}
|
||||
if not cluster_uuids:
|
||||
raise ClusterRecommendationNotFound("برای این مزرعه هنوز خروجی KMeans در location_data ثبت نشده است.")
|
||||
|
||||
cluster_blocks_by_uuid = {
|
||||
str(cluster_block.uuid): cluster_block
|
||||
for cluster_block in RemoteSensingClusterBlock.objects.filter(uuid__in=list(cluster_uuids)).select_related("result")
|
||||
}
|
||||
cluster_entries = _build_cluster_entries(
|
||||
snapshots,
|
||||
cluster_blocks_by_uuid=cluster_blocks_by_uuid,
|
||||
)
|
||||
_attach_missing_satellite_metrics(cluster_entries)
|
||||
if not cluster_entries:
|
||||
raise ClusterRecommendationNotFound("برای این مزرعه هنوز کلاستر قابل استفاده پیدا نشد.")
|
||||
|
||||
base_payloads: dict[str, dict[str, Any]] = {}
|
||||
for assignment in plant_assignments:
|
||||
plant_name = str(getattr(assignment.plant, "name", "") or "").strip()
|
||||
if not plant_name or plant_name in base_payloads:
|
||||
continue
|
||||
try:
|
||||
base_payloads[plant_name] = build_simulation_payload_from_farm(
|
||||
farm_uuid=str(farm.farm_uuid),
|
||||
plant_name=plant_name,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ClusterRecommendationValidationError(
|
||||
f"مقایسه گیاهها با crop_simulation انجام نشد: {exc}"
|
||||
) from exc
|
||||
|
||||
response_clusters: list[dict[str, Any]] = []
|
||||
for cluster_entry in cluster_entries:
|
||||
candidate_plants = _rank_cluster_plants(
|
||||
cluster_entry,
|
||||
plant_assignments=plant_assignments,
|
||||
base_payloads=base_payloads,
|
||||
)
|
||||
response_clusters.append(
|
||||
{
|
||||
"block_code": cluster_entry.get("block_code") or "",
|
||||
"cluster_uuid": cluster_entry.get("cluster_uuid"),
|
||||
"sub_block_code": cluster_entry.get("sub_block_code") or "",
|
||||
"cluster_label": cluster_entry.get("cluster_label"),
|
||||
"temporal_extent": cluster_entry.get("temporal_extent"),
|
||||
"cluster_block": _serialize_cluster_block(cluster_entry.get("cluster_block")),
|
||||
"satellite_metrics": dict(cluster_entry.get("satellite_metrics") or {}),
|
||||
"sensor_metrics": dict(cluster_entry.get("sensor_metrics") or {}),
|
||||
"resolved_metrics": dict(cluster_entry.get("resolved_metrics") or {}),
|
||||
"candidate_plants": candidate_plants,
|
||||
"suggested_plant": candidate_plants[0] if candidate_plants else None,
|
||||
"source_metadata": dict(cluster_entry.get("source_metadata") or {}),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"farm_uuid": str(farm.farm_uuid),
|
||||
"location_id": location.id,
|
||||
"evaluated_plant_count": len(base_payloads),
|
||||
"cluster_count": len(response_clusters),
|
||||
"registered_plants": [
|
||||
{
|
||||
"plant_id": assignment.plant.backend_plant_id,
|
||||
"plant_name": assignment.plant.name,
|
||||
"position": assignment.position,
|
||||
"stage": assignment.stage,
|
||||
}
|
||||
for assignment in plant_assignments
|
||||
],
|
||||
"clusters": response_clusters,
|
||||
"source_metadata": {
|
||||
"source": "location_data+kmeans+farm_data+crop_simulation",
|
||||
"location_id": location.id,
|
||||
"snapshot_block_count": len(snapshots),
|
||||
},
|
||||
}
|
||||
@@ -16,6 +16,8 @@ from django.db import transaction
|
||||
|
||||
from .block_subdivision import detect_elbow_point, point_in_polygon, render_elbow_plot
|
||||
from .models import (
|
||||
build_default_sub_block,
|
||||
ensure_block_layout_defaults,
|
||||
AnalysisGridObservation,
|
||||
BlockSubdivision,
|
||||
RemoteSensingClusterBlock,
|
||||
@@ -1272,7 +1274,7 @@ def sync_location_block_layout_with_result(
|
||||
result: RemoteSensingSubdivisionResult,
|
||||
cluster_summaries: list[dict[str, Any]],
|
||||
) -> None:
|
||||
layout = dict(location.block_layout or {})
|
||||
layout = ensure_block_layout_defaults(location.block_layout, block_count=location.input_block_count)
|
||||
blocks = list(layout.get("blocks") or [])
|
||||
target_block = None
|
||||
for block in blocks:
|
||||
@@ -1307,6 +1309,14 @@ def sync_location_block_layout_with_result(
|
||||
}
|
||||
for cluster in cluster_summaries
|
||||
]
|
||||
if not target_block["sub_blocks"]:
|
||||
target_block["sub_blocks"] = [
|
||||
build_default_sub_block(
|
||||
str(target_block.get("block_code") or "block-1"),
|
||||
boundary=target_block.get("boundary") or {},
|
||||
)
|
||||
]
|
||||
|
||||
target_block["subdivision_summary"] = {
|
||||
"type": "data_driven_remote_sensing",
|
||||
"cluster_count": result.cluster_count,
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
from location_data.models import (
|
||||
AnalysisGridCell,
|
||||
AnalysisGridObservation,
|
||||
BlockSubdivision,
|
||||
NdviObservation,
|
||||
RemoteSensingClusterAssignment,
|
||||
RemoteSensingClusterBlock,
|
||||
RemoteSensingRun,
|
||||
RemoteSensingSubdivisionOption,
|
||||
RemoteSensingSubdivisionOptionAssignment,
|
||||
RemoteSensingSubdivisionOptionBlock,
|
||||
RemoteSensingSubdivisionResult,
|
||||
SoilLocation,
|
||||
)
|
||||
|
||||
SEED_DATA = {
|
||||
"soillocations": [
|
||||
{
|
||||
"id": 1,
|
||||
"latitude": "50.000000",
|
||||
"longitude": "50.000000",
|
||||
"task_id": "",
|
||||
"farm_boundary": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[49.9995, 49.9995], [50.0005, 49.9995], [50.0005, 50.0005], [49.9995, 50.0005], [49.9995, 49.9995]]],
|
||||
},
|
||||
"input_block_count": 1,
|
||||
"block_layout": {
|
||||
"blocks": [
|
||||
{
|
||||
"order": 1,
|
||||
"source": "default",
|
||||
"boundary": {},
|
||||
"block_code": "block-1",
|
||||
"sub_blocks": [],
|
||||
"needs_subdivision": None,
|
||||
},
|
||||
{
|
||||
"order": 2,
|
||||
"source": "remote_sensing",
|
||||
"block_code": "",
|
||||
"sub_blocks": [
|
||||
{
|
||||
"geometry": {"type": "Polygon", "coordinates": [[[49.9995, 49.9995], [49.99992, 49.9995], [50.000339, 49.9995], [50.000339, 49.99977], [50.000339, 50.00004], [49.99992, 50.00004], [49.9995, 50.00004], [49.9995, 49.99977], [49.9995, 49.9995]]]},
|
||||
"metadata": {"source": "analysis_grid_cells", "center_selection": {"strategy": "coordinate_1_center", "center_radius": 0.0004993, "center_cell_code": "loc-1__block-farm__chunk-900__r0000c0000", "center_mean_distance": 0.00029732}, "cell_geometry_type": "Polygon"},
|
||||
"cell_count": 4,
|
||||
"centroid_lat": 49.99977,
|
||||
"centroid_lon": 49.99992,
|
||||
"cluster_uuid": "daa278cb-cf75-4f17-bc94-bb3a780dd4d4",
|
||||
"cluster_label": 0,
|
||||
"sub_block_code": "cluster-0",
|
||||
"center_cell_lat": 49.999635,
|
||||
"center_cell_lon": 49.99971,
|
||||
"center_cell_code": "loc-1__block-farm__chunk-900__r0000c0000",
|
||||
},
|
||||
{
|
||||
"geometry": {"type": "Polygon", "coordinates": [[[50.000339, 49.9995], [50.000759, 49.9995], [50.000759, 49.99977], [50.000759, 50.00004], [50.000759, 50.000309], [50.000759, 50.000579], [50.000339, 50.000579], [49.99992, 50.000579], [49.9995, 50.000579], [49.9995, 50.000309], [49.9995, 50.00004], [49.99992, 50.00004], [50.000339, 50.00004], [50.000339, 49.99977], [50.000339, 49.9995]]]},
|
||||
"metadata": {"source": "analysis_grid_cells", "center_selection": {"strategy": "coordinate_1_center", "center_radius": 0.0006827, "center_cell_code": "loc-1__block-farm__chunk-900__r0002c0001", "center_mean_distance": 0.00041092}, "cell_geometry_type": "Polygon"},
|
||||
"cell_count": 8,
|
||||
"centroid_lat": 50.000174,
|
||||
"centroid_lon": 50.000235,
|
||||
"cluster_uuid": "e9beea1c-8736-4c45-ac5b-f186705bad76",
|
||||
"cluster_label": 1,
|
||||
"sub_block_code": "cluster-1",
|
||||
"center_cell_lat": 50.000174,
|
||||
"center_cell_lon": 50.00013,
|
||||
"center_cell_code": "loc-1__block-farm__chunk-900__r0002c0001",
|
||||
},
|
||||
],
|
||||
"needs_subdivision": True,
|
||||
"subdivision_summary": {
|
||||
"type": "data_driven_remote_sensing",
|
||||
"run_id": 1,
|
||||
"cluster_count": 2,
|
||||
"used_cell_count": 12,
|
||||
"selected_features": ["ndvi", "ndwi", "soil_vv_db"],
|
||||
"skipped_cell_count": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"algorithm_status": "completed",
|
||||
"default_full_farm": True,
|
||||
"input_block_count": 1,
|
||||
"analysis_grid_summary": {"cell_count": 12, "chunk_size_sqm": 900},
|
||||
},
|
||||
"created_at": "2026-05-11T14:41:43.319380+00:00",
|
||||
"updated_at": "2026-05-12T12:37:38.239904+00:00",
|
||||
}
|
||||
],
|
||||
"blocksubdivisions": [],
|
||||
"remotesensingruns": [
|
||||
{
|
||||
"id": 1,
|
||||
"soil_location_id": 1,
|
||||
"block_subdivision_id": None,
|
||||
"block_code": "",
|
||||
"provider": "openeo",
|
||||
"chunk_size_sqm": 900,
|
||||
"temporal_start": "2026-04-11",
|
||||
"temporal_end": "2026-05-11",
|
||||
"status": "success",
|
||||
"metadata": {"farm_uuid": "11111111-1111-1111-1111-111111111111", "requested_via": "api", "scope": "all_blocks"},
|
||||
"error_message": "",
|
||||
"started_at": "2026-05-12T12:19:03.911826+00:00",
|
||||
"finished_at": "2026-05-12T12:37:39.018428+00:00",
|
||||
"created_at": "2026-05-12T12:19:03.912346+00:00",
|
||||
"updated_at": "2026-05-12T12:37:39.019007+00:00",
|
||||
}
|
||||
],
|
||||
"remotesensingsubdivisionresults": [
|
||||
{
|
||||
"id": 1,
|
||||
"soil_location_id": 1,
|
||||
"run_id": 1,
|
||||
"block_subdivision_id": None,
|
||||
"block_code": "",
|
||||
"chunk_size_sqm": 900,
|
||||
"temporal_start": "2026-04-11",
|
||||
"temporal_end": "2026-05-11",
|
||||
"cluster_count": 2,
|
||||
"selected_features": ["ndvi", "ndwi", "soil_vv_db"],
|
||||
"skipped_cell_codes": [],
|
||||
"metadata": {"selection_strategy": "elbow", "used_cell_count": 12},
|
||||
"created_at": "2026-05-12T12:37:27.897155+00:00",
|
||||
"updated_at": "2026-05-12T12:37:27.897180+00:00",
|
||||
}
|
||||
],
|
||||
"remotesensingclusterblocks": [
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "daa278cb-cf75-4f17-bc94-bb3a780dd4d4",
|
||||
"result_id": 1,
|
||||
"soil_location_id": 1,
|
||||
"block_subdivision_id": None,
|
||||
"block_code": "",
|
||||
"sub_block_code": "cluster-0",
|
||||
"cluster_label": 0,
|
||||
"chunk_size_sqm": 900,
|
||||
"centroid_lat": "49.999770",
|
||||
"centroid_lon": "49.999920",
|
||||
"center_cell_code": "loc-1__block-farm__chunk-900__r0000c0000",
|
||||
"center_cell_lat": "49.999635",
|
||||
"center_cell_lon": "49.999710",
|
||||
"geometry": {"type": "Polygon", "coordinates": [[[49.9995, 49.9995], [49.99992, 49.9995], [50.000339, 49.9995], [50.000339, 49.99977], [50.000339, 50.00004], [49.99992, 50.00004], [49.9995, 50.00004], [49.9995, 49.99977], [49.9995, 49.9995]]]},
|
||||
"cell_count": 4,
|
||||
"cell_codes": ["loc-1__block-farm__chunk-900__r0000c0000", "loc-1__block-farm__chunk-900__r0000c0001", "loc-1__block-farm__chunk-900__r0001c0000", "loc-1__block-farm__chunk-900__r0001c0001"],
|
||||
"metadata": {"source": "analysis_grid_cells"},
|
||||
"created_at": "2026-05-12T12:37:27.899874+00:00",
|
||||
"updated_at": "2026-05-12T12:37:27.899899+00:00",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"uuid": "e9beea1c-8736-4c45-ac5b-f186705bad76",
|
||||
"result_id": 1,
|
||||
"soil_location_id": 1,
|
||||
"block_subdivision_id": None,
|
||||
"block_code": "",
|
||||
"sub_block_code": "cluster-1",
|
||||
"cluster_label": 1,
|
||||
"chunk_size_sqm": 900,
|
||||
"centroid_lat": "50.000174",
|
||||
"centroid_lon": "50.000235",
|
||||
"center_cell_code": "loc-1__block-farm__chunk-900__r0002c0001",
|
||||
"center_cell_lat": "50.000174",
|
||||
"center_cell_lon": "50.000130",
|
||||
"geometry": {"type": "Polygon", "coordinates": [[[50.000339, 49.9995], [50.000759, 49.9995], [50.000759, 49.99977], [50.000759, 50.00004], [50.000759, 50.000309], [50.000759, 50.000579], [50.000339, 50.000579], [49.99992, 50.000579], [49.9995, 50.000579], [49.9995, 50.000309], [49.9995, 50.00004], [49.99992, 50.00004], [50.000339, 50.00004], [50.000339, 49.99977], [50.000339, 49.9995]]]},
|
||||
"cell_count": 8,
|
||||
"cell_codes": ["loc-1__block-farm__chunk-900__r0000c0002", "loc-1__block-farm__chunk-900__r0001c0002", "loc-1__block-farm__chunk-900__r0002c0000", "loc-1__block-farm__chunk-900__r0002c0001", "loc-1__block-farm__chunk-900__r0002c0002", "loc-1__block-farm__chunk-900__r0003c0000", "loc-1__block-farm__chunk-900__r0003c0001", "loc-1__block-farm__chunk-900__r0003c0002"],
|
||||
"metadata": {"source": "analysis_grid_cells"},
|
||||
"created_at": "2026-05-12T12:37:27.901926+00:00",
|
||||
"updated_at": "2026-05-12T12:37:27.901945+00:00",
|
||||
},
|
||||
],
|
||||
"analysisgridcells": [
|
||||
{"id": 1, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0000c0000", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.9995, 49.9995], [49.99992, 49.9995], [49.99992, 49.99977], [49.9995, 49.99977], [49.9995, 49.9995]]]}, "centroid_lat": "49.999635", "centroid_lon": "49.999710", "created_at": "2026-05-12T12:19:03.930590+00:00", "updated_at": "2026-05-12T12:19:03.930609+00:00"},
|
||||
{"id": 2, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0000c0001", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.99992, 49.9995], [50.000339, 49.9995], [50.000339, 49.99977], [49.99992, 49.99977], [49.99992, 49.9995]]]}, "centroid_lat": "49.999635", "centroid_lon": "50.000130", "created_at": "2026-05-12T12:19:03.931797+00:00", "updated_at": "2026-05-12T12:19:03.931817+00:00"},
|
||||
{"id": 3, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0000c0002", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[50.000339, 49.9995], [50.000759, 49.9995], [50.000759, 49.99977], [50.000339, 49.99977], [50.000339, 49.9995]]]}, "centroid_lat": "49.999635", "centroid_lon": "50.000549", "created_at": "2026-05-12T12:19:03.931857+00:00", "updated_at": "2026-05-12T12:19:03.931864+00:00"},
|
||||
{"id": 4, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0001c0000", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.9995, 49.99977], [49.99992, 49.99977], [49.99992, 50.00004], [49.9995, 50.00004], [49.9995, 49.99977]]]}, "centroid_lat": "49.999905", "centroid_lon": "49.999710", "created_at": "2026-05-12T12:19:03.931899+00:00", "updated_at": "2026-05-12T12:19:03.931906+00:00"},
|
||||
{"id": 5, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0001c0001", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.99992, 49.99977], [50.000339, 49.99977], [50.000339, 50.00004], [49.99992, 50.00004], [49.99992, 49.99977]]]}, "centroid_lat": "49.999905", "centroid_lon": "50.000130", "created_at": "2026-05-12T12:19:03.931939+00:00", "updated_at": "2026-05-12T12:19:03.931945+00:00"},
|
||||
{"id": 6, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0001c0002", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[50.000339, 49.99977], [50.000759, 49.99977], [50.000759, 50.00004], [50.000339, 50.00004], [50.000339, 49.99977]]]}, "centroid_lat": "49.999905", "centroid_lon": "50.000549", "created_at": "2026-05-12T12:19:03.931978+00:00", "updated_at": "2026-05-12T12:19:03.931985+00:00"},
|
||||
{"id": 7, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0002c0000", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.9995, 50.00004], [49.99992, 50.00004], [49.99992, 50.000309], [49.9995, 50.000309], [49.9995, 50.00004]]]}, "centroid_lat": "50.000174", "centroid_lon": "49.999710", "created_at": "2026-05-12T12:19:03.932017+00:00", "updated_at": "2026-05-12T12:19:03.932024+00:00"},
|
||||
{"id": 8, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0002c0001", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.99992, 50.00004], [50.000339, 50.00004], [50.000339, 50.000309], [49.99992, 50.000309], [49.99992, 50.00004]]]}, "centroid_lat": "50.000174", "centroid_lon": "50.000130", "created_at": "2026-05-12T12:19:03.932056+00:00", "updated_at": "2026-05-12T12:19:03.932063+00:00"},
|
||||
{"id": 9, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0002c0002", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[50.000339, 50.00004], [50.000759, 50.00004], [50.000759, 50.000309], [50.000339, 50.000309], [50.000339, 50.00004]]]}, "centroid_lat": "50.000174", "centroid_lon": "50.000549", "created_at": "2026-05-12T12:19:03.932100+00:00", "updated_at": "2026-05-12T12:19:03.932107+00:00"},
|
||||
{"id": 10, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0003c0000", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.9995, 50.000309], [49.99992, 50.000309], [49.99992, 50.000579], [49.9995, 50.000579], [49.9995, 50.000309]]]}, "centroid_lat": "50.000444", "centroid_lon": "49.999710", "created_at": "2026-05-12T12:19:03.932145+00:00", "updated_at": "2026-05-12T12:19:03.932152+00:00"},
|
||||
{"id": 11, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0003c0001", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[49.99992, 50.000309], [50.000339, 50.000309], [50.000339, 50.000579], [49.99992, 50.000579], [49.99992, 50.000309]]]}, "centroid_lat": "50.000444", "centroid_lon": "50.000130", "created_at": "2026-05-12T12:19:03.932191+00:00", "updated_at": "2026-05-12T12:19:03.932198+00:00"},
|
||||
{"id": 12, "soil_location_id": 1, "block_subdivision_id": None, "block_code": "", "cell_code": "loc-1__block-farm__chunk-900__r0003c0002", "chunk_size_sqm": 900, "geometry": {"type": "Polygon", "coordinates": [[[50.000339, 50.000309], [50.000759, 50.000309], [50.000759, 50.000579], [50.000339, 50.000579], [50.000339, 50.000309]]]}, "centroid_lat": "50.000444", "centroid_lon": "50.000549", "created_at": "2026-05-12T12:19:03.932234+00:00", "updated_at": "2026-05-12T12:19:03.932241+00:00"},
|
||||
],
|
||||
"analysisgridobservations": [
|
||||
{"id": 1, "cell_id": 1, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6483580933676826, "ndwi": -0.5629512800110711, "soil_vv": -15.369688, "soil_vv_db": -15.369688, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.614257+00:00", "updated_at": "2026-05-12T12:37:26.614281+00:00"},
|
||||
{"id": 2, "cell_id": 2, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6588515573077731, "ndwi": -0.5730775992075602, "soil_vv": -14.043169, "soil_vv_db": -14.043169, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.615124+00:00", "updated_at": "2026-05-12T12:37:26.615143+00:00"},
|
||||
{"id": 3, "cell_id": 3, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6946650213665433, "ndwi": -0.6026291714774238, "soil_vv": -13.727797, "soil_vv_db": -13.727797, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.615867+00:00", "updated_at": "2026-05-12T12:37:26.615885+00:00"},
|
||||
{"id": 4, "cell_id": 4, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6682408054669698, "ndwi": -0.578668495019277, "soil_vv": -13.127913, "soil_vv_db": -13.127913, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.616668+00:00", "updated_at": "2026-05-12T12:37:26.616687+00:00"},
|
||||
{"id": 5, "cell_id": 5, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6642558574676514, "ndwi": -0.5712497035662333, "soil_vv": -12.400669, "soil_vv_db": -12.400669, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.617453+00:00", "updated_at": "2026-05-12T12:37:26.617472+00:00"},
|
||||
{"id": 6, "cell_id": 6, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.7056174145804511, "ndwi": -0.599965857134925, "soil_vv": -12.273758, "soil_vv_db": -12.273758, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.618181+00:00", "updated_at": "2026-05-12T12:37:26.618199+00:00"},
|
||||
{"id": 7, "cell_id": 7, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6824584868219163, "ndwi": -0.5929381317562528, "soil_vv": -12.147284, "soil_vv_db": -12.147284, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.618964+00:00", "updated_at": "2026-05-12T12:37:26.618982+00:00"},
|
||||
{"id": 8, "cell_id": 8, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6956862476136949, "ndwi": -0.6001381145583259, "soil_vv": -13.170681, "soil_vv_db": -13.170681, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.619733+00:00", "updated_at": "2026-05-12T12:37:26.619750+00:00"},
|
||||
{"id": 9, "cell_id": 9, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.7093238963021172, "ndwi": -0.6121659080187479, "soil_vv": -13.873331, "soil_vv_db": -13.873331, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.620504+00:00", "updated_at": "2026-05-12T12:37:26.620522+00:00"},
|
||||
{"id": 10, "cell_id": 10, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6764502127965292, "ndwi": -0.5939946042166816, "soil_vv": -14.09151, "soil_vv_db": -14.09151, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.621257+00:00", "updated_at": "2026-05-12T12:37:26.621275+00:00"},
|
||||
{"id": 11, "cell_id": 11, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6600760486390855, "ndwi": -0.5822326408492194, "soil_vv": -13.272252, "soil_vv_db": -13.272252, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.621989+00:00", "updated_at": "2026-05-12T12:37:26.622008+00:00"},
|
||||
{"id": 12, "cell_id": 12, "run_id": 1, "temporal_start": "2026-04-11", "temporal_end": "2026-05-11", "ndvi": 0.6991057925754123, "ndwi": -0.6043583750724792, "soil_vv": -12.991811, "soil_vv_db": -12.991811, "dem_m": None, "slope_deg": None, "metadata": {}, "created_at": "2026-05-12T12:37:26.622751+00:00", "updated_at": "2026-05-12T12:37:26.622769+00:00"},
|
||||
],
|
||||
"remotesensingclusterassignments": [
|
||||
{"id": 1, "result_id": 1, "cell_id": 1, "cluster_label": 0, "raw_feature_values": {"ndvi": 0.6483580933676826, "ndwi": -0.5629512800110711, "soil_vv_db": -15.369688}, "scaled_feature_values": {"ndvi": -1.630738, "ndwi": 1.792215, "soil_vv_db": -2.274005}, "created_at": "2026-05-12T12:37:27.901986+00:00", "updated_at": "2026-05-12T12:37:27.902003+00:00"},
|
||||
{"id": 2, "result_id": 1, "cell_id": 2, "cluster_label": 0, "raw_feature_values": {"ndvi": 0.6588515573077731, "ndwi": -0.5730775992075602, "soil_vv_db": -14.043169}, "scaled_feature_values": {"ndvi": -1.094298, "ndwi": 1.109414, "soil_vv_db": -0.762373}, "created_at": "2026-05-12T12:37:27.902768+00:00", "updated_at": "2026-05-12T12:37:27.902786+00:00"},
|
||||
{"id": 3, "result_id": 1, "cell_id": 3, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6946650213665433, "ndwi": -0.6026291714774238, "soil_vv_db": -13.727797}, "scaled_feature_values": {"ndvi": 0.736534, "ndwi": -0.8832, "soil_vv_db": -0.402992}, "created_at": "2026-05-12T12:37:27.903479+00:00", "updated_at": "2026-05-12T12:37:27.903497+00:00"},
|
||||
{"id": 4, "result_id": 1, "cell_id": 4, "cluster_label": 0, "raw_feature_values": {"ndvi": 0.6682408054669698, "ndwi": -0.578668495019277, "soil_vv_db": -13.127913}, "scaled_feature_values": {"ndvi": -0.614307, "ndwi": 0.732429, "soil_vv_db": 0.280605}, "created_at": "2026-05-12T12:37:27.904197+00:00", "updated_at": "2026-05-12T12:37:27.904214+00:00"},
|
||||
{"id": 5, "result_id": 1, "cell_id": 5, "cluster_label": 0, "raw_feature_values": {"ndvi": 0.6642558574676514, "ndwi": -0.5712497035662333, "soil_vv_db": -12.400669}, "scaled_feature_values": {"ndvi": -0.818023, "ndwi": 1.232666, "soil_vv_db": 1.109334}, "created_at": "2026-05-12T12:37:27.904909+00:00", "updated_at": "2026-05-12T12:37:27.904926+00:00"},
|
||||
{"id": 6, "result_id": 1, "cell_id": 6, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.7056174145804511, "ndwi": -0.599965857134925, "soil_vv_db": -12.273758}, "scaled_feature_values": {"ndvi": 1.296436, "ndwi": -0.703617, "soil_vv_db": 1.253955}, "created_at": "2026-05-12T12:37:27.905606+00:00", "updated_at": "2026-05-12T12:37:27.905623+00:00"},
|
||||
{"id": 7, "result_id": 1, "cell_id": 7, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6824584868219163, "ndwi": -0.5929381317562528, "soil_vv_db": -12.147284}, "scaled_feature_values": {"ndvi": 0.11252, "ndwi": -0.229749, "soil_vv_db": 1.398079}, "created_at": "2026-05-12T12:37:27.906325+00:00", "updated_at": "2026-05-12T12:37:27.906343+00:00"},
|
||||
{"id": 8, "result_id": 1, "cell_id": 8, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6956862476136949, "ndwi": -0.6001381145583259, "soil_vv_db": -13.170681}, "scaled_feature_values": {"ndvi": 0.788741, "ndwi": -0.715232, "soil_vv_db": 0.231869}, "created_at": "2026-05-12T12:37:27.907052+00:00", "updated_at": "2026-05-12T12:37:27.907069+00:00"},
|
||||
{"id": 9, "result_id": 1, "cell_id": 9, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.7093238963021172, "ndwi": -0.6121659080187479, "soil_vv_db": -13.873331}, "scaled_feature_values": {"ndvi": 1.485916, "ndwi": -1.526247, "soil_vv_db": -0.568835}, "created_at": "2026-05-12T12:37:27.907735+00:00", "updated_at": "2026-05-12T12:37:27.907752+00:00"},
|
||||
{"id": 10, "result_id": 1, "cell_id": 10, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6764502127965292, "ndwi": -0.5939946042166816, "soil_vv_db": -14.09151}, "scaled_feature_values": {"ndvi": -0.194631, "ndwi": -0.300985, "soil_vv_db": -0.81746}, "created_at": "2026-05-12T12:37:27.908441+00:00", "updated_at": "2026-05-12T12:37:27.908458+00:00"},
|
||||
{"id": 11, "result_id": 1, "cell_id": 11, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6600760486390855, "ndwi": -0.5822326408492194, "soil_vv_db": -13.272252}, "scaled_feature_values": {"ndvi": -1.031701, "ndwi": 0.492105, "soil_vv_db": 0.116124}, "created_at": "2026-05-12T12:37:27.909117+00:00", "updated_at": "2026-05-12T12:37:27.909134+00:00"},
|
||||
{"id": 12, "result_id": 1, "cell_id": 12, "cluster_label": 1, "raw_feature_values": {"ndvi": 0.6991057925754123, "ndwi": -0.6043583750724792, "soil_vv_db": -12.991811}, "scaled_feature_values": {"ndvi": 0.963553, "ndwi": -0.999798, "soil_vv_db": 0.4357}, "created_at": "2026-05-12T12:37:27.909828+00:00", "updated_at": "2026-05-12T12:37:27.909845+00:00"},
|
||||
],
|
||||
"remotesensingsubdivisionoptions": [],
|
||||
"remotesensingsubdivisionoptionblocks": [],
|
||||
"remotesensingsubdivisionoptionassignments": [],
|
||||
"ndviobservations": [],
|
||||
}
|
||||
|
||||
|
||||
def _dt(value: str | None):
|
||||
if not value:
|
||||
return None
|
||||
parsed = parse_datetime(value)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
return datetime.fromisoformat(value)
|
||||
|
||||
|
||||
def _date(value: str | None):
|
||||
if not value:
|
||||
return None
|
||||
return date.fromisoformat(value)
|
||||
|
||||
|
||||
def _decimal(value):
|
||||
if value is None:
|
||||
return None
|
||||
return Decimal(str(value))
|
||||
|
||||
|
||||
def _uuid(value):
|
||||
if not value:
|
||||
return None
|
||||
return UUID(str(value))
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed the current location_data database snapshot into local tables."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--flush-existing",
|
||||
action="store_true",
|
||||
help="Delete existing location_data seeded tables before inserting the snapshot.",
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
if options["flush_existing"]:
|
||||
self._flush_existing()
|
||||
|
||||
self._seed_soil_locations()
|
||||
self._seed_block_subdivisions()
|
||||
self._seed_remote_sensing_runs()
|
||||
self._seed_subdivision_results()
|
||||
self._seed_cluster_blocks()
|
||||
self._seed_analysis_grid_cells()
|
||||
self._seed_analysis_grid_observations()
|
||||
self._seed_cluster_assignments()
|
||||
self._seed_subdivision_options()
|
||||
self._seed_subdivision_option_blocks()
|
||||
self._seed_subdivision_option_assignments()
|
||||
self._seed_ndvi_observations()
|
||||
self.stdout.write(self.style.SUCCESS("location_data seed snapshot applied successfully."))
|
||||
|
||||
def _flush_existing(self):
|
||||
RemoteSensingSubdivisionOptionAssignment.objects.all().delete()
|
||||
RemoteSensingSubdivisionOptionBlock.objects.all().delete()
|
||||
RemoteSensingSubdivisionOption.objects.all().delete()
|
||||
RemoteSensingClusterAssignment.objects.all().delete()
|
||||
AnalysisGridObservation.objects.all().delete()
|
||||
RemoteSensingClusterBlock.objects.all().delete()
|
||||
RemoteSensingSubdivisionResult.objects.all().delete()
|
||||
RemoteSensingRun.objects.all().delete()
|
||||
AnalysisGridCell.objects.all().delete()
|
||||
BlockSubdivision.objects.all().delete()
|
||||
NdviObservation.objects.all().delete()
|
||||
SoilLocation.objects.all().delete()
|
||||
|
||||
def _seed_soil_locations(self):
|
||||
for row in SEED_DATA["soillocations"]:
|
||||
obj, _ = SoilLocation.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"latitude": _decimal(row["latitude"]),
|
||||
"longitude": _decimal(row["longitude"]),
|
||||
"task_id": row["task_id"],
|
||||
"farm_boundary": row["farm_boundary"],
|
||||
"input_block_count": row["input_block_count"],
|
||||
"block_layout": row["block_layout"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_block_subdivisions(self):
|
||||
for row in SEED_DATA["blocksubdivisions"]:
|
||||
obj, _ = BlockSubdivision.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"soil_location_id": row["soil_location_id"],
|
||||
"block_code": row["block_code"],
|
||||
"source_boundary": row["source_boundary"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"grid_points": row["grid_points"],
|
||||
"centroid_points": row["centroid_points"],
|
||||
"grid_point_count": row["grid_point_count"],
|
||||
"centroid_count": row["centroid_count"],
|
||||
"elbow_plot": row.get("elbow_plot", ""),
|
||||
"subdivision_summary": row.get("subdivision_summary", {}),
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_remote_sensing_runs(self):
|
||||
for row in SEED_DATA["remotesensingruns"]:
|
||||
obj, _ = RemoteSensingRun.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"soil_location_id": row["soil_location_id"],
|
||||
"block_subdivision_id": row["block_subdivision_id"],
|
||||
"block_code": row["block_code"],
|
||||
"provider": row["provider"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"temporal_start": _date(row["temporal_start"]),
|
||||
"temporal_end": _date(row["temporal_end"]),
|
||||
"status": row["status"],
|
||||
"metadata": row["metadata"],
|
||||
"error_message": row["error_message"],
|
||||
"started_at": _dt(row["started_at"]),
|
||||
"finished_at": _dt(row["finished_at"]),
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_subdivision_results(self):
|
||||
for row in SEED_DATA["remotesensingsubdivisionresults"]:
|
||||
obj, _ = RemoteSensingSubdivisionResult.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"soil_location_id": row["soil_location_id"],
|
||||
"run_id": row["run_id"],
|
||||
"block_subdivision_id": row["block_subdivision_id"],
|
||||
"block_code": row["block_code"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"temporal_start": _date(row["temporal_start"]),
|
||||
"temporal_end": _date(row["temporal_end"]),
|
||||
"cluster_count": row["cluster_count"],
|
||||
"selected_features": row["selected_features"],
|
||||
"skipped_cell_codes": row["skipped_cell_codes"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_cluster_blocks(self):
|
||||
for row in SEED_DATA["remotesensingclusterblocks"]:
|
||||
obj, _ = RemoteSensingClusterBlock.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"uuid": _uuid(row["uuid"]),
|
||||
"result_id": row["result_id"],
|
||||
"soil_location_id": row["soil_location_id"],
|
||||
"block_subdivision_id": row["block_subdivision_id"],
|
||||
"block_code": row["block_code"],
|
||||
"sub_block_code": row["sub_block_code"],
|
||||
"cluster_label": row["cluster_label"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"centroid_lat": _decimal(row["centroid_lat"]),
|
||||
"centroid_lon": _decimal(row["centroid_lon"]),
|
||||
"center_cell_code": row["center_cell_code"],
|
||||
"center_cell_lat": _decimal(row["center_cell_lat"]),
|
||||
"center_cell_lon": _decimal(row["center_cell_lon"]),
|
||||
"geometry": row["geometry"],
|
||||
"cell_count": row["cell_count"],
|
||||
"cell_codes": row["cell_codes"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_analysis_grid_cells(self):
|
||||
for row in SEED_DATA["analysisgridcells"]:
|
||||
obj, _ = AnalysisGridCell.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"soil_location_id": row["soil_location_id"],
|
||||
"block_subdivision_id": row["block_subdivision_id"],
|
||||
"block_code": row["block_code"],
|
||||
"cell_code": row["cell_code"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"geometry": row["geometry"],
|
||||
"centroid_lat": _decimal(row["centroid_lat"]),
|
||||
"centroid_lon": _decimal(row["centroid_lon"]),
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_analysis_grid_observations(self):
|
||||
for row in SEED_DATA["analysisgridobservations"]:
|
||||
obj, _ = AnalysisGridObservation.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"cell_id": row["cell_id"],
|
||||
"run_id": row["run_id"],
|
||||
"temporal_start": _date(row["temporal_start"]),
|
||||
"temporal_end": _date(row["temporal_end"]),
|
||||
"ndvi": row["ndvi"],
|
||||
"ndwi": row["ndwi"],
|
||||
"soil_vv": row["soil_vv"],
|
||||
"soil_vv_db": row["soil_vv_db"],
|
||||
"dem_m": row["dem_m"],
|
||||
"slope_deg": row["slope_deg"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_cluster_assignments(self):
|
||||
for row in SEED_DATA["remotesensingclusterassignments"]:
|
||||
obj, _ = RemoteSensingClusterAssignment.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"result_id": row["result_id"],
|
||||
"cell_id": row["cell_id"],
|
||||
"cluster_label": row["cluster_label"],
|
||||
"raw_feature_values": row["raw_feature_values"],
|
||||
"scaled_feature_values": row["scaled_feature_values"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_subdivision_options(self):
|
||||
for row in SEED_DATA["remotesensingsubdivisionoptions"]:
|
||||
obj, _ = RemoteSensingSubdivisionOption.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"result_id": row["result_id"],
|
||||
"requested_k": row["requested_k"],
|
||||
"effective_cluster_count": row["effective_cluster_count"],
|
||||
"is_active": row["is_active"],
|
||||
"is_recommended": row["is_recommended"],
|
||||
"selection_source": row["selection_source"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_subdivision_option_blocks(self):
|
||||
for row in SEED_DATA["remotesensingsubdivisionoptionblocks"]:
|
||||
obj, _ = RemoteSensingSubdivisionOptionBlock.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"option_id": row["option_id"],
|
||||
"cluster_label": row["cluster_label"],
|
||||
"sub_block_code": row["sub_block_code"],
|
||||
"chunk_size_sqm": row["chunk_size_sqm"],
|
||||
"centroid_lat": _decimal(row["centroid_lat"]),
|
||||
"centroid_lon": _decimal(row["centroid_lon"]),
|
||||
"center_cell_code": row["center_cell_code"],
|
||||
"center_cell_lat": _decimal(row["center_cell_lat"]),
|
||||
"center_cell_lon": _decimal(row["center_cell_lon"]),
|
||||
"geometry": row["geometry"],
|
||||
"cell_count": row["cell_count"],
|
||||
"cell_codes": row["cell_codes"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_subdivision_option_assignments(self):
|
||||
for row in SEED_DATA["remotesensingsubdivisionoptionassignments"]:
|
||||
obj, _ = RemoteSensingSubdivisionOptionAssignment.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"option_id": row["option_id"],
|
||||
"cell_id": row["cell_id"],
|
||||
"cluster_label": row["cluster_label"],
|
||||
"raw_feature_values": row["raw_feature_values"],
|
||||
"scaled_feature_values": row["scaled_feature_values"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row)
|
||||
|
||||
def _seed_ndvi_observations(self):
|
||||
for row in SEED_DATA["ndviobservations"]:
|
||||
obj, _ = NdviObservation.objects.update_or_create(
|
||||
id=row["id"],
|
||||
defaults={
|
||||
"location_id": row["location_id"],
|
||||
"observation_date": _date(row["observation_date"]),
|
||||
"mean_ndvi": row["mean_ndvi"],
|
||||
"ndvi_map": row["ndvi_map"],
|
||||
"vegetation_health_class": row["vegetation_health_class"],
|
||||
"satellite_source": row["satellite_source"],
|
||||
"cloud_cover": row["cloud_cover"],
|
||||
"metadata": row["metadata"],
|
||||
},
|
||||
)
|
||||
self._touch(obj, row, created_field=False)
|
||||
|
||||
def _touch(self, obj, row, *, created_field=True):
|
||||
updates = []
|
||||
if created_field and hasattr(obj, "created_at") and row.get("created_at"):
|
||||
obj.created_at = _dt(row["created_at"])
|
||||
updates.append("created_at")
|
||||
if hasattr(obj, "updated_at") and row.get("updated_at"):
|
||||
obj.updated_at = _dt(row["updated_at"])
|
||||
updates.append("updated_at")
|
||||
if updates:
|
||||
obj.save(update_fields=updates)
|
||||
+84
-28
@@ -3,24 +3,83 @@ import uuid
|
||||
from django.db import models
|
||||
|
||||
|
||||
def build_default_sub_block(block_code: str, *, boundary: dict | None = None) -> dict:
|
||||
normalized_block_code = str(block_code or "block-1").strip() or "block-1"
|
||||
return {
|
||||
"sub_block_code": f"{normalized_block_code}-sub-1",
|
||||
"cluster_label": 0,
|
||||
"source": "default",
|
||||
"boundary": boundary or {},
|
||||
"cluster_uuid": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def ensure_block_layout_defaults(layout: dict | None, *, block_count: int | None = None) -> dict:
|
||||
raw_layout = dict(layout or {})
|
||||
raw_blocks = list(raw_layout.get("blocks") or [])
|
||||
normalized_count = len(raw_blocks) if raw_blocks else max(int(block_count or raw_layout.get("input_block_count") or 1), 1)
|
||||
normalized_blocks: list[dict] = []
|
||||
|
||||
for index in range(normalized_count):
|
||||
raw_block = raw_blocks[index] if index < len(raw_blocks) else {}
|
||||
block_code = str(raw_block.get("block_code") or f"block-{index + 1}").strip() or f"block-{index + 1}"
|
||||
boundary = raw_block.get("boundary") or {}
|
||||
sub_blocks = [dict(sub_block) for sub_block in (raw_block.get("sub_blocks") or []) if isinstance(sub_block, dict)]
|
||||
if not sub_blocks:
|
||||
sub_blocks = [build_default_sub_block(block_code, boundary=boundary)]
|
||||
|
||||
normalized_block = {
|
||||
"block_code": block_code,
|
||||
"order": int(raw_block.get("order") or index + 1),
|
||||
"source": raw_block.get("source") or ("input" if raw_blocks or normalized_count > 1 else "default"),
|
||||
"boundary": boundary,
|
||||
"needs_subdivision": raw_block.get("needs_subdivision"),
|
||||
"sub_blocks": sub_blocks,
|
||||
}
|
||||
for extra_key in ("subdivision_summary", "analysis_grid_summary", "aggregated_metrics"):
|
||||
if extra_key in raw_block:
|
||||
normalized_block[extra_key] = raw_block[extra_key]
|
||||
normalized_blocks.append(normalized_block)
|
||||
|
||||
return {
|
||||
"input_block_count": normalized_count,
|
||||
"default_full_farm": raw_layout.get("default_full_farm", normalized_count == 1),
|
||||
"algorithm_status": raw_layout.get("algorithm_status") or "pending",
|
||||
"blocks": normalized_blocks,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def build_block_layout(block_count: int = 1, blocks: list[dict] | None = None) -> dict:
|
||||
normalized_blocks = []
|
||||
if blocks:
|
||||
for index, block in enumerate(blocks):
|
||||
normalized_blocks.append(
|
||||
{
|
||||
"block_code": str(block.get("block_code") or f"block-{index + 1}").strip(),
|
||||
"order": int(block.get("order") or index + 1),
|
||||
"source": "input",
|
||||
"boundary": block.get("boundary") or {},
|
||||
"needs_subdivision": None,
|
||||
"sub_blocks": [],
|
||||
}
|
||||
)
|
||||
else:
|
||||
normalized_count = max(int(block_count or 1), 1)
|
||||
for index in range(normalized_count):
|
||||
normalized_blocks.append(
|
||||
return ensure_block_layout_defaults(
|
||||
{
|
||||
"input_block_count": len(blocks),
|
||||
"default_full_farm": len(blocks) == 1,
|
||||
"algorithm_status": "pending",
|
||||
"blocks": [
|
||||
{
|
||||
"block_code": str(block.get("block_code") or f"block-{index + 1}").strip(),
|
||||
"order": int(block.get("order") or index + 1),
|
||||
"source": "input",
|
||||
"boundary": block.get("boundary") or {},
|
||||
"needs_subdivision": None,
|
||||
"sub_blocks": [dict(sub_block) for sub_block in (block.get("sub_blocks") or [])],
|
||||
}
|
||||
for index, block in enumerate(blocks)
|
||||
],
|
||||
},
|
||||
block_count=len(blocks),
|
||||
)
|
||||
|
||||
normalized_count = max(int(block_count or 1), 1)
|
||||
return ensure_block_layout_defaults(
|
||||
{
|
||||
"input_block_count": normalized_count,
|
||||
"default_full_farm": normalized_count == 1,
|
||||
"algorithm_status": "pending",
|
||||
"blocks": [
|
||||
{
|
||||
"block_code": f"block-{index + 1}",
|
||||
"order": index + 1,
|
||||
@@ -29,16 +88,11 @@ def build_block_layout(block_count: int = 1, blocks: list[dict] | None = None) -
|
||||
"needs_subdivision": None,
|
||||
"sub_blocks": [],
|
||||
}
|
||||
)
|
||||
|
||||
normalized_count = len(normalized_blocks) if normalized_blocks else max(int(block_count or 1), 1)
|
||||
|
||||
return {
|
||||
"input_block_count": normalized_count,
|
||||
"default_full_farm": normalized_count == 1,
|
||||
"algorithm_status": "pending",
|
||||
"blocks": normalized_blocks,
|
||||
}
|
||||
for index in range(normalized_count)
|
||||
],
|
||||
},
|
||||
block_count=normalized_count,
|
||||
)
|
||||
|
||||
|
||||
class SoilLocation(models.Model):
|
||||
@@ -122,8 +176,10 @@ class SoilLocation(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.input_block_count:
|
||||
self.input_block_count = 1
|
||||
if not self.block_layout:
|
||||
self.block_layout = build_block_layout(self.input_block_count)
|
||||
self.block_layout = ensure_block_layout_defaults(
|
||||
self.block_layout,
|
||||
block_count=self.input_block_count,
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
from django.db.models import Avg, QuerySet
|
||||
|
||||
from .models import (
|
||||
ensure_block_layout_defaults,
|
||||
AnalysisGridObservation,
|
||||
RemoteSensingRun,
|
||||
RemoteSensingSubdivisionResult,
|
||||
@@ -90,7 +91,7 @@ def build_location_block_satellite_snapshots(
|
||||
*,
|
||||
sensor_payload: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
block_layout = location.block_layout or {}
|
||||
block_layout = ensure_block_layout_defaults(location.block_layout, block_count=location.input_block_count)
|
||||
blocks = block_layout.get("blocks") or []
|
||||
if not blocks:
|
||||
return [build_location_satellite_snapshot(location, sensor_payload=sensor_payload)]
|
||||
@@ -112,7 +113,7 @@ def build_block_layout_metric_summary(
|
||||
*,
|
||||
sensor_payload: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
layout = dict(location.block_layout or {})
|
||||
layout = ensure_block_layout_defaults(location.block_layout, block_count=location.input_block_count)
|
||||
blocks = [dict(block) for block in (layout.get("blocks") or [])]
|
||||
snapshots_by_block_code = {
|
||||
str(snapshot.get("block_code") or ""): snapshot
|
||||
@@ -461,7 +462,7 @@ def build_block_sensor_summary(
|
||||
|
||||
|
||||
def _build_active_sub_block_lookup(location: SoilLocation) -> dict[str, Any]:
|
||||
block_layout = dict(location.block_layout or {})
|
||||
block_layout = ensure_block_layout_defaults(location.block_layout, block_count=location.input_block_count)
|
||||
by_cluster_uuid: dict[str, dict[str, Any]] = {}
|
||||
by_sub_block_code: dict[str, list[dict[str, Any]]] = {}
|
||||
by_block_and_cluster_label: dict[tuple[str, int], dict[str, Any]] = {}
|
||||
|
||||
@@ -144,6 +144,10 @@ class RemoteSensingFarmRequestSerializer(serializers.Serializer):
|
||||
page_size = serializers.IntegerField(required=False, min_value=1, max_value=200, default=100)
|
||||
|
||||
|
||||
class ClusterCropRecommendationRequestSerializer(serializers.Serializer):
|
||||
farm_uuid = serializers.UUIDField(required=True, help_text="شناسه یکتای مزرعه")
|
||||
|
||||
|
||||
class RemoteSensingClusterBlockLiveRequestSerializer(serializers.Serializer):
|
||||
temporal_start = serializers.DateField(required=False)
|
||||
temporal_end = serializers.DateField(required=False)
|
||||
@@ -426,6 +430,54 @@ class RemoteSensingClusterBlockLiveResponseSerializer(serializers.Serializer):
|
||||
metadata = serializers.JSONField()
|
||||
|
||||
|
||||
class ClusterCropRegisteredPlantSerializer(serializers.Serializer):
|
||||
plant_id = serializers.IntegerField()
|
||||
plant_name = serializers.CharField()
|
||||
position = serializers.IntegerField(allow_null=True)
|
||||
stage = serializers.CharField(allow_blank=True)
|
||||
|
||||
|
||||
class ClusterCropCandidateSerializer(serializers.Serializer):
|
||||
plant_id = serializers.IntegerField(allow_null=True)
|
||||
plant_name = serializers.CharField()
|
||||
position = serializers.IntegerField(allow_null=True)
|
||||
stage = serializers.CharField(allow_blank=True)
|
||||
score = serializers.FloatField()
|
||||
predicted_yield = serializers.FloatField(allow_null=True)
|
||||
predicted_yield_tons = serializers.FloatField(allow_null=True)
|
||||
biomass = serializers.FloatField(allow_null=True)
|
||||
max_lai = serializers.FloatField(allow_null=True)
|
||||
simulation_engine = serializers.CharField(allow_null=True)
|
||||
simulation_model_name = serializers.CharField(allow_null=True)
|
||||
simulation_warning = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
supporting_metrics = serializers.JSONField()
|
||||
|
||||
|
||||
class ClusterCropRecommendationClusterSerializer(serializers.Serializer):
|
||||
block_code = serializers.CharField(allow_blank=True)
|
||||
cluster_uuid = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
sub_block_code = serializers.CharField()
|
||||
cluster_label = serializers.IntegerField(allow_null=True)
|
||||
temporal_extent = serializers.JSONField(allow_null=True)
|
||||
cluster_block = RemoteSensingClusterBlockSerializer(allow_null=True)
|
||||
satellite_metrics = serializers.JSONField()
|
||||
sensor_metrics = serializers.JSONField()
|
||||
resolved_metrics = serializers.JSONField()
|
||||
candidate_plants = ClusterCropCandidateSerializer(many=True)
|
||||
suggested_plant = ClusterCropCandidateSerializer(allow_null=True)
|
||||
source_metadata = serializers.JSONField()
|
||||
|
||||
|
||||
class ClusterCropRecommendationResponseSerializer(serializers.Serializer):
|
||||
farm_uuid = serializers.CharField()
|
||||
location_id = serializers.IntegerField()
|
||||
evaluated_plant_count = serializers.IntegerField()
|
||||
cluster_count = serializers.IntegerField()
|
||||
registered_plants = ClusterCropRegisteredPlantSerializer(many=True)
|
||||
clusters = ClusterCropRecommendationClusterSerializer(many=True)
|
||||
source_metadata = serializers.JSONField()
|
||||
|
||||
|
||||
class RemoteSensingSubdivisionOptionListResponseSerializer(serializers.Serializer):
|
||||
result_id = serializers.IntegerField()
|
||||
active_requested_k = serializers.IntegerField(allow_null=True)
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
from datetime import date
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from farm_data.models import FarmPlantAssignment, PlantCatalogSnapshot, SensorData
|
||||
from location_data.models import (
|
||||
AnalysisGridCell,
|
||||
AnalysisGridObservation,
|
||||
BlockSubdivision,
|
||||
RemoteSensingClusterAssignment,
|
||||
RemoteSensingClusterBlock,
|
||||
RemoteSensingRun,
|
||||
RemoteSensingSubdivisionResult,
|
||||
SoilLocation,
|
||||
)
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF="location_data.urls")
|
||||
class RemoteSensingClusterRecommendationApiTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.boundary = {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[51.3890, 35.6890],
|
||||
[51.3900, 35.6890],
|
||||
[51.3900, 35.6900],
|
||||
[51.3890, 35.6900],
|
||||
[51.3890, 35.6890],
|
||||
]
|
||||
],
|
||||
}
|
||||
self.location = SoilLocation.objects.create(
|
||||
latitude="35.689200",
|
||||
longitude="51.389000",
|
||||
farm_boundary=self.boundary,
|
||||
)
|
||||
self.location.set_input_block_count(1)
|
||||
self.location.save(update_fields=["input_block_count", "block_layout", "updated_at"])
|
||||
self.farm = SensorData.objects.create(
|
||||
farm_uuid="11111111-1111-1111-1111-111111111111",
|
||||
center_location=self.location,
|
||||
sensor_payload={},
|
||||
)
|
||||
for day_index in range(1, 5):
|
||||
WeatherForecast.objects.create(
|
||||
location=self.location,
|
||||
forecast_date=date(2025, 2, day_index),
|
||||
temperature_min=12.0,
|
||||
temperature_max=24.0,
|
||||
temperature_mean=18.0,
|
||||
precipitation=1.0,
|
||||
precipitation_probability=25.0,
|
||||
humidity_mean=55.0,
|
||||
wind_speed_max=10.0,
|
||||
et0=3.0,
|
||||
weather_code=1,
|
||||
)
|
||||
self.subdivision = BlockSubdivision.objects.create(
|
||||
soil_location=self.location,
|
||||
block_code="block-1",
|
||||
source_boundary=self.boundary,
|
||||
chunk_size_sqm=900,
|
||||
status="subdivided",
|
||||
)
|
||||
self.run = RemoteSensingRun.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
chunk_size_sqm=900,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
status=RemoteSensingRun.STATUS_SUCCESS,
|
||||
metadata={"stage": "completed"},
|
||||
)
|
||||
self.result = RemoteSensingSubdivisionResult.objects.create(
|
||||
soil_location=self.location,
|
||||
run=self.run,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
chunk_size_sqm=900,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
cluster_count=2,
|
||||
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
||||
metadata={"used_cell_count": 2, "skipped_cell_count": 0},
|
||||
)
|
||||
|
||||
self.cell_1 = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code="cell-1",
|
||||
chunk_size_sqm=900,
|
||||
geometry=self.boundary,
|
||||
centroid_lat="35.689250",
|
||||
centroid_lon="51.389250",
|
||||
)
|
||||
self.cell_2 = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code="cell-2",
|
||||
chunk_size_sqm=900,
|
||||
geometry=self.boundary,
|
||||
centroid_lat="35.689750",
|
||||
centroid_lon="51.389750",
|
||||
)
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=self.cell_1,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.51,
|
||||
ndwi=0.24,
|
||||
soil_vv=0.13,
|
||||
soil_vv_db=-10.0,
|
||||
metadata={"backend_name": "openeo"},
|
||||
)
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=self.cell_2,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.71,
|
||||
ndwi=0.48,
|
||||
soil_vv=0.19,
|
||||
soil_vv_db=-7.5,
|
||||
metadata={"backend_name": "openeo"},
|
||||
)
|
||||
RemoteSensingClusterAssignment.objects.create(
|
||||
result=self.result,
|
||||
cell=self.cell_1,
|
||||
cluster_label=0,
|
||||
raw_feature_values={"ndvi": 0.51, "ndwi": 0.24, "soil_vv_db": -10.0},
|
||||
scaled_feature_values={"ndvi": -1.0, "ndwi": -1.0, "soil_vv_db": -1.0},
|
||||
)
|
||||
RemoteSensingClusterAssignment.objects.create(
|
||||
result=self.result,
|
||||
cell=self.cell_2,
|
||||
cluster_label=1,
|
||||
raw_feature_values={"ndvi": 0.71, "ndwi": 0.48, "soil_vv_db": -7.5},
|
||||
scaled_feature_values={"ndvi": 1.0, "ndwi": 1.0, "soil_vv_db": 1.0},
|
||||
)
|
||||
self.cluster_0 = RemoteSensingClusterBlock.objects.create(
|
||||
result=self.result,
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
sub_block_code="cluster-0",
|
||||
cluster_label=0,
|
||||
chunk_size_sqm=900,
|
||||
centroid_lat="35.689250",
|
||||
centroid_lon="51.389250",
|
||||
center_cell_code="cell-1",
|
||||
cell_count=1,
|
||||
cell_codes=["cell-1"],
|
||||
geometry=self.boundary,
|
||||
)
|
||||
self.cluster_1 = RemoteSensingClusterBlock.objects.create(
|
||||
result=self.result,
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
sub_block_code="cluster-1",
|
||||
cluster_label=1,
|
||||
chunk_size_sqm=900,
|
||||
centroid_lat="35.689750",
|
||||
centroid_lon="51.389750",
|
||||
center_cell_code="cell-2",
|
||||
cell_count=1,
|
||||
cell_codes=["cell-2"],
|
||||
geometry=self.boundary,
|
||||
)
|
||||
|
||||
self.location.block_layout = {
|
||||
"input_block_count": 1,
|
||||
"default_full_farm": True,
|
||||
"algorithm_status": "completed",
|
||||
"blocks": [
|
||||
{
|
||||
"block_code": "block-1",
|
||||
"order": 1,
|
||||
"source": "input",
|
||||
"boundary": self.boundary,
|
||||
"needs_subdivision": True,
|
||||
"sub_blocks": [
|
||||
{
|
||||
"sub_block_code": "cluster-0",
|
||||
"cluster_label": 0,
|
||||
"cluster_uuid": str(self.cluster_0.uuid),
|
||||
},
|
||||
{
|
||||
"sub_block_code": "cluster-1",
|
||||
"cluster_label": 1,
|
||||
"cluster_uuid": str(self.cluster_1.uuid),
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
self.location.save(update_fields=["block_layout", "updated_at"])
|
||||
|
||||
self.tomato = PlantCatalogSnapshot.objects.create(
|
||||
backend_plant_id=101,
|
||||
name="Tomato",
|
||||
growth_profile={"simulation": {"crop_parameters": {"crop_name": "Tomato", "MAX_BIOMASS": 14000.0}}},
|
||||
)
|
||||
self.wheat = PlantCatalogSnapshot.objects.create(
|
||||
backend_plant_id=102,
|
||||
name="Wheat",
|
||||
growth_profile={"simulation": {"crop_parameters": {"crop_name": "Wheat", "MAX_BIOMASS": 11000.0}}},
|
||||
)
|
||||
FarmPlantAssignment.objects.create(farm=self.farm, plant=self.tomato, position=0, stage="vegetative")
|
||||
FarmPlantAssignment.objects.create(farm=self.farm, plant=self.wheat, position=1, stage="vegetative")
|
||||
|
||||
@patch("location_data.cluster_recommendation._simulate_candidate")
|
||||
def test_cluster_recommendations_return_ranked_plants_for_each_cluster(self, simulate_mock):
|
||||
def fake_simulation(*, base_payload, soil_parameters, site_parameters):
|
||||
plant_name = base_payload["crop_parameters"]["crop_name"]
|
||||
smfcf = float(soil_parameters["SMFCF"])
|
||||
if plant_name == "Tomato":
|
||||
yield_estimate = 150.0 if smfcf >= 0.4 else 80.0
|
||||
else:
|
||||
yield_estimate = 110.0 if smfcf >= 0.4 else 120.0
|
||||
return (
|
||||
{
|
||||
"engine": "pcse",
|
||||
"model_name": "Wofost81_NWLP_CWB_CNB",
|
||||
"metrics": {
|
||||
"yield_estimate": yield_estimate,
|
||||
"biomass": yield_estimate * 2,
|
||||
"max_lai": 4.2,
|
||||
},
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
simulate_mock.side_effect = fake_simulation
|
||||
|
||||
response = self.client.get(
|
||||
"/remote-sensing/cluster-recommendations/",
|
||||
data={"farm_uuid": str(self.farm.farm_uuid)},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()["data"]
|
||||
self.assertEqual(payload["cluster_count"], 2)
|
||||
self.assertEqual(payload["evaluated_plant_count"], 2)
|
||||
self.assertEqual(len(payload["registered_plants"]), 2)
|
||||
|
||||
clusters = {item["cluster_label"]: item for item in payload["clusters"]}
|
||||
self.assertEqual(clusters[0]["resolved_metrics"]["ndvi"], 0.51)
|
||||
self.assertEqual(clusters[0]["resolved_metrics"]["ndwi"], 0.24)
|
||||
self.assertEqual(clusters[0]["resolved_metrics"]["soil_vv"], 0.13)
|
||||
self.assertEqual(clusters[1]["resolved_metrics"]["ndwi"], 0.48)
|
||||
|
||||
self.assertEqual(clusters[0]["suggested_plant"]["plant_name"], "Wheat")
|
||||
self.assertEqual(clusters[1]["suggested_plant"]["plant_name"], "Tomato")
|
||||
self.assertEqual(clusters[0]["candidate_plants"][0]["score"], 120.0)
|
||||
self.assertEqual(clusters[1]["candidate_plants"][0]["score"], 150.0)
|
||||
self.assertEqual(clusters[0]["cluster_block"]["uuid"], str(self.cluster_0.uuid))
|
||||
self.assertEqual(clusters[1]["cluster_block"]["uuid"], str(self.cluster_1.uuid))
|
||||
|
||||
def test_cluster_recommendations_return_400_when_no_plants_registered(self):
|
||||
FarmPlantAssignment.objects.all().delete()
|
||||
|
||||
response = self.client.get(
|
||||
"/remote-sensing/cluster-recommendations/",
|
||||
data={"farm_uuid": str(self.farm.farm_uuid)},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
response.json()["msg"],
|
||||
"برای این مزرعه هنوز هیچ گیاهی در farm_data ثبت نشده است.",
|
||||
)
|
||||
@@ -57,10 +57,29 @@ class SoilDataApiTests(TestCase):
|
||||
self.assertEqual(len(payload["block_layout"]["blocks"]), 1)
|
||||
self.assertEqual(payload["block_layout"]["blocks"][0]["boundary"], self.block_boundary)
|
||||
self.assertEqual(payload["block_layout"]["algorithm_status"], "pending")
|
||||
self.assertEqual(len(payload["block_layout"]["blocks"][0]["sub_blocks"]), 1)
|
||||
self.assertEqual(payload["block_layout"]["blocks"][0]["sub_blocks"][0]["sub_block_code"], "block-1-sub-1")
|
||||
self.assertEqual(payload["block_layout"]["blocks"][0]["sub_blocks"][0]["cluster_label"], 0)
|
||||
self.assertEqual(len(payload["block_subdivisions"]), 1)
|
||||
self.assertEqual(payload["block_subdivisions"][0]["status"], "defined")
|
||||
self.assertEqual(payload["satellite_snapshots"][0]["status"], "missing")
|
||||
|
||||
def test_model_default_layout_includes_default_sub_block_when_missing(self):
|
||||
location = SoilLocation.objects.create(
|
||||
latitude="35.689201",
|
||||
longitude="51.389001",
|
||||
)
|
||||
|
||||
self.assertEqual(location.input_block_count, 1)
|
||||
self.assertEqual(location.block_layout["blocks"][0]["block_code"], "block-1")
|
||||
self.assertEqual(len(location.block_layout["blocks"][0]["sub_blocks"]), 1)
|
||||
self.assertEqual(
|
||||
location.block_layout["blocks"][0]["sub_blocks"][0]["sub_block_code"],
|
||||
"block-1-sub-1",
|
||||
)
|
||||
self.assertEqual(location.block_layout["blocks"][0]["sub_blocks"][0]["source"], "default")
|
||||
|
||||
|
||||
def test_post_updates_block_layout_from_input(self):
|
||||
SoilLocation.objects.create(
|
||||
latitude="35.689200",
|
||||
|
||||
@@ -4,6 +4,7 @@ from .views import (
|
||||
NdviHealthView,
|
||||
RemoteSensingAnalysisView,
|
||||
RemoteSensingClusterBlockLiveView,
|
||||
RemoteSensingClusterRecommendationView,
|
||||
RemoteSensingSubdivisionOptionActivateView,
|
||||
RemoteSensingSubdivisionOptionListView,
|
||||
RemoteSensingRunStatusView,
|
||||
@@ -18,6 +19,11 @@ urlpatterns = [
|
||||
RemoteSensingClusterBlockLiveView.as_view(),
|
||||
name="remote-sensing-cluster-block-live",
|
||||
),
|
||||
path(
|
||||
"remote-sensing/cluster-recommendations/",
|
||||
RemoteSensingClusterRecommendationView.as_view(),
|
||||
name="remote-sensing-cluster-recommendations",
|
||||
),
|
||||
path(
|
||||
"remote-sensing/results/<int:result_id>/k-options/",
|
||||
RemoteSensingSubdivisionOptionListView.as_view(),
|
||||
|
||||
@@ -37,8 +37,15 @@ from farm_data.models import SensorData
|
||||
|
||||
from .data_driven_subdivision import DEFAULT_CLUSTER_FEATURES
|
||||
from .data_driven_subdivision import activate_subdivision_option
|
||||
from .cluster_recommendation import (
|
||||
ClusterRecommendationNotFound,
|
||||
ClusterRecommendationValidationError,
|
||||
build_cluster_crop_recommendations,
|
||||
)
|
||||
from .serializers import (
|
||||
BlockSubdivisionSerializer,
|
||||
ClusterCropRecommendationRequestSerializer,
|
||||
ClusterCropRecommendationResponseSerializer,
|
||||
NdviHealthRequestSerializer,
|
||||
NdviHealthResponseSerializer,
|
||||
RemoteSensingCellObservationSerializer,
|
||||
@@ -140,6 +147,10 @@ RemoteSensingClusterBlockLiveEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingClusterBlockLiveEnvelopeSerializer",
|
||||
RemoteSensingClusterBlockLiveResponseSerializer,
|
||||
)
|
||||
ClusterCropRecommendationEnvelopeSerializer = build_envelope_serializer(
|
||||
"ClusterCropRecommendationEnvelopeSerializer",
|
||||
ClusterCropRecommendationResponseSerializer,
|
||||
)
|
||||
RemoteSensingSubdivisionOptionListEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingSubdivisionOptionListEnvelopeSerializer",
|
||||
RemoteSensingSubdivisionOptionListResponseSerializer,
|
||||
@@ -843,6 +854,75 @@ class RemoteSensingClusterBlockLiveView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class RemoteSensingClusterRecommendationView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
summary="پیشنهاد گیاه برای هر کلاستر KMeans",
|
||||
description=(
|
||||
"با دریافت farm_uuid، داده هر کلاستر KMeans location_data بههمراه "
|
||||
"ndvi، ndwi، soil_vv، soil_vv_db و مقایسه گیاههای ثبتشده در farm_data "
|
||||
"با crop_simulation برگردانده میشود و برای هر زیربلاک یک گیاه پیشنهادی ارائه میشود."
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="farm_uuid",
|
||||
type=str,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="شناسه یکتای مزرعه",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: build_response(
|
||||
ClusterCropRecommendationEnvelopeSerializer,
|
||||
"داده کلاسترها و پیشنهاد گیاه برای هر زیربلاک بازگردانده شد.",
|
||||
),
|
||||
400: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"پیشنیازهای مقایسه نامعتبر یا ناقص است.",
|
||||
),
|
||||
404: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"مزرعه یا خروجی KMeans یافت نشد.",
|
||||
),
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"نمونه درخواست پیشنهاد گیاه برای کلاسترها",
|
||||
value={"farm_uuid": "11111111-1111-1111-1111-111111111111"},
|
||||
parameter_only=("farm_uuid", "query"),
|
||||
)
|
||||
],
|
||||
)
|
||||
def get(self, request):
|
||||
serializer = ClusterCropRecommendationRequestSerializer(data=request.query_params)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
payload = build_cluster_crop_recommendations(
|
||||
str(serializer.validated_data["farm_uuid"])
|
||||
)
|
||||
except ClusterRecommendationNotFound as exc:
|
||||
return Response(
|
||||
{"code": 404, "msg": str(exc), "data": None},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except ClusterRecommendationValidationError as exc:
|
||||
return Response(
|
||||
{"code": 400, "msg": str(exc), "data": None},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": payload},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class RemoteSensingSubdivisionOptionListView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
|
||||
Reference in New Issue
Block a user