Spaces:
Running on Zero
Running on Zero
Download scripts/analysis/check_cavity_proximity.py from ageraustine/River_Network: direct link, hf CLI and curl.
- Browser
- Download file 7.85 kB
-
https://huggingface.co/spaces/ageraustine/River_Network/resolve/main/scripts/analysis/check_cavity_proximity.py
- Command line
-
hf download hf://spaces/ageraustine/River_Network/scripts/analysis/check_cavity_proximity.py
-
curl -L -o check_cavity_proximity.py https://huggingface.co/spaces/ageraustine/River_Network/resolve/main/scripts/analysis/check_cavity_proximity.py
7.85 kB
| """ | |
| Real, independent karst cross-check for every real gauge station, using | |
| the actual raw BDCavités data -- not the precomputed distance_to_ | |
| nearest_cavity_km column, which turned out to never have been saved | |
| anywhere in the current reach-graph pipeline (confirmed directly: the | |
| column is absent from both real enriched node files, despite the raw | |
| cavité data itself existing on disk). | |
| Built specifically because the current karst/losing-reach flag | |
| (verified_continuous=False on 3 real edges of La Risle's bétoire | |
| stretch) is a hardcoded list based on real station naming evidence | |
| ("[amont bétoire]"/"[aval bétoire]"), not a systematic scan -- it can | |
| only ever confirm what was already suspected from names, never surface | |
| a DIFFERENT real station that also sits near a real cavity but never | |
| got a "bétoire"-style name. This script is that systematic scan. | |
| Usage: | |
| python -m scripts.analysis.check_cavity_proximity --data-root datasets | |
| """ | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import List, Optional | |
| import numpy as np | |
| import pandas as pd | |
| def _haversine_km_vec(lat1, lon1, lat2, lon2): | |
| """Real haversine distance, vectorized -- same formula used | |
| throughout this project's other real distance computations.""" | |
| R = 6371.0 | |
| lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2]) | |
| dlat, dlon = lat2 - lat1, lon2 - lon1 | |
| a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2 | |
| return R * 2 * np.arcsin(np.sqrt(a)) | |
| def load_real_cavity_coordinates(geojson_path: Path, allowed_types: Optional[List[str]] = None) -> np.ndarray: | |
| """ | |
| Extracts real [lat, lon] pairs directly from the raw BDCavités | |
| GeoJSON -- point features only, skipping anything malformed rather | |
| than crashing the whole load on a single bad real record (large | |
| real government datasets do have occasional genuine data-quality | |
| issues; skip-and-count is more useful here than a hard failure). | |
| allowed_types, when given, filters on the real "type_cavite" field. | |
| Confirmed as a real, necessary filter, not an optional refinement: | |
| an unfiltered first run showed 24 of 27 real stations within 1km of | |
| *some* cavity, with counts in the thousands -- because BDCavités | |
| catalogs every cavity type, and the real breakdown (confirmed | |
| directly against this project's actual file) is dominated by | |
| quarries (carrière, 21,813), civil/military structures (ouvrage | |
| civil/militaire, ~9,900), and cellars (cave, 3,990) -- none | |
| hydrologically meaningful. Only "naturelle" (8,313) corresponds to | |
| genuine natural cavities, the category that could actually indicate | |
| real karst/water-loss connectivity. "indéterminé" (18,349) is | |
| genuinely ambiguous -- could hide real unclassified natural | |
| features or unclassified quarries -- so it's deliberately not | |
| included by default; check separately with it included rather than | |
| assume either way. | |
| """ | |
| with open(geojson_path) as f: | |
| data = json.load(f) | |
| coords = [] | |
| n_skipped = 0 | |
| n_filtered = 0 | |
| for feature in data.get("features", []): | |
| geom = feature.get("geometry") | |
| if geom is None or geom.get("type") != "Point": | |
| n_skipped += 1 | |
| continue | |
| if allowed_types is not None: | |
| real_type = feature.get("properties", {}).get("type_cavite") | |
| if real_type not in allowed_types: | |
| n_filtered += 1 | |
| continue | |
| lon, lat = geom["coordinates"][0], geom["coordinates"][1] | |
| coords.append((lat, lon)) | |
| filter_msg = f", {n_filtered} filtered out by type" if allowed_types is not None else "" | |
| print(f"Loaded {len(coords)} real cavity point(s) from {geojson_path.name} " | |
| f"({n_skipped} non-point/malformed feature(s) skipped{filter_msg})") | |
| return np.array(coords) | |
| def check_cavity_proximity(stations_df: pd.DataFrame, cavity_coords: np.ndarray, radius_km: float = 20.0) -> pd.DataFrame: | |
| """ | |
| Real nearest-cavity distance and real cavity count within radius_km, | |
| per real station -- same two metrics documented for the (never | |
| actually saved) distance_to_nearest_cavity_km / n_cavities_within_ | |
| 20km features, computed fresh here directly against the real raw | |
| data. | |
| """ | |
| cav_lat, cav_lon = cavity_coords[:, 0], cavity_coords[:, 1] | |
| results = [] | |
| for _, row in stations_df.iterrows(): | |
| dists = _haversine_km_vec(row["latitude"], row["longitude"], cav_lat, cav_lon) | |
| nearest_km = float(dists.min()) | |
| n_within_radius = int((dists <= radius_km).sum()) | |
| results.append({ | |
| "station_code": row["station_code"], | |
| "distance_to_nearest_cavity_km": nearest_km, | |
| "n_cavities_within_20km": n_within_radius, | |
| }) | |
| return pd.DataFrame(results).sort_values("distance_to_nearest_cavity_km") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Real cavity-proximity cross-check for every real gauge station") | |
| parser.add_argument("--data-root", type=Path, default=Path("datasets")) | |
| parser.add_argument("--stations-path", type=Path, default=None) | |
| parser.add_argument("--close-threshold-km", type=float, default=1.0, | |
| help="Real distance below which a station is flagged as plausibly karst-connected.") | |
| args = parser.parse_args() | |
| stations_path = args.stations_path or (args.data_root / "station_elevations.csv") | |
| stations_df = pd.read_csv(stations_path) | |
| geojson_path = args.data_root / "bdcavites" / "cavite_localisee.geojson" | |
| if not geojson_path.exists(): | |
| print(f"No real cavity file found at {geojson_path} -- run " | |
| f"scripts.data_acquisition.download_bdcavites first.") | |
| return | |
| # Two real, confirmed-necessary filtered runs, not one -- an | |
| # unfiltered check showed 24/27 real stations within 1km of *some* | |
| # cavity (counts in the thousands), because BDCavités catalogs | |
| # every cavity type and quarries/civil structures/cellars dominate | |
| # the real breakdown. "naturelle" is the conservative, defensible | |
| # filter; "naturelle + indéterminé" is a broader one that also | |
| # captures any real natural feature that was never properly typed | |
| # -- reported side by side rather than picking one and hiding the | |
| # other's disagreement. | |
| for label, allowed_types in [ | |
| ("naturelle only (conservative)", ["naturelle"]), | |
| ("naturelle + indéterminé (broader)", ["naturelle", "indéterminé"]), | |
| ]: | |
| print() | |
| print("=" * 70) | |
| print(f"Filter: {label}") | |
| print("=" * 70) | |
| cavity_coords = load_real_cavity_coordinates(geojson_path, allowed_types=allowed_types) | |
| if len(cavity_coords) == 0: | |
| print("Zero real cavities matched this filter -- nothing to check against.") | |
| continue | |
| result_df = check_cavity_proximity(stations_df, cavity_coords) | |
| print(result_df.to_string(index=False)) | |
| close = result_df[result_df["distance_to_nearest_cavity_km"] < args.close_threshold_km] | |
| print() | |
| if len(close) > 0: | |
| print(f"Station(s) within {args.close_threshold_km}km of a real, filtered cavity " | |
| f"(cross-check against the known bétoire stations -- H605641101, H605022010, " | |
| f"H605641401, H605641201 -- anything else here is a real, new candidate):") | |
| print(close.to_string(index=False)) | |
| else: | |
| print(f"No real station within {args.close_threshold_km}km of a real, filtered cavity.") | |
| output_path = args.data_root / f"cavity_proximity_check_{allowed_types[0]}.csv" | |
| result_df.to_csv(output_path, index=False) | |
| print(f"Saved to {output_path}") | |
| print(f"\nSaved to {output_path}") | |
| if __name__ == "__main__": | |
| main() |