| 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: |
| 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 |
|
|