fimeval 0.1.56__py3-none-any.whl → 0.1.57__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ """
2
+ This utility function contains how to retrieve all the necessary metadata of benchmark FIM
3
+ from the s3 bucket during benchmark FIM querying.
4
+
5
+ Authors: Supath Dhital, sdhital@crimson.ua.edu
6
+ Updated date: 25 Nov, 2025
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import os, re, json, datetime as dt
11
+ from typing import List, Dict, Any, Optional
12
+
13
+ import urllib.parse
14
+ import boto3
15
+ from botocore import UNSIGNED
16
+ from botocore.config import Config
17
+
18
+ # constants
19
+ BUCKET = "sdmlab"
20
+ CATALOG_KEY = (
21
+ "FIM_Database/FIM_Viz/catalog_core.json" # Path of the json file in the s3 bucket
22
+ )
23
+
24
+ # s3 client
25
+ _S3 = boto3.client("s3", config=Config(signature_version=UNSIGNED))
26
+
27
+
28
+ # helpers for direct S3 file links
29
+ def s3_http_url(bucket: str, key: str) -> str:
30
+ """Build a public-style S3 HTTPS URL."""
31
+ return f"https://{bucket}.s3.amazonaws.com/{urllib.parse.quote(key, safe='/')}"
32
+
33
+
34
+ # utils
35
+ _YMD_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
36
+ _YMD_COMPACT_RE = re.compile(r"^\d{8}$")
37
+ _YMDH_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}$")
38
+ _YMDHMS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$")
39
+
40
+
41
+ def _normalize_user_dt(s: str) -> str:
42
+ s = s.strip()
43
+ s = s.replace("/", "-")
44
+ s = re.sub(r"\s+", " ", s)
45
+ return s
46
+
47
+
48
+ def _to_date(s: str) -> dt.date:
49
+ s = _normalize_user_dt(s)
50
+ if _YMD_COMPACT_RE.match(s):
51
+ return dt.datetime.strptime(s, "%Y%m%d").date()
52
+ if _YMD_RE.match(s):
53
+ return dt.date.fromisoformat(s)
54
+ try:
55
+ return dt.datetime.fromisoformat(s).date()
56
+ except Exception:
57
+ m = re.match(r"^(\d{4}-\d{2}-\d{2})[ T](\d{2})$", s)
58
+ if m:
59
+ return dt.datetime.fromisoformat(f"{m.group(1)} {m.group(2)}:00:00").date()
60
+ raise ValueError(f"Bad date format: {s}")
61
+
62
+
63
+ def _to_hour_or_none(s: str) -> Optional[int]:
64
+ s = _normalize_user_dt(s)
65
+ if _YMD_RE.match(s) or _YMD_COMPACT_RE.match(s):
66
+ return None
67
+ m = re.match(r"^\d{4}-\d{2}-\d{2}[ T](\d{2})$", s)
68
+ if m:
69
+ return int(m.group(1))
70
+ try:
71
+ dt_obj = dt.datetime.fromisoformat(s)
72
+ return dt_obj.hour
73
+ except Exception:
74
+ m2 = re.match(r"^\d{4}-\d{2}-\d{2}T(\d{2})$", s)
75
+ if m2:
76
+ return int(m2.group(1))
77
+ return None
78
+
79
+
80
+ def _record_day(rec: Dict[str, Any]) -> Optional[dt.date]:
81
+ ymd = rec.get("date_ymd")
82
+ if isinstance(ymd, str):
83
+ try:
84
+ return dt.date.fromisoformat(ymd)
85
+ except Exception:
86
+ pass
87
+ raw = rec.get("date_of_flood")
88
+ if isinstance(raw, str) and len(raw) >= 8:
89
+ try:
90
+ return dt.datetime.strptime(raw[:8], "%Y%m%d").date()
91
+ except Exception:
92
+ return None
93
+ return None
94
+
95
+
96
+ def _record_hour_or_none(rec: Dict[str, Any]) -> Optional[int]:
97
+ raw = rec.get("date_of_flood")
98
+ if isinstance(raw, str) and "T" in raw and len(raw) >= 11:
99
+ try:
100
+ return int(raw.split("T", 1)[1][:2])
101
+ except Exception:
102
+ return None
103
+ return None
104
+
105
+
106
+ # Printing helpers
107
+ def _pretty_date_for_print(rec: Dict[str, Any]) -> str:
108
+ raw = rec.get("date_of_flood")
109
+ if isinstance(raw, str) and "T" in raw and len(raw) >= 11:
110
+ return f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}T{raw.split('T',1)[1][:2]}"
111
+ ymd = rec.get("date_ymd")
112
+ if isinstance(ymd, str) and _YMD_RE.match(ymd):
113
+ return ymd
114
+ if isinstance(raw, str) and len(raw) >= 8:
115
+ return f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}"
116
+ return "unknown"
117
+
118
+
119
+ def _context_str(
120
+ huc8: Optional[str] = None,
121
+ date_input: Optional[str] = None,
122
+ file_name: Optional[str] = None,
123
+ start_date: Optional[str] = None,
124
+ end_date: Optional[str] = None,
125
+ ) -> str:
126
+ """
127
+ Builds a readable context summary for printing headers.
128
+ Example outputs:
129
+ - "HUC 12090301"
130
+ - "HUC 12090301, date '2017-08-30'"
131
+ - "HUC 12090301, range 2017-08-30 to 2017-09-01"
132
+ - "HUC 12090301, file 'PSS_3_0m_20170830T162251_BM.tif'"
133
+ """
134
+ parts = []
135
+ if huc8:
136
+ parts.append(f"HUC {huc8}")
137
+ if date_input:
138
+ parts.append(f"date '{date_input}'")
139
+ if start_date or end_date:
140
+ if start_date and end_date:
141
+ parts.append(f"range {start_date} to {end_date}")
142
+ elif start_date:
143
+ parts.append(f"from {start_date}")
144
+ elif end_date:
145
+ parts.append(f"until {end_date}")
146
+ if file_name:
147
+ parts.append(f"file '{file_name}'")
148
+
149
+ return ", ".join(parts) if parts else "your filters"
150
+
151
+
152
+ def format_records_for_print(
153
+ records: List[Dict[str, Any]], context: Optional[str] = None
154
+ ) -> str:
155
+ if not records:
156
+ ctx = context or "your filters"
157
+ return f"Benchmark FIMs were not matched for {ctx}."
158
+
159
+ header = (
160
+ f"Following are the available benchmark data for {context}:\n"
161
+ if context
162
+ else ""
163
+ )
164
+
165
+ def _is_synthetic_tier_local(r: Dict[str, Any]) -> bool:
166
+ t = str(r.get("tier") or r.get("quality") or "").lower()
167
+ return "tier_4" in t or t.strip() == "4"
168
+
169
+ def _return_period_text_local(r: Dict[str, Any]) -> str:
170
+ rp = (
171
+ r.get("return_period")
172
+ or r.get("return_period_yr")
173
+ or r.get("rp")
174
+ or r.get("rp_years")
175
+ )
176
+ if rp is None:
177
+ return "synthetic flow (return period unknown)"
178
+ try:
179
+ rp_int = int(float(str(rp).strip().replace("yr", "").replace("-year", "")))
180
+ return f"{rp_int}-year synthetic flow"
181
+ except Exception:
182
+ return f"{rp} synthetic flow"
183
+
184
+ blocks: List[str] = []
185
+ for r in records:
186
+ tier = r.get("tier") or r.get("quality") or "Unknown"
187
+ res = r.get("resolution_m")
188
+ res_txt = f"{res}m" if res is not None else "NA"
189
+ fname = r.get("file_name") or "NA"
190
+
191
+ # Build lines with Tier-aware event text
192
+ lines = [f"Data Tier: {tier}"]
193
+ if _is_synthetic_tier_local(r):
194
+ lines.append(f"Return Period: {_return_period_text_local(r)}")
195
+ else:
196
+ date_str = _pretty_date_for_print(r)
197
+ lines.append(f"Benchmark FIM date: {date_str}")
198
+
199
+ lines.extend([
200
+ f"Spatial Resolution: {res_txt}",
201
+ f"Benchmark FIM raster name in DB: {fname}",
202
+ ])
203
+ blocks.append("\n".join(lines))
204
+
205
+ return (header + "\n\n".join(blocks)).strip()
206
+
207
+ # S3 and json catalog
208
+ def load_catalog_core() -> Dict[str, Any]:
209
+ obj = _S3.get_object(Bucket=BUCKET, Key=CATALOG_KEY)
210
+ return json.loads(obj["Body"].read().decode("utf-8", "replace"))
211
+
212
+
213
+ def _list_prefix(prefix: str) -> List[str]:
214
+ keys: List[str] = []
215
+ paginator = _S3.get_paginator("list_objects_v2")
216
+ for page in paginator.paginate(Bucket=BUCKET, Prefix=prefix):
217
+ for obj in page.get("Contents", []) or []:
218
+ keys.append(obj["Key"])
219
+ return keys
220
+
221
+
222
+ def _download(bucket: str, key: str, dest_path: str) -> str:
223
+ os.makedirs(os.path.dirname(dest_path), exist_ok=True)
224
+ _S3.download_file(bucket, key, dest_path)
225
+ return dest_path
226
+
227
+ # Get the files from s3 bucket
228
+ def _folder_from_record(rec: Dict[str, Any]) -> str:
229
+ s3_key = rec.get("s3_key")
230
+ if not s3_key or "/" not in s3_key:
231
+ raise ValueError("Record lacks s3_key to derive folder")
232
+ return s3_key.rsplit("/", 1)[0] + "/"
233
+
234
+
235
+ def _tif_key_from_record(rec: Dict[str, Any]) -> Optional[str]:
236
+ tif_url = rec.get("tif_url")
237
+ if isinstance(tif_url, str) and ".amazonaws.com/" in tif_url:
238
+ return tif_url.split(".amazonaws.com/", 1)[1]
239
+ fname = rec.get("file_name")
240
+ if not fname:
241
+ return None
242
+ return _folder_from_record(rec) + fname
243
+
244
+ #Download that tif and the boundary file --> need to add building footprint automation as well.
245
+ def download_fim_assets(record: Dict[str, Any], dest_dir: str) -> Dict[str, Any]:
246
+ """
247
+ Download the .tif (if present) and any .gpkg from the record's folder to dest_dir.
248
+ """
249
+ os.makedirs(dest_dir, exist_ok=True)
250
+ out = {"tif": None, "gpkg_files": []}
251
+
252
+ # TIF
253
+ tif_key = _tif_key_from_record(record)
254
+ if tif_key:
255
+ local = os.path.join(dest_dir, os.path.basename(tif_key))
256
+ if not os.path.exists(local):
257
+ _download(BUCKET, tif_key, local)
258
+ out["tif"] = local
259
+
260
+ # GPKGs (list folder)
261
+ folder = _folder_from_record(record)
262
+ for key in _list_prefix(folder):
263
+ if key.lower().endswith(".gpkg"):
264
+ local = os.path.join(dest_dir, os.path.basename(key))
265
+ if not os.path.exists(local):
266
+ _download(BUCKET, key, local)
267
+ out["gpkg_files"].append(local)
268
+
269
+ return out
@@ -130,3 +130,5 @@ def BuildingFootprintwithISO(countryISO, ROI, out_dir, geeprojectID=None):
130
130
  getBuildingFootprintSpark(
131
131
  countryISO, ROI, out_dir, tile_size=0.05, projectID=geeprojectID
132
132
  )
133
+
134
+ BuildingFootprintwithISO("USA", "/Users/supath/Downloads/S1A_9_6m_20190530T23573_910244W430506N_AOI.gpkg", "/Users/supath/Downloads/AOI", geeprojectID="supathdh")
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import numpy as np
3
4
  from pathlib import Path
4
5
  import geopandas as gpd
@@ -12,6 +13,8 @@ from rasterio.io import MemoryFile
12
13
  from rasterio import features
13
14
  from rasterio.mask import mask
14
15
 
16
+ os.environ["CHECK_DISK_FREE_SPACE"] = "NO"
17
+
15
18
  import warnings
16
19
 
17
20
  warnings.filterwarnings("ignore", category=rasterio.errors.ShapeSkipWarning)
@@ -19,7 +22,8 @@ warnings.filterwarnings("ignore", category=rasterio.errors.ShapeSkipWarning)
19
22
  from .methods import AOI, smallest_extent, convex_hull, get_smallest_raster_path
20
23
  from .metrics import evaluationmetrics
21
24
  from .PWBs3 import get_PWB
22
- from ..utilis import MakeFIMsUniform
25
+ from ..utilis import MakeFIMsUniform, benchmark_name, find_best_boundary
26
+ from ..setup_benchFIM import ensure_benchmark
23
27
 
24
28
 
25
29
  # giving the permission to the folder
@@ -98,20 +102,18 @@ def evaluateFIM(
98
102
 
99
103
  # If method is AOI, and direct shapefile directory is not provided, then it will search for the shapefile in the folder
100
104
  if method.__name__ == "AOI":
101
- # If shapefile is not provided, search in the folder
105
+ # Ubest-matching boundary file, prefer .gpkg from benchFIM downloads
102
106
  if shapefile is None:
103
- for ext in (".shp", ".gpkg", ".geojson", ".kml"):
104
- for file in os.listdir(folder):
105
- if file.lower().endswith(ext):
106
- shapefile = os.path.join(folder, file)
107
- print(f"Auto-detected shapefile: {shapefile}")
108
- break
109
- if shapefile:
110
- break
111
- if shapefile is None:
107
+ shapefile_path = find_best_boundary(Path(folder), Path(benchmark_path))
108
+ if shapefile_path is None:
112
109
  raise FileNotFoundError(
113
- "No shapefile (.shp, .gpkg, .geojson, .kml) found in the folder and none provided. Either provide a shapefile directory or put shapefile inside folder directory."
110
+ f"No boundary file (.gpkg, .shp, .geojson, .kml) found in {folder}. "
111
+ "Either provide a shapefile path or place a boundary file in the folder."
114
112
  )
113
+ shapefile = str(shapefile_path)
114
+ else:
115
+ shapefile = str(shapefile)
116
+
115
117
  # Run AOI with the found or provided shapefile
116
118
  bounding_geom = AOI(benchmark_path, shapefile, save_dir)
117
119
 
@@ -277,8 +279,8 @@ def evaluateFIM(
277
279
  out_transform1,
278
280
  )
279
281
  merged = out_image1 + out_image2_resized
280
- merged[merged==7] = 5
281
-
282
+ merged[merged == 7] = 5
283
+
282
284
  # Get Evaluation Metrics
283
285
  (
284
286
  unique_values,
@@ -392,13 +394,17 @@ def safe_delete_folder(folder_path):
392
394
 
393
395
  def EvaluateFIM(
394
396
  main_dir,
395
- method_name,
396
- output_dir,
397
+ method_name=None,
398
+ output_dir=None,
397
399
  PWB_dir=None,
398
400
  shapefile_dir=None,
399
401
  target_crs=None,
400
402
  target_resolution=None,
403
+ benchmark_dict=None,
401
404
  ):
405
+ if output_dir is None:
406
+ output_dir = os.path.join(os.getcwd(), "Evaluation_Results")
407
+
402
408
  main_dir = Path(main_dir)
403
409
  # Read the permanent water bodies
404
410
  if PWB_dir is None:
@@ -414,32 +420,46 @@ def EvaluateFIM(
414
420
  benchmark_path = None
415
421
  candidate_path = []
416
422
 
417
- if len(tif_files) == 2:
418
- for tif_file in tif_files:
419
- if "benchmark" in tif_file.name.lower() or "BM" in tif_file.name:
420
- benchmark_path = tif_file
421
- else:
422
- candidate_path.append(tif_file)
423
+ for tif_file in tif_files:
424
+ if benchmark_name(tif_file):
425
+ benchmark_path = tif_file
426
+ else:
427
+ candidate_path.append(tif_file)
423
428
 
424
- elif len(tif_files) > 2:
425
- for tif_file in tif_files:
426
- if "benchmark" in tif_file.name.lower() or "BM" in tif_file.name:
427
- benchmark_path = tif_file
429
+ if benchmark_path and candidate_path:
430
+ if method_name is None:
431
+ local_method = "AOI"
432
+
433
+ #For single case, if user have explicitly send boundary, use that, else use the boundary from the benchmark FIM evaluation
434
+ if shapefile_dir is not None:
435
+ local_shapefile = shapefile_dir
428
436
  else:
429
- candidate_path.append(tif_file)
437
+ boundary = find_best_boundary(folder_dir, benchmark_path)
438
+ if boundary is None:
439
+ print(
440
+ f"Skipping {folder_dir.name}: no boundary file found "
441
+ f"and method_name is None (auto-AOI)."
442
+ )
443
+ return
444
+ local_shapefile = str(boundary)
445
+ else:
446
+ local_method = method_name
447
+ local_shapefile = shapefile_dir
430
448
 
431
- if benchmark_path and candidate_path:
432
449
  print(f"**Flood Inundation Evaluation of {folder_dir.name}**")
433
- Metrics = evaluateFIM(
434
- benchmark_path,
435
- candidate_path,
436
- gdf,
437
- folder_dir,
438
- method_name,
439
- output_dir,
440
- shapefile_dir,
441
- )
442
- print("\n", Metrics, "\n")
450
+ try:
451
+ Metrics = evaluateFIM(
452
+ benchmark_path,
453
+ candidate_path,
454
+ gdf,
455
+ folder_dir,
456
+ local_method,
457
+ output_dir,
458
+ shapefile=local_shapefile,
459
+ )
460
+ print("\n", Metrics, "\n")
461
+ except Exception as e:
462
+ print(f"Error evaluating {folder_dir.name}: {e}")
443
463
  else:
444
464
  print(
445
465
  f"Skipping {folder_dir.name} as it doesn't have a valid benchmark and candidate configuration."
@@ -448,34 +468,54 @@ def EvaluateFIM(
448
468
  # Check if main_dir directly contains tif files
449
469
  TIFFfiles_main_dir = list(main_dir.glob("*.tif"))
450
470
  if TIFFfiles_main_dir:
451
- MakeFIMsUniform(
452
- main_dir, target_crs=target_crs, target_resolution=target_resolution
471
+
472
+ # Ensure benchmark is present if needed
473
+ TIFFfiles_main_dir = ensure_benchmark(
474
+ main_dir, TIFFfiles_main_dir, benchmark_dict
453
475
  )
454
476
 
455
- # processing folder
456
477
  processing_folder = main_dir / "processing"
457
- TIFFfiles = list(processing_folder.glob("*.tif"))
478
+ try:
479
+ MakeFIMsUniform(
480
+ main_dir, target_crs=target_crs, target_resolution=target_resolution
481
+ )
458
482
 
459
- process_TIFF(TIFFfiles, main_dir)
460
- safe_delete_folder(processing_folder)
483
+ # processing folder
484
+ TIFFfiles = list(processing_folder.glob("*.tif"))
485
+
486
+ process_TIFF(TIFFfiles, main_dir)
487
+ except Exception as e:
488
+ print(f"Error processing {main_dir}: {e}")
489
+ finally:
490
+ safe_delete_folder(processing_folder)
461
491
  else:
462
492
  for folder in main_dir.iterdir():
463
493
  if folder.is_dir():
464
494
  tif_files = list(folder.glob("*.tif"))
465
495
 
466
496
  if tif_files:
467
- MakeFIMsUniform(
468
- folder,
469
- target_crs=target_crs,
470
- target_resolution=target_resolution,
471
- )
497
+ processing_folder = folder / "processing"
498
+ try:
499
+ # Ensure benchmark is present if needed
500
+ tif_files = ensure_benchmark(
501
+ folder, tif_files, benchmark_dict
502
+ )
503
+
504
+ MakeFIMsUniform(
505
+ folder,
506
+ target_crs=target_crs,
507
+ target_resolution=target_resolution,
508
+ )
472
509
 
473
- processing_folder = folder / "processing"
474
- TIFFfiles = list(processing_folder.glob("*.tif"))
510
+ TIFFfiles = list(processing_folder.glob("*.tif"))
475
511
 
476
- process_TIFF(TIFFfiles, folder)
477
- safe_delete_folder(processing_folder)
512
+ process_TIFF(TIFFfiles, folder)
513
+ except Exception as e:
514
+ print(f"Error processing folder {folder.name}: {e}")
515
+ finally:
516
+ safe_delete_folder(processing_folder)
478
517
  else:
479
518
  print(
480
519
  f"Skipping {folder.name} as it doesn't contain any tif files."
481
520
  )
521
+
fimeval/__init__.py CHANGED
@@ -10,6 +10,9 @@ from .utilis import compress_tif_lzw
10
10
  # Evaluation with Building foorprint module
11
11
  from .BuildingFootprint.evaluationwithBF import EvaluationWithBuildingFootprint
12
12
 
13
+ #Access benchmark FIM module
14
+ from .BenchFIMQuery.access_benchfim import benchFIMquery
15
+
13
16
  __all__ = [
14
17
  "EvaluateFIM",
15
18
  "PrintContingencyMap",
@@ -17,4 +20,5 @@ __all__ = [
17
20
  "get_PWB",
18
21
  "EvaluationWithBuildingFootprint",
19
22
  "compress_tif_lzw",
23
+ "benchFIMquery",
20
24
  ]
@@ -0,0 +1,39 @@
1
+ """
2
+ This code setup all the case folders whether it has valid benchmark FIM/ which benchmark need to access from catalog and so on.
3
+ Basically It will do everything before going into the actual evaluation process.
4
+ Author: Supath Dhital
5
+ Date updated: 25 Nov, 2025
6
+ """
7
+ from pathlib import Path
8
+
9
+ from .BenchFIMQuery.access_benchfim import benchFIMquery
10
+ from .utilis import benchmark_name
11
+
12
+ def ensure_benchmark(folder_dir, tif_files, benchmark_map):
13
+ """
14
+ If no local benchmark is found in `tif_files`, and `folder_dir.name`
15
+ exists in `benchmark_map`, download it into this folder using benchFIMquery.
16
+ Returns an updated list of tif files.
17
+ """
18
+ folder_dir = Path(folder_dir)
19
+
20
+ # If a benchmark/BM tif is already present, just use existing files
21
+ has_benchmark = any(benchmark_name(f) for f in tif_files)
22
+ if has_benchmark or not benchmark_map:
23
+ return tif_files
24
+
25
+ # If folder not in mapping, do nothing
26
+ folder_key = folder_dir.name
27
+ file_name = benchmark_map.get(folder_key)
28
+ if not file_name:
29
+ return tif_files
30
+
31
+ # Download benchmark FIM by filename into this folder
32
+ benchFIMquery(
33
+ file_name=file_name,
34
+ download=True,
35
+ out_dir=str(folder_dir),
36
+ )
37
+
38
+ # Return refreshed tif list
39
+ return list(folder_dir.glob("*.tif"))
fimeval/utilis.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import shutil
3
4
  import pyproj
4
5
  import rasterio
@@ -182,3 +183,51 @@ def MakeFIMsUniform(fim_dir, target_crs=None, target_resolution=None):
182
183
  resample_to_resolution(str(src_path), coarsest_x, coarsest_y)
183
184
  else:
184
185
  print("All rasters already have the same resolution. No resampling needed.")
186
+
187
+ #Function to find the best boundary file in the folder if multiple boundary files are present
188
+ def find_best_boundary(folder: Path, benchmark_path: Path):
189
+ """
190
+ Choose the best boundary file in `folder`:
191
+ - prefer .gpkg (from benchFIM downloads),
192
+ - otherwise, pick the file with the most name tokens in common with the benchmark.
193
+ """
194
+ exts = [".gpkg", ".shp", ".geojson", ".kml"]
195
+ candidates = []
196
+ for ext in exts:
197
+ candidates.extend(folder.glob(f"*{ext}"))
198
+
199
+ if not candidates:
200
+ return None
201
+ if len(candidates) == 1:
202
+ print(f"Auto-detected boundary: {candidates[0]}")
203
+ return candidates[0]
204
+
205
+ bench_tokens = set(
206
+ t for t in re.split(r"[_\-\.\s]+", benchmark_path.stem.lower()) if t
207
+ )
208
+
209
+ def score(path: Path):
210
+ name_tokens = set(
211
+ t for t in re.split(r"[_\-\.\s]+", path.stem.lower()) if t
212
+ )
213
+ common = len(bench_tokens & name_tokens)
214
+ bonus = 1 if path.suffix.lower() == ".gpkg" else 0
215
+ return (common, bonus)
216
+
217
+ best = max(candidates, key=score)
218
+ print(f"Auto-detected boundary (best match to benchmark): {best}")
219
+ return best
220
+
221
+
222
+ #To test whether the tif is benchmark or not
223
+ def benchmark_name(f: Path) -> bool:
224
+ name = f.stem.lower()
225
+
226
+ # Explicit word
227
+ if "benchmark" in name:
228
+ return True
229
+
230
+ # Treating underscores/dashes/dots as separators and look for a 'bm' token
231
+ tokens = re.split(r"[_\-\.\s]+", name)
232
+ return "bm" in tokens
233
+