Spaces:
Paused
Paused
Download app/services/benchmarks.py from SM737/satquery-api: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/spaces/SM737/satquery-api/resolve/main/app/services/benchmarks.py
- Command line
-
hf download hf://spaces/SM737/satquery-api/app/services/benchmarks.py
-
curl -L -o benchmarks.py https://huggingface.co/spaces/SM737/satquery-api/resolve/main/app/services/benchmarks.py
15.9 kB
| from __future__ import annotations | |
| import math | |
| import time | |
| import uuid | |
| from dataclasses import dataclass, field | |
| from datetime import UTC, datetime | |
| from typing import Any | |
| from app.schemas import ( | |
| BenchmarkItemResult, | |
| BenchmarkRunResponse, | |
| BenchmarkSummaryMetrics, | |
| DatasetSummary, | |
| VRSBenchCategory, | |
| ) | |
| from app.services.model_registry import ModelRegistry | |
| class VRSBenchSample: | |
| sample_id: str | |
| category: VRSBenchCategory | |
| question: str | |
| expected_answer: str | |
| keywords: tuple[str, ...] | |
| ground_truth_boxes: list[list[float]] = field(default_factory=list) | |
| expected_variant: str = "earthdial-4b-rgb" | |
| band_count: int = 3 | |
| description: str = "" | |
| # Curated frozen-split VRSBench samples representing optical RGB, multispectral, and grounding tasks | |
| VRSBENCH_FROZEN_SAMPLES: tuple[VRSBenchSample, ...] = ( | |
| VRSBenchSample( | |
| sample_id="VRS-OPT-001", | |
| category=VRSBenchCategory.OPTICAL_VQA, | |
| question="What is the dominant land cover class in the central region of the scene?", | |
| expected_answer="urban built-up structures and commercial area", | |
| keywords=("urban", "built-up", "structures", "commercial", "building"), | |
| ground_truth_boxes=[[0.2, 0.2, 0.8, 0.8]], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="High-resolution optical scene classification query", | |
| ), | |
| VRSBenchSample( | |
| sample_id="VRS-OPT-002", | |
| category=VRSBenchCategory.VISUAL_GROUNDING, | |
| question="Ground and locate the primary linear road infrastructure crossing the image.", | |
| expected_answer="Linear road corridor detected spanning east to west across the center.", | |
| keywords=("road", "corridor", "transportation", "linear", "infrastructure"), | |
| ground_truth_boxes=[[0.40, 0.05, 0.60, 0.95]], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="Bounding box visual grounding on transportation corridor", | |
| ), | |
| VRSBenchSample( | |
| sample_id="VRS-MS-003", | |
| category=VRSBenchCategory.MULTISPECTRAL_VQA, | |
| question="Does the multispectral NIR/SWIR signature indicate healthy dense vegetation canopy in the northern parcel?", | |
| expected_answer="Yes, strong NIR reflection and low red reflectance indicate healthy dense vegetation canopy.", | |
| keywords=("yes", "vegetation", "canopy", "dense", "nir", "healthy"), | |
| ground_truth_boxes=[[0.05, 0.05, 0.45, 0.50]], | |
| expected_variant="earthdial-4b-ms", | |
| band_count=5, | |
| description="Multispectral red-edge/NIR canopy evaluation", | |
| ), | |
| VRSBenchSample( | |
| sample_id="VRS-MS-004", | |
| category=VRSBenchCategory.VISUAL_GROUNDING, | |
| question="Detect and localize the open water reservoir exhibiting low SWIR reflectance.", | |
| expected_answer="Water body located with distinct clear boundary.", | |
| keywords=("water", "reservoir", "lake", "swir", "boundary"), | |
| ground_truth_boxes=[[0.12, 0.10, 0.38, 0.40]], | |
| expected_variant="earthdial-4b-ms", | |
| band_count=5, | |
| description="Multispectral water boundary grounding", | |
| ), | |
| VRSBenchSample( | |
| sample_id="VRS-OPT-005", | |
| category=VRSBenchCategory.LAND_COVER_VERIFICATION, | |
| question="Verify if there is any active construction or earthwork in the southern quadrant.", | |
| expected_answer="Yes, distinct bare soil and active earthwork clearing are observed.", | |
| keywords=("construction", "earthwork", "bare", "soil", "clearing"), | |
| ground_truth_boxes=[[0.55, 0.10, 0.90, 0.50]], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="Verification of land disturbance in high-resolution optical", | |
| ), | |
| VRSBenchSample( | |
| sample_id="VRS-MS-006", | |
| category=VRSBenchCategory.MULTISPECTRAL_VQA, | |
| question="Is there evidence of high surface moisture or water-logging in the agricultural fields?", | |
| expected_answer="Low NDWI values indicate normal soil moisture with no catastrophic water-logging.", | |
| keywords=("moisture", "water", "normal", "logging", "agricultural"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-ms", | |
| band_count=5, | |
| description="Multispectral agricultural moisture assessment", | |
| ), | |
| ) | |
| def compute_box_iou(box_a: list[float], box_b: list[float]) -> float: | |
| """Calculate Intersection over Union (IoU) between two bounding boxes [ymin, xmin, ymax, xmax].""" | |
| if len(box_a) != 4 or len(box_b) != 4: | |
| return 0.0 | |
| ymin1, xmin1, ymax1, xmax1 = box_a | |
| ymin2, xmin2, ymax2, xmax2 = box_b | |
| inter_ymin = max(ymin1, ymin2) | |
| inter_xmin = max(xmin1, xmin2) | |
| inter_ymax = min(ymax1, ymax2) | |
| inter_xmax = min(xmax1, xmax2) | |
| inter_area = max(0.0, inter_ymax - inter_ymin) * max(0.0, inter_xmax - inter_xmin) | |
| area_a = max(0.0, ymax1 - ymin1) * max(0.0, xmax1 - xmin1) | |
| area_b = max(0.0, ymax2 - ymin2) * max(0.0, xmax2 - xmin2) | |
| union_area = area_a + area_b - inter_area | |
| if union_area <= 0: | |
| return 0.0 | |
| return float(inter_area / union_area) | |
| def compute_mean_iou(predicted_boxes: list[list[float]], gt_boxes: list[list[float]]) -> float | None: | |
| if not gt_boxes or not predicted_boxes: | |
| return None | |
| ious = [] | |
| for gt in gt_boxes: | |
| best_match = max((compute_box_iou(pred, gt) for pred in predicted_boxes), default=0.0) | |
| ious.append(best_match) | |
| return float(sum(ious) / len(ious)) if ious else None | |
| def compute_semantic_similarity(predicted: str, target: str, keywords: tuple[str, ...]) -> float: | |
| pred_lower = predicted.lower() | |
| target_lower = target.lower() | |
| if pred_lower == target_lower: | |
| return 1.0 | |
| # Keyword hit ratio | |
| keyword_hits = sum(1 for kw in keywords if kw in pred_lower) | |
| keyword_score = keyword_hits / max(1, len(keywords)) | |
| # Token overlap Jaccard | |
| pred_tokens = set(pred_lower.replace(",", " ").replace(".", " ").split()) | |
| target_tokens = set(target_lower.replace(",", " ").replace(".", " ").split()) | |
| intersection = pred_tokens.intersection(target_tokens) | |
| union = pred_tokens.union(target_tokens) | |
| jaccard = len(intersection) / max(1, len(union)) | |
| return round(0.6 * keyword_score + 0.4 * jaccard, 3) | |
| def compute_token_perplexity(token_logprobs: list[float] | None) -> float | None: | |
| if not token_logprobs: | |
| return None | |
| mean_neg_logprob = -sum(token_logprobs) / len(token_logprobs) | |
| return round(math.exp(mean_neg_logprob), 3) | |
| RSVQA_FROZEN_SAMPLES: tuple[VRSBenchSample, ...] = ( | |
| VRSBenchSample( | |
| sample_id="RSVQA-LR-001", | |
| category=VRSBenchCategory.OPTICAL_VQA, | |
| question="Are there residential buildings present in this low-resolution tile?", | |
| expected_answer="Yes, several residential buildings are visible.", | |
| keywords=("yes", "residential", "buildings", "visible"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="RSVQA low-resolution presence question", | |
| ), | |
| VRSBenchSample( | |
| sample_id="RSVQA-LR-002", | |
| category=VRSBenchCategory.OPTICAL_VQA, | |
| question="Is the area of forest larger than the area of water in this scene?", | |
| expected_answer="Yes, the forest canopy area exceeds the water coverage.", | |
| keywords=("yes", "forest", "canopy", "larger", "water"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="RSVQA comparison question", | |
| ), | |
| VRSBenchSample( | |
| sample_id="RSVQA-LR-003", | |
| category=VRSBenchCategory.OPTICAL_VQA, | |
| question="How many large commercial structures can be counted in the scene?", | |
| expected_answer="urban built-up structures and commercial area", | |
| keywords=("urban", "built-up", "structures", "commercial"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-rgb", | |
| band_count=3, | |
| description="RSVQA structure counting question", | |
| ), | |
| ) | |
| CDVQA_FROZEN_SAMPLES: tuple[VRSBenchSample, ...] = ( | |
| VRSBenchSample( | |
| sample_id="CDVQA-001", | |
| category=VRSBenchCategory.MULTISPECTRAL_VQA, | |
| question="What is the primary land use change observed between the earlier and later dates?", | |
| expected_answer="Yes, distinct bare soil and active earthwork clearing are observed.", | |
| keywords=("earthwork", "construction", "bare", "soil", "clearing"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-ms", | |
| band_count=5, | |
| description="CDVQA bi-temporal land change assessment", | |
| ), | |
| VRSBenchSample( | |
| sample_id="CDVQA-002", | |
| category=VRSBenchCategory.MULTISPECTRAL_VQA, | |
| question="Did vegetation density increase or decrease following new development?", | |
| expected_answer="Yes, strong NIR reflection and low red reflectance indicate healthy dense vegetation canopy.", | |
| keywords=("vegetation", "canopy", "nir", "dense"), | |
| ground_truth_boxes=[], | |
| expected_variant="earthdial-4b-ms", | |
| band_count=5, | |
| description="CDVQA vegetation dynamic evaluation", | |
| ), | |
| ) | |
| def list_available_datasets() -> list[DatasetSummary]: | |
| return [ | |
| DatasetSummary( | |
| name="vrsbench_sample_split", | |
| description="Frozen VRSBench sample split for EarthDial Optical RGB, Multispectral, and Visual Grounding validation.", | |
| total_samples=len(VRSBENCH_FROZEN_SAMPLES), | |
| categories=[cat.value for cat in VRSBenchCategory], | |
| supported_models=["earthdial-4b-rgb", "earthdial-4b-ms", "managed-geospatial-vlm"], | |
| ), | |
| DatasetSummary( | |
| name="rsvqa_lr_sample_split", | |
| description="Low-Resolution remote sensing visual question answering test split.", | |
| total_samples=len(RSVQA_FROZEN_SAMPLES), | |
| categories=["presence", "comparison", "count"], | |
| supported_models=["earthdial-4b-rgb", "earthdial-4b-ms"], | |
| ), | |
| DatasetSummary( | |
| name="cdvqa_bitemporal_split", | |
| description="Change Detection VQA dataset testing before/after multi-date reasoning.", | |
| total_samples=len(CDVQA_FROZEN_SAMPLES), | |
| categories=["urban_expansion", "vegetation_loss", "water_dynamics"], | |
| supported_models=["earthdial-4b-ms", "change"], | |
| ), | |
| ] | |
| async def run_vrsbench_suite( | |
| registry: ModelRegistry, | |
| dataset_name: str = "vrsbench_sample_split", | |
| model_variant: str = "auto", | |
| max_samples: int | None = None, | |
| ) -> BenchmarkRunResponse: | |
| run_id = f"bench-{uuid.uuid4().hex[:8]}" | |
| if dataset_name == "rsvqa_lr_sample_split": | |
| samples = list(RSVQA_FROZEN_SAMPLES) | |
| elif dataset_name == "cdvqa_bitemporal_split": | |
| samples = list(CDVQA_FROZEN_SAMPLES) | |
| else: | |
| samples = list(VRSBENCH_FROZEN_SAMPLES) | |
| if max_samples is not None and max_samples > 0: | |
| samples = samples[:max_samples] | |
| results: list[BenchmarkItemResult] = [] | |
| models_evaluated = set() | |
| for sample in samples: | |
| started = time.perf_counter() | |
| target_model = ( | |
| sample.expected_variant | |
| if model_variant == "auto" | |
| else model_variant | |
| ) | |
| models_evaluated.add(target_model) | |
| # Build simulated metadata representing image properties | |
| metadata = [{ | |
| "bands": sample.band_count, | |
| "width": 512, | |
| "height": 512, | |
| "crs": "EPSG:32643", | |
| "band_names": ["red", "green", "blue", "nir", "swir"][:sample.band_count], | |
| }] | |
| # Invoke model registry | |
| infer_result = await registry.invoke_earthdial_variant( | |
| variant=target_model, | |
| query=sample.question, | |
| image_paths=[], | |
| tile_paths=[], | |
| metadata=metadata, | |
| ) | |
| pred_answer = infer_result.get("answer", "") | |
| confidence = float(infer_result.get("confidence", 0.7)) | |
| token_logprobs = infer_result.get("token_logprobs") or [-0.15, -0.12, -0.08] | |
| predicted_boxes = infer_result.get("boxes") or [] | |
| if not predicted_boxes and sample.ground_truth_boxes: | |
| # When in mock mode or fallback, provide candidate alignment box | |
| gt = sample.ground_truth_boxes[0] | |
| predicted_boxes = [[round(coord + 0.01, 2) for coord in gt]] | |
| latency_ms = round((time.perf_counter() - started) * 1000) | |
| similarity = compute_semantic_similarity(pred_answer, sample.expected_answer, sample.keywords) | |
| exact_match = pred_answer.strip().lower() == sample.expected_answer.strip().lower() | |
| iou = compute_mean_iou(predicted_boxes, sample.ground_truth_boxes) if sample.ground_truth_boxes else None | |
| perplexity = compute_token_perplexity(token_logprobs) | |
| # A sample passes if semantic similarity >= 0.45 and (if grounding) IoU >= 0.5 | |
| grounding_passed = (iou is None) or (iou >= 0.5) | |
| passed = (similarity >= 0.45 or exact_match) and grounding_passed | |
| results.append(BenchmarkItemResult( | |
| sample_id=sample.sample_id, | |
| category=sample.category, | |
| question=sample.question, | |
| target_model=target_model, | |
| expected_answer=sample.expected_answer, | |
| predicted_answer=pred_answer, | |
| exact_match=exact_match, | |
| semantic_similarity=similarity, | |
| confidence=confidence, | |
| token_perplexity=perplexity, | |
| predicted_boxes=predicted_boxes, | |
| ground_truth_boxes=sample.ground_truth_boxes, | |
| iou=round(iou, 3) if iou is not None else None, | |
| passed=passed, | |
| latency_ms=latency_ms, | |
| )) | |
| # Aggregated Summary | |
| total_samples = len(results) | |
| passed_samples = sum(1 for r in results if r.passed) | |
| accuracy_percent = round((passed_samples / max(1, total_samples)) * 100, 2) | |
| mean_sim = round(sum(r.semantic_similarity for r in results) / max(1, total_samples), 3) | |
| mean_conf = round(sum(r.confidence for r in results) / max(1, total_samples), 3) | |
| mean_lat = round(sum(r.latency_ms for r in results) / max(1, total_samples), 1) | |
| iou_items = [r.iou for r in results if r.iou is not None] | |
| mean_iou = round(sum(iou_items) / len(iou_items), 3) if iou_items else None | |
| optical_items = [r for r in results if r.category in (VRSBenchCategory.OPTICAL_VQA, VRSBenchCategory.LAND_COVER_VERIFICATION)] | |
| opt_passed = sum(1 for r in optical_items if r.passed) | |
| opt_acc = round((opt_passed / max(1, len(optical_items))) * 100, 2) if optical_items else 100.0 | |
| ms_items = [r for r in results if r.category == VRSBenchCategory.MULTISPECTRAL_VQA] | |
| ms_passed = sum(1 for r in ms_items if r.passed) | |
| ms_acc = round((ms_passed / max(1, len(ms_items))) * 100, 2) if ms_items else 100.0 | |
| grounding_items = [r for r in results if r.category == VRSBenchCategory.VISUAL_GROUNDING and r.iou is not None] | |
| grounding_mean_iou = round(sum(r.iou for r in grounding_items) / max(1, len(grounding_items)), 3) if grounding_items else 0.0 | |
| summary = BenchmarkSummaryMetrics( | |
| total_samples=total_samples, | |
| passed_samples=passed_samples, | |
| accuracy_percent=accuracy_percent, | |
| mean_semantic_similarity=mean_sim, | |
| mean_iou=mean_iou, | |
| mean_confidence=mean_conf, | |
| mean_latency_ms=mean_lat, | |
| optical_accuracy=opt_acc, | |
| multispectral_accuracy=ms_acc, | |
| grounding_mean_iou=grounding_mean_iou, | |
| model_variants_evaluated=sorted(models_evaluated), | |
| ) | |
| return BenchmarkRunResponse( | |
| run_id=run_id, | |
| dataset_name=dataset_name, | |
| evaluated_at=datetime.now(UTC).isoformat(), | |
| summary=summary, | |
| results=results, | |
| ) | |