fimeval 0.1.56__py3-none-any.whl → 0.1.58__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.
@@ -1,4 +1,9 @@
1
+ """
2
+ Author: Supath Dhital
3
+ Date Updated: January 2026
4
+ """
1
5
  import os
6
+ import re
2
7
  import numpy as np
3
8
  from pathlib import Path
4
9
  import geopandas as gpd
@@ -10,16 +15,20 @@ import pandas as pd
10
15
  from rasterio.warp import reproject, Resampling
11
16
  from rasterio.io import MemoryFile
12
17
  from rasterio import features
18
+ from shapely.geometry import shape
13
19
  from rasterio.mask import mask
14
20
 
21
+ os.environ["CHECK_DISK_FREE_SPACE"] = "NO"
22
+
15
23
  import warnings
16
24
 
17
25
  warnings.filterwarnings("ignore", category=rasterio.errors.ShapeSkipWarning)
18
26
 
19
- from .methods import AOI, smallest_extent, convex_hull, get_smallest_raster_path
27
+ from .methods import AOI, convex_hull, smallest_extent, get_smallest_raster_path
20
28
  from .metrics import evaluationmetrics
21
- from .PWBs3 import get_PWB
22
- from ..utilis import MakeFIMsUniform
29
+ from .water_bodies import ExtractPWB
30
+ from ..utilis import MakeFIMsUniform, benchmark_name, find_best_boundary
31
+ from ..setup_benchFIM import ensure_benchmark
23
32
 
24
33
 
25
34
  # giving the permission to the folder
@@ -64,7 +73,7 @@ def fix_permissions(path):
64
73
 
65
74
  # Function for the evalution of the model
66
75
  def evaluateFIM(
67
- benchmark_path, candidate_paths, gdf, folder, method, output_dir, shapefile=None
76
+ benchmark_path, candidate_paths, PWB_Dir, folder, method, output_dir, shapefile=None
68
77
  ):
69
78
  # Lists to store evaluation metrics
70
79
  csi_values = []
@@ -98,20 +107,18 @@ def evaluateFIM(
98
107
 
99
108
  # If method is AOI, and direct shapefile directory is not provided, then it will search for the shapefile in the folder
100
109
  if method.__name__ == "AOI":
101
- # If shapefile is not provided, search in the folder
110
+ # Ubest-matching boundary file, prefer .gpkg from benchFIM downloads
102
111
  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:
112
+ shapefile_path = find_best_boundary(Path(folder), Path(benchmark_path))
113
+ if shapefile_path is None:
112
114
  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."
115
+ f"No boundary file (.gpkg, .shp, .geojson, .kml) found in {folder}. "
116
+ "Either provide a shapefile path or place a boundary file in the folder."
114
117
  )
118
+ shapefile = str(shapefile_path)
119
+ else:
120
+ shapefile = str(shapefile)
121
+
115
122
  # Run AOI with the found or provided shapefile
116
123
  bounding_geom = AOI(benchmark_path, shapefile, save_dir)
117
124
 
@@ -127,8 +134,23 @@ def evaluateFIM(
127
134
  benchmark_nodata = src1.nodata
128
135
  benchmark_crs = src1.crs
129
136
  b_profile = src1.profile
137
+
138
+ #Getting the correct geometry shape and crs to extract PWB
139
+ boundary_shape = shape(bounding_geom[0])
140
+ boundary_gdf = gpd.GeoDataFrame(geometry=[boundary_shape], crs=benchmark_crs)
141
+
142
+ #Proceed the masking
130
143
  out_image1[out_image1 == benchmark_nodata] = 0
131
144
  out_image1 = np.where(out_image1 > 0, 2, 0).astype(np.float32)
145
+
146
+ #If PWB_Dir is provided, use the local PWB shapefile, else download from ArcGIS API
147
+ if PWB_Dir is not None:
148
+ gdf = gpd.read_file(PWB_Dir)
149
+ else:
150
+ #Get the permanent water bodies from ArcGIS REST API
151
+ pwb_obj = ExtractPWB(boundary = boundary_gdf, save = False)
152
+ gdf = pwb_obj.gdf
153
+
132
154
  gdf = gdf.to_crs(benchmark_crs)
133
155
  shapes1 = [
134
156
  geom for geom in gdf.geometry if geom is not None and not geom.is_empty
@@ -277,8 +299,8 @@ def evaluateFIM(
277
299
  out_transform1,
278
300
  )
279
301
  merged = out_image1 + out_image2_resized
280
- merged[merged==7] = 5
281
-
302
+ merged[merged == 7] = 5
303
+
282
304
  # Get Evaluation Metrics
283
305
  (
284
306
  unique_values,
@@ -392,19 +414,18 @@ def safe_delete_folder(folder_path):
392
414
 
393
415
  def EvaluateFIM(
394
416
  main_dir,
395
- method_name,
396
- output_dir,
417
+ method_name=None,
418
+ output_dir=None,
397
419
  PWB_dir=None,
398
420
  shapefile_dir=None,
399
421
  target_crs=None,
400
422
  target_resolution=None,
423
+ benchmark_dict=None,
401
424
  ):
425
+ if output_dir is None:
426
+ output_dir = os.path.join(os.getcwd(), "Evaluation_Results")
427
+
402
428
  main_dir = Path(main_dir)
403
- # Read the permanent water bodies
404
- if PWB_dir is None:
405
- gdf = get_PWB()
406
- else:
407
- gdf = gpd.read_file(PWB_dir)
408
429
 
409
430
  # Grant the permission to the main directory
410
431
  fix_permissions(main_dir)
@@ -414,32 +435,55 @@ def EvaluateFIM(
414
435
  benchmark_path = None
415
436
  candidate_path = []
416
437
 
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)
438
+ for tif_file in tif_files:
439
+ if benchmark_name(tif_file):
440
+ benchmark_path = tif_file
441
+ else:
442
+ candidate_path.append(tif_file)
443
+
444
+ if benchmark_path and candidate_path:
445
+ if method_name is None:
446
+ local_method = "AOI"
423
447
 
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
448
+ # For single case, if user have explicitly send boundary, use that, else use the boundary from the benchmark FIM evaluation
449
+ if shapefile_dir is not None:
450
+ local_shapefile = shapefile_dir
428
451
  else:
429
- candidate_path.append(tif_file)
452
+ boundary = find_best_boundary(folder_dir, benchmark_path)
453
+ if boundary is None:
454
+ print(
455
+ f"Skipping {folder_dir.name}: no boundary file found "
456
+ f"and method_name is None (auto-AOI)."
457
+ )
458
+ return
459
+ local_shapefile = str(boundary)
460
+ else:
461
+ local_method = method_name
462
+ local_shapefile = shapefile_dir
430
463
 
431
- if benchmark_path and candidate_path:
432
464
  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")
465
+ try:
466
+ Metrics = evaluateFIM(
467
+ benchmark_path,
468
+ candidate_path,
469
+ PWB_dir,
470
+ folder_dir,
471
+ local_method,
472
+ output_dir,
473
+ shapefile=local_shapefile,
474
+ )
475
+
476
+ # Print results in structured table format with 3 decimal points
477
+ candidate_names = [os.path.splitext(os.path.basename(path))[0] for path in candidate_path]
478
+ df_display = pd.DataFrame.from_dict(Metrics, orient='index')
479
+ df_display.columns = candidate_names
480
+ df_display.reset_index(inplace=True)
481
+ df_display.rename(columns={'index': 'Metrics'}, inplace=True)
482
+ print("\n")
483
+ print(df_display.to_string(index=False, float_format='%.3f'))
484
+ print("\n")
485
+ except Exception as e:
486
+ print(f"Error evaluating {folder_dir.name}: {e}")
443
487
  else:
444
488
  print(
445
489
  f"Skipping {folder_dir.name} as it doesn't have a valid benchmark and candidate configuration."
@@ -448,34 +492,51 @@ def EvaluateFIM(
448
492
  # Check if main_dir directly contains tif files
449
493
  TIFFfiles_main_dir = list(main_dir.glob("*.tif"))
450
494
  if TIFFfiles_main_dir:
451
- MakeFIMsUniform(
452
- main_dir, target_crs=target_crs, target_resolution=target_resolution
495
+
496
+ # Ensure benchmark is present if needed
497
+ TIFFfiles_main_dir = ensure_benchmark(
498
+ main_dir, TIFFfiles_main_dir, benchmark_dict
453
499
  )
454
500
 
455
- # processing folder
456
501
  processing_folder = main_dir / "processing"
457
- TIFFfiles = list(processing_folder.glob("*.tif"))
502
+ try:
503
+ MakeFIMsUniform(
504
+ main_dir, target_crs=target_crs, target_resolution=target_resolution
505
+ )
458
506
 
459
- process_TIFF(TIFFfiles, main_dir)
460
- safe_delete_folder(processing_folder)
507
+ # processing folder
508
+ TIFFfiles = list(processing_folder.glob("*.tif"))
509
+
510
+ process_TIFF(TIFFfiles, main_dir)
511
+ except Exception as e:
512
+ print(f"Error processing {main_dir}: {e}")
513
+ finally:
514
+ safe_delete_folder(processing_folder)
461
515
  else:
462
516
  for folder in main_dir.iterdir():
463
517
  if folder.is_dir():
464
518
  tif_files = list(folder.glob("*.tif"))
465
519
 
466
520
  if tif_files:
467
- MakeFIMsUniform(
468
- folder,
469
- target_crs=target_crs,
470
- target_resolution=target_resolution,
471
- )
472
-
473
521
  processing_folder = folder / "processing"
474
- TIFFfiles = list(processing_folder.glob("*.tif"))
522
+ try:
523
+ # Ensure benchmark is present if needed
524
+ tif_files = ensure_benchmark(folder, tif_files, benchmark_dict)
525
+
526
+ MakeFIMsUniform(
527
+ folder,
528
+ target_crs=target_crs,
529
+ target_resolution=target_resolution,
530
+ )
531
+
532
+ TIFFfiles = list(processing_folder.glob("*.tif"))
475
533
 
476
- process_TIFF(TIFFfiles, folder)
477
- safe_delete_folder(processing_folder)
534
+ process_TIFF(TIFFfiles, folder)
535
+ except Exception as e:
536
+ print(f"Error processing folder {folder.name}: {e}")
537
+ finally:
538
+ safe_delete_folder(processing_folder)
478
539
  else:
479
540
  print(
480
541
  f"Skipping {folder.name} as it doesn't contain any tif files."
481
- )
542
+ )
@@ -101,7 +101,9 @@ def getContingencyMap(raster_path, method_path):
101
101
  base_name = os.path.basename(raster_path).split(".")[0]
102
102
  output_path = os.path.join(plot_dir, f"{base_name}.png")
103
103
  plt.savefig(output_path, dpi=500, bbox_inches="tight")
104
- plt.show()
104
+ plt.show(block=False)
105
+ plt.pause(5.0)
106
+ plt.close()
105
107
 
106
108
 
107
109
  def PrintContingencyMap(main_dir, method_name, out_dir):
@@ -0,0 +1,175 @@
1
+ """
2
+ Author: Supath Dhital
3
+ Date Created: January 2026
4
+
5
+ Description: This module extracts permanent water bodies
6
+ using the ArcGIS REST API and AWS S3 for a given boundary file. The mechanism is using AWS, it retrieve all the US permanent water bodies shapefile from S3 bucket and then clip on the boundary.
7
+
8
+ This FIMeval module now uses the ArcGIS REST API to extract water bodies within a specified boundary. As it query data for only specified boundary, it is more efficient and faster than downloading the entire US water bodies dataset.
9
+ """
10
+
11
+ # import Libraries
12
+ import geopandas as gpd
13
+ import boto3
14
+ import botocore
15
+ import os
16
+ import tempfile
17
+ import requests
18
+ import pandas as pd
19
+ import numpy as np
20
+ import json
21
+ from pathlib import Path
22
+ from typing import Union, Optional
23
+ from shapely.geometry import box
24
+
25
+ #USING ANONYMOUS S3 CLIENT TO ACCESS PUBLIC DATA
26
+ # Initialize an anonymous S3 client
27
+ s3 = boto3.client(
28
+ "s3", config=botocore.config.Config(signature_version=botocore.UNSIGNED)
29
+ )
30
+
31
+ bucket_name = "sdmlab"
32
+ pwb_folder = "PWB/"
33
+
34
+
35
+ def PWB_inS3(s3_client, bucket, prefix):
36
+ """Download all components of a shapefile from S3 into a temporary directory."""
37
+ tmp_dir = tempfile.mkdtemp()
38
+ response = s3_client.list_objects_v2(Bucket=bucket, Prefix=prefix)
39
+ if "Contents" not in response:
40
+ raise ValueError("No files found in the specified S3 folder.")
41
+
42
+ for obj in response["Contents"]:
43
+ file_key = obj["Key"]
44
+ file_name = os.path.basename(file_key)
45
+ if file_name.endswith((".shp", ".shx", ".dbf", ".prj", ".cpg")):
46
+ local_path = os.path.join(tmp_dir, file_name)
47
+ s3_client.download_file(bucket, file_key, local_path)
48
+
49
+ shp_files = [f for f in os.listdir(tmp_dir) if f.endswith(".shp")]
50
+ if not shp_files:
51
+ raise ValueError("No .shp file found after download.")
52
+
53
+ shp_path = os.path.join(tmp_dir, shp_files[0])
54
+ return shp_path
55
+
56
+
57
+ def get_PWB():
58
+ shp_path = PWB_inS3(s3, bucket_name, pwb_folder)
59
+ pwb = gpd.read_file(shp_path)
60
+ return pwb
61
+
62
+
63
+ #USING ARCGIS REST TO ACCESS PUBLIC DATA- More fast
64
+ class ExtractPWB:
65
+ SERVICE_URL = "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_Detailed_Water_Bodies/FeatureServer/0"
66
+
67
+ def __init__(
68
+ self,
69
+ boundary: Union[str, Path, gpd.GeoDataFrame],
70
+ layer: Optional[str] = None,
71
+ output_dir: Optional[Union[str, Path]] = None,
72
+ save: bool = True,
73
+ output_filename: str = "permanent_water.gpkg"
74
+ ):
75
+ self.boundary_gdf = self._load_boundary(boundary, layer)
76
+ self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "PWBOutputs"
77
+
78
+ # We store the final result in self.gdf so it can be accessed after init
79
+ self.gdf = self.extract(save=save, output_filename=output_filename)
80
+
81
+ def _load_boundary(self, boundary, layer):
82
+ if isinstance(boundary, gpd.GeoDataFrame):
83
+ gdf = boundary.copy()
84
+ else:
85
+ kwargs = {"layer": layer} if layer else {}
86
+ gdf = gpd.read_file(boundary, **kwargs)
87
+ return gdf.to_crs("EPSG:4326") if gdf.crs != "EPSG:4326" else gdf
88
+
89
+ def _get_query_envelopes(self, threshold=1.0):
90
+ xmin, ymin, xmax, ymax = self.boundary_gdf.total_bounds
91
+ cols = list(np.arange(xmin, xmax, threshold)) + [xmax]
92
+ rows = list(np.arange(ymin, ymax, threshold)) + [ymax]
93
+
94
+ grid = []
95
+ for i in range(len(cols)-1):
96
+ for j in range(len(rows)-1):
97
+ grid.append({
98
+ "xmin": cols[i], "ymin": rows[j],
99
+ "xmax": cols[i+1], "ymax": rows[j+1],
100
+ "spatialReference": {"wkid": 4326}
101
+ })
102
+ return grid
103
+
104
+ def extract(self, save: bool = True, output_filename: str = "permanent_water.gpkg", verbose: bool = True) -> gpd.GeoDataFrame:
105
+ all_features = []
106
+ query_url = f"{self.SERVICE_URL}/query"
107
+ envelopes = self._get_query_envelopes()
108
+
109
+ permanent_filter = "FTYPE IN ('Lake/Pond', 'Stream/River', 'Reservoir', 'Canal/Ditch')"
110
+
111
+ for env_idx, env in enumerate(envelopes):
112
+ offset = 0
113
+ limit = 1000
114
+
115
+ while True:
116
+ payload = {
117
+ "f": "geojson",
118
+ "where": permanent_filter,
119
+ "geometry": json.dumps(env),
120
+ "geometryType": "esriGeometryEnvelope",
121
+ "inSR": "4326",
122
+ "spatialRel": "esriSpatialRelIntersects",
123
+ "outFields": "NAME,FTYPE,FCODE,SQKM",
124
+ "returnGeometry": "true",
125
+ "outSR": "4326",
126
+ "resultOffset": offset,
127
+ "resultRecordCount": limit
128
+ }
129
+
130
+ try:
131
+ response = requests.post(query_url, data=payload, timeout=60)
132
+ response.raise_for_status()
133
+ data = response.json()
134
+
135
+ features = data.get("features", [])
136
+ if not features:
137
+ break
138
+
139
+ batch_gdf = gpd.GeoDataFrame.from_features(features, crs="EPSG:4326")
140
+ all_features.append(batch_gdf)
141
+
142
+ if verbose and offset > 0:
143
+ print(f" Grid {env_idx}: Paginated to offset {offset}...")
144
+
145
+ if len(features) < limit:
146
+ break
147
+
148
+ offset += limit
149
+
150
+ except Exception as e:
151
+ print(f"Error at grid {env_idx}, offset {offset}: {e}")
152
+ break
153
+
154
+ if not all_features:
155
+ print("No water bodies found.")
156
+ return gpd.GeoDataFrame()
157
+
158
+ # Combine and Deduplicate
159
+ full_gdf = pd.concat(all_features, ignore_index=True)
160
+ full_gdf = gpd.GeoDataFrame(full_gdf, crs="EPSG:4326")
161
+ full_gdf = full_gdf.loc[full_gdf.geometry.to_wkt().drop_duplicates().index]
162
+
163
+ # Clip to exact AOI
164
+ final_gdf = gpd.clip(full_gdf, self.boundary_gdf)
165
+
166
+ # Conditional Saving
167
+ if save:
168
+ self.output_dir.mkdir(parents=True, exist_ok=True)
169
+ output_path = self.output_dir / output_filename
170
+ final_gdf.to_file(output_path, driver="GPKG")
171
+ if verbose: print(f"Saved {len(final_gdf)} features to {output_path}")
172
+ else:
173
+ if verbose: print(f"PWB Extraction complete.")
174
+
175
+ return final_gdf
fimeval/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  from .ContingencyMap.evaluationFIM import EvaluateFIM
3
3
  from .ContingencyMap.printcontingency import PrintContingencyMap
4
4
  from .ContingencyMap.plotevaluationmetrics import PlotEvaluationMetrics
5
- from .ContingencyMap.PWBs3 import get_PWB
5
+ from .ContingencyMap.water_bodies import get_PWB, ExtractPWB
6
6
 
7
7
  # Utility modules
8
8
  from .utilis import compress_tif_lzw
@@ -10,6 +10,12 @@ 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
+
16
+ # Building Footprint module
17
+ from .BuildingFootprint.arcgis_API import getBuildingFootprint
18
+
13
19
  __all__ = [
14
20
  "EvaluateFIM",
15
21
  "PrintContingencyMap",
@@ -17,4 +23,7 @@ __all__ = [
17
23
  "get_PWB",
18
24
  "EvaluationWithBuildingFootprint",
19
25
  "compress_tif_lzw",
26
+ "benchFIMquery",
27
+ "getBuildingFootprint",
28
+ "ExtractPWB",
20
29
  ]
@@ -0,0 +1,41 @@
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
+
8
+ from pathlib import Path
9
+
10
+ from .BenchFIMQuery.access_benchfim import benchFIMquery
11
+ from .utilis import benchmark_name
12
+
13
+
14
+ def ensure_benchmark(folder_dir, tif_files, benchmark_map):
15
+ """
16
+ If no local benchmark is found in `tif_files`, and `folder_dir.name`
17
+ exists in `benchmark_map`, download it into this folder using benchFIMquery.
18
+ Returns an updated list of tif files.
19
+ """
20
+ folder_dir = Path(folder_dir)
21
+
22
+ # If a benchmark/BM tif is already present, just use existing files
23
+ has_benchmark = any(benchmark_name(f) for f in tif_files)
24
+ if has_benchmark or not benchmark_map:
25
+ return tif_files
26
+
27
+ # If folder not in mapping, do nothing
28
+ folder_key = folder_dir.name
29
+ file_name = benchmark_map.get(folder_key)
30
+ if not file_name:
31
+ return tif_files
32
+
33
+ # Download benchmark FIM by filename into this folder
34
+ benchFIMquery(
35
+ file_name=file_name,
36
+ download=True,
37
+ out_dir=str(folder_dir),
38
+ )
39
+
40
+ # Return refreshed tif list
41
+ 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,49 @@ 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
+
188
+ # Function to find the best boundary file in the folder if multiple boundary files are present
189
+ def find_best_boundary(folder: Path, benchmark_path: Path):
190
+ """
191
+ Choose the best boundary file in `folder`:
192
+ - prefer .gpkg (from benchFIM downloads),
193
+ - otherwise, pick the file with the most name tokens in common with the benchmark.
194
+ """
195
+ exts = [".gpkg", ".shp", ".geojson", ".kml"]
196
+ candidates = []
197
+ for ext in exts:
198
+ candidates.extend(folder.glob(f"*{ext}"))
199
+
200
+ if not candidates:
201
+ return None
202
+ if len(candidates) == 1:
203
+ print(f"Auto-detected boundary: {candidates[0]}")
204
+ return candidates[0]
205
+
206
+ bench_tokens = set(
207
+ t for t in re.split(r"[_\-\.\s]+", benchmark_path.stem.lower()) if t
208
+ )
209
+
210
+ def score(path: Path):
211
+ name_tokens = set(t for t in re.split(r"[_\-\.\s]+", path.stem.lower()) if t)
212
+ common = len(bench_tokens & name_tokens)
213
+ bonus = 1 if path.suffix.lower() == ".gpkg" else 0
214
+ return (common, bonus)
215
+
216
+ best = max(candidates, key=score)
217
+ print(f"Auto-detected boundary (best match to benchmark): {best}")
218
+ return best
219
+
220
+
221
+ # To test whether the tif is benchmark or not
222
+ def benchmark_name(f: Path) -> bool:
223
+ name = f.stem.lower()
224
+
225
+ # Explicit word
226
+ if "benchmark" in name:
227
+ return True
228
+
229
+ # Treating underscores/dashes/dots as separators and look for a 'bm' token
230
+ tokens = re.split(r"[_\-\.\s]+", name)
231
+ return "bm" in tokens