Eishaan's picture
Add KML terrain and soil analysis layers
2f91d7e
Raw
History Blame Contribute Delete
6.17 kB
from __future__ import annotations
from typing import Any
from .evidence import make_evidence
from .http_client import get_json
from .models import EvidenceItem, SiteSelection
SOILGRIDS_URL = "https://rest.isric.org/soilgrids/v2.0/properties/query"
def fetch_soil(selection: SiteSelection) -> tuple[dict[str, Any], list[EvidenceItem]]:
if selection.anchor_lat is None or selection.anchor_lon is None:
return {}, [
make_evidence(
category="Soil / ground",
finding="SoilGrids lookup was skipped because no real-world site coordinate is available.",
source_name="SoilGrids / ISRIC",
source_url="https://docs.isric.org/globaldata/soilgrids/",
source_type="global soil model",
resolution_or_scope="not available",
confidence="low",
limitation="Soil lookup needs a WGS84 coordinate.",
design_implication="Use field/geotechnical verification before any soil-related design assumptions.",
verification_needed="Provide a map/KML/GeoJSON boundary or anchor coordinate.",
output_label="professional_verification_required",
)
]
try:
data = get_json(
SOILGRIDS_URL,
{
"lon": selection.anchor_lon,
"lat": selection.anchor_lat,
"property": ["clay", "sand", "silt", "phh2o", "bdod", "soc"],
"depth": "0-5cm",
"value": "mean",
},
timeout=30,
)
values = _extract_soil_values(data)
soil = _interpret_soil(values)
return soil, [
make_evidence(
category="Soil / ground",
finding=_soil_finding(soil),
source_name="SoilGrids / ISRIC",
source_url="https://docs.isric.org/globaldata/soilgrids/",
source_type="global soil model",
resolution_or_scope="anchor coordinate; 0-5 cm modelled topsoil properties",
confidence="low",
limitation="SoilGrids is coarse global model data, not plot-level geotechnical testing or bearing-capacity evidence.",
design_implication=soil.get("design_implication", "Use only as a ground-condition prompt."),
verification_needed="Obtain site-specific geotechnical/professional verification before foundation or excavation decisions.",
output_label="professional_verification_required",
)
]
except Exception as exc: # noqa: BLE001
return {}, [
make_evidence(
category="Soil / ground",
finding="SoilGrids soil properties could not be retrieved.",
source_name="SoilGrids / ISRIC",
source_url="https://docs.isric.org/globaldata/soilgrids/",
source_type="global soil model",
resolution_or_scope="not available",
confidence="low",
limitation=f"SoilGrids request failed: {type(exc).__name__}.",
design_implication="Do not infer soil type from this missing layer.",
verification_needed="Use geotechnical report, soil test, local engineer input, or official soil maps.",
output_label="professional_verification_required",
)
]
def _extract_soil_values(data: dict[str, Any]) -> dict[str, float | None]:
values: dict[str, float | None] = {}
for layer in data.get("properties", {}).get("layers", []):
name = layer.get("name")
depths = layer.get("depths") or []
if not name or not depths:
continue
mean = (depths[0].get("values") or {}).get("mean")
values[name] = float(mean) if mean is not None else None
return values
def _interpret_soil(values: dict[str, float | None]) -> dict[str, Any]:
clay_pct = _gkg_to_pct(values.get("clay"))
sand_pct = _gkg_to_pct(values.get("sand"))
silt_pct = _gkg_to_pct(values.get("silt"))
ph = _ph_value(values.get("phh2o"))
texture = "mixed or uncertain texture"
implication = "Use as a preliminary prompt for soil verification; do not make foundation decisions from this layer."
if clay_pct is not None and clay_pct >= 35:
texture = "clay-heavy topsoil signal"
implication = "Check drainage, shrink-swell behaviour, water retention, and settlement risk with professional testing."
elif sand_pct is not None and sand_pct >= 55:
texture = "sandy topsoil signal"
implication = "Check drainage, erosion, excavation stability, and bearing conditions with professional testing."
elif silt_pct is not None and silt_pct >= 45:
texture = "silt-heavy topsoil signal"
implication = "Check waterlogging, compaction, erosion, and drainage behaviour with professional testing."
return {
"texture_signal": texture,
"clay_pct": clay_pct,
"sand_pct": sand_pct,
"silt_pct": silt_pct,
"ph_h2o": ph,
"bulk_density_raw": values.get("bdod"),
"soc_raw": values.get("soc"),
"design_implication": implication,
"safe_note": "Preliminary SoilGrids model output only; verify with geotechnical/professional sources.",
}
def _soil_finding(soil: dict[str, Any]) -> str:
parts = [soil.get("texture_signal") or "soil texture signal unavailable"]
if soil.get("clay_pct") is not None:
parts.append(f"clay {soil['clay_pct']}%")
if soil.get("sand_pct") is not None:
parts.append(f"sand {soil['sand_pct']}%")
if soil.get("silt_pct") is not None:
parts.append(f"silt {soil['silt_pct']}%")
if soil.get("ph_h2o") is not None:
parts.append(f"pH {soil['ph_h2o']}")
return "SoilGrids preliminary topsoil signal: " + ", ".join(parts) + "."
def _gkg_to_pct(value: float | None) -> float | None:
return round(value / 10, 1) if value is not None else None
def _ph_value(value: float | None) -> float | None:
return round(value / 10, 1) if value is not None else None