fire2a.cell2fire
Classes and methods auxiliary to using Cell2FireW and its QGIS integration
Currently:
- Writes C2FW's firebreak specification (a.csv file) from a QGIS raster layer
Processes C2FW plain text outputs
- Fire Scars (Grids directories) into rasters and polygons
- Statistics (Intensity, HitRos, ... directories) into rasters
Auxiliary:
- get_scars_files (all together but a bit slower than the next two methods)
- get_scars_indexed (part 1/2)
- group_scars (part 2/2)
Sample Usage:
python -m fire2a.cell2fire -vvv --base-raster ../fuels.asc --authid EPSG:25831 --scar-sample Grids/Grids2/F
orestGrid01.csv --scar-raster scar_raster.tif --scar-poly scar_poly.shp --burn-prob burn_prob.tif --stat-sample RateOfSpread/ROSFile11.asc --stat-raster ros.tif --stat-summary ros_stats.tif
1#!python 2# fmt: off 3""" 4Classes and methods auxiliary to using Cell2FireW and its QGIS integration 5 6Currently: 7* Writes C2FW's firebreak specification (a.csv file) from a QGIS raster layer 8 9Processes C2FW plain text outputs 10* Fire Scars (Grids directories) into rasters and polygons 11* Statistics (Intensity, HitRos, ... directories) into rasters 12 13Auxiliary: 14* get_scars_files (all together but a bit slower than the next two methods) 15* get_scars_indexed (part 1/2) 16* group_scars (part 2/2) 17 18Sample Usage: 19```bash 20python -m fire2a.cell2fire -vvv --base-raster ../fuels.asc --authid EPSG:25831 --scar-sample Grids/Grids2/F 21orestGrid01.csv --scar-raster scar_raster.tif --scar-poly scar_poly.shp --burn-prob burn_prob.tif --stat-sample RateOfSpread/ROSFile11.asc --stat-raster ros.tif --stat-summary ros_stats.tif 22``` 23""" 24# fmt: on 25__author__ = "Fernando Badilla" 26__revision__ = "$Format:%H$" 27 28import logging 29import sys 30from pathlib import Path 31 32from qgis.core import QgsRasterLayer 33 34from fire2a import setup_file 35from fire2a.utils import count_header_lines, fprint, loadtxt_nodata 36 37logger = logging.getLogger(__name__) 38"""captures the local logger""" 39 40NAME, FILEPATH = setup_file(name="cell2fire", filepath=Path("/home/fdo/source/fire2a-lib/src/fire2a")) 41"""setups the file name and path for logger (set path manually when pasting into ipython or jupyter session)""" 42 43 44def raster_layer_to_firebreak_csv(layer: QgsRasterLayer, firebreak_val: int = 1, output_file="firebreaks.csv") -> None: 45 """Write a (Cell2) Fire Simulator (C2FW) firebreak csv file from a QGIS raster layer 46 Usage as cli argument `Cell2Fire --FirebreakCells firebreaks.csv ...` 47 48 Args: 49 - layer (QgsRasterLayer): A QGIS raster layer, default is the active layer 50 - firebreak_val (int): The value used to identify the firebreaks, default is 666 51 - output_file (str or Path): The path to the output csv file, default is firebreaks.csv 52 53 QGIS Desktop Example: Choose method A or B, draw with serval (2) 54 55 1.A. Use the 'Create constant raster layer' tool to create one with the same extent extent, crs and pixel size than the fuels raster layer. Recommended: 56 - constant value = 0 57 - (Advanced Parameters) output raster data type = Byte 58 59 1.B. Use the 'raster calculator' with a base layer and 0 in the formula 60 61 2. Use 'Serval' plugin-tool to draw with the mouse the firebreaks on the new raster layer (values =1). Reload or save as to see changes. 62 63 QGIS Python Console Example: 64 ``` 65 layer = iface.activeLayer() 66 from fire2a.firebreaks import raster_layer_to_firebreak_csv 67 raster_layer_to_firebreak_csv(layer) 68 import processing 69 processing.run("fire2a:cell2firesimulator", ... 70 ``` 71 See also: https://fire2a.github.io/docs/docs/qgis-toolbox/c2f_firebreaks.html 72 """ # fmt: skip 73 from numpy import array as np_array 74 from numpy import where as np_where 75 76 from fire2a.raster import get_rlayer_data, xy2id 77 78 width = layer.width() 79 data = get_rlayer_data(layer) 80 81 # numpy is hh,ww indexing 82 yy, xx = np_where(data == firebreak_val) 83 ids = np_array([xy2id(x, y, width) for x, y in zip(xx, yy)]) 84 ids += 1 85 86 with open(output_file, "w") as f: 87 f.write("Year,Ncell\n") 88 f.write(f"1,{','.join(map(str,ids))}\n") 89 90 91def get_scars_files(sample_file: Path): 92 """Get sorted lists of (non-empty) files matching the pattern `root/parent(+any digit)/children(+any digit).(any extension)` 93 94 Normally used to read Cell2FireW scars `results/Grids/Grids*/ForestGrid*.csv` 95 96 Args: 97 - sample_file (Path): A sample file to extract the name and extension, parent and root directories 98 99 Returns a tuple: 100 - bool: True if successful, False otherwise. 101 - str: Error message, if any. 102 - Path: `root` - all other paths are relative to this and must be used as root/parent or root/child. 103 - list[Path]: `parents` - sorted list of parent directories. 104 - list[int]: `parents_ids` - corresponding simulation ids of these parents. 105 - list[list[Path]]: `children` - sorted list of lists of children files (grouped by simulation) 106 - list[list[int]]: `children_ids` - list of lists of corresponding period ids of each simulation 107 108 Sample Usage: 109 ```python 110 ret_val, msg, root, parent_dirs, parent_ids, children, children_ids = get_scars_files(Path(sample_file)) 111 if not ret_val: 112 logger.error(msg) 113 ``` 114 """ # fmt: skip 115 from re import search as re_search 116 117 ext = sample_file.suffix 118 if match := re_search(r"(\d+)$", sample_file.stem): 119 num = match.group() 120 else: 121 msg = f"sample_file: {sample_file} does not contain a number at the end" 122 return False, msg, None, None, None, None, None 123 file_name_wo_num = sample_file.stem[: -len(num)] 124 parent = sample_file.absolute().parent 125 root = sample_file.absolute().parent.parent 126 parent = parent.relative_to(root) 127 if match := re_search(r"(\d+)$", parent.name): 128 num = match.group() 129 else: 130 msg = f"sample_file:{sample_file} parent:{parent} does not contain a number at the end" 131 return False, msg, root, None, None, None, None 132 133 parent_wo_num = parent.name[: -len(num)] 134 parent_dirs = [] 135 parent_ids = [] 136 for par in root.glob(parent_wo_num + "[0-9]*"): 137 if par.is_dir(): 138 par = par.relative_to(root) 139 parent_ids += [int(re_search(r"(\d+)$", par.name).group(0))] 140 parent_dirs += [par] 141 adict = dict(zip(parent_dirs, parent_ids)) 142 parent_dirs.sort(key=lambda x: adict[x]) 143 parent_ids.sort() 144 145 children = [] 146 children_ids = [] 147 for par in parent_dirs: 148 chl_files = [] 149 chl_ids = [] 150 for afile in (root / par).glob(file_name_wo_num + "[0-9]*" + ext): 151 if afile.is_file() and afile.stat().st_size > 0: 152 afile = afile.relative_to(root) 153 chl_ids += [int(re_search(r"(\d+)$", afile.stem).group(0))] 154 chl_files += [afile] 155 adict = dict(zip(chl_files, chl_ids)) 156 chl_files.sort(key=lambda x: adict[x]) 157 chl_ids.sort() 158 children += [chl_files] 159 children_ids += [chl_ids] 160 161 # msg = f"Got {len(parent_dirs)} parent directories with {sum([len(chl) for chl in children])} children files" 162 msg = "" 163 return True, msg, root, parent_dirs, parent_ids, children, children_ids 164 165 166def get_scars_indexed(sample_file: Path): 167 """Get a sorted list of files with the pattern `root/parent(+any digit)/children(+any digit).(any extension)` 168 169 Args: 170 - sample_file (Path): A sample file to extract the extension, children name (wo ending number), parent (wo ending number) and root directory 171 172 Returns a tuple: 173 - return_value (bool): True if successful, False otherwise 174 - return_message (str): Debug/Error message if any 175 - root (Path): all paths are relative to this and must be used as root/file 176 - parent_wo_num (str): parent name without the ending number 177 - child_wo_num (str): children name without the ending number 178 - extension (str): file extension 179 - files (list[Path]): sorted list of (relative paths) files 180 - indexes (list[Tuple[int,int]]]): list of tuples of simulation and period ids 181 182 Sample Usage 183 ```python 184 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 185 if not ret_val: 186 logger.error(msg) 187 ``` 188 """ 189 from os import sep 190 from re import findall as re_findall 191 from re import search as re_search 192 193 from numpy import array as np_array 194 from numpy import fromiter as np_fromiter 195 196 ext = sample_file.suffix 197 if match := re_search(r"(\d+)$", sample_file.stem): 198 num = match.group() 199 else: 200 msg = f"sample_file: {sample_file} does not contain a number at the end" 201 return False, msg, None, None, None, ext, None, None 202 child_wo_num = sample_file.stem[: -len(num)] 203 parent = sample_file.absolute().parent 204 root = sample_file.absolute().parent.parent 205 parent = parent.relative_to(root) 206 if match := re_search(r"(\d+)$", parent.name): 207 num = match.group() 208 else: 209 msg = f"sample_file:{sample_file} parent:{parent} does not contain a number at the end" 210 return False, msg, root, None, child_wo_num, None, None 211 212 parent_wo_num = parent.name[: -len(num)] 213 214 files = np_array( 215 [ 216 afile.relative_to(root) 217 for afile in root.glob(parent_wo_num + "[0-9]*" + sep + child_wo_num + "[0-9]*" + ext) 218 if afile.is_file() and afile.stat().st_size > 0 219 ] 220 ) 221 222 if sep == "\\": 223 sep = "\\\\" 224 indexes = np_fromiter( 225 # re_findall(parent_wo_num + r"(\d+)" + sep + child_wo_num + r"(\d+)" + ext, " ".join(map(str, files))), 226 re_findall(r"(\d+)" + sep + child_wo_num + r"(\d+)", " ".join(map(str, files))), 227 dtype=[("sim", int), ("per", int)], 228 count=len(files), 229 ) 230 231 files = files[indexes.argsort(order=("sim", "per"))] 232 indexes.sort() 233 234 msg = "" 235 # msg = f"Got {len(files)} files\n" 236 # msg += f"{len(np_unique(indexes['sim']))} simulations\n" 237 # msg += f"{len(np_unique(indexes['per']))} different periods" 238 239 return True, msg, root, parent_wo_num, child_wo_num, ext, files, indexes 240 241 242def group_scars(root, parent_wo_num, child_wo_num, ext, files, indexes): 243 """Group scars files by simulation and period 244 245 Args: 246 - root (Path): root directory 247 - parent_wo_num (str): parent name without the ending number 248 - child_wo_num (str): children name without the ending number 249 - ext (str): file extension 250 - files (list[Path]): list of files 251 - indexes (list[Tuple[int,int]]): list of tuples of simulation and period ids 252 253 Returns: 254 - parent_ids (list[int]): list of simulation ids 255 - parent_dirs (list[Path]): list of parent directories 256 - children_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids 257 - children_files (list[Path]): list of children files 258 - final_scars_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids 259 - final_scars_files (list[Path]): list of final scars files 260 261 Sample: 262 ```python 263 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 264 if not retval: 265 logger.error(retmsg) 266 sys.exit(1) 267 parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files = group_scars( 268 root, parent_wo_num, child_wo_num, ext, files, indexes 269 ) 270 ``` 271 """ 272 from numpy import unique as np_unique 273 from numpy import where as np_where 274 275 parent_ids = [sim_id for sim_id in np_unique(indexes["sim"])] 276 children_ids = [[(sim_id, per_id) for per_id in indexes["per"][indexes["sim"] == sim_id]] for sim_id in parent_ids] 277 children_files = [[afile for afile in files[np_where(indexes["sim"] == pid)[0]]] for pid in parent_ids] 278 279 final_idx = [np_where(indexes["sim"] == pid)[0][-1] for pid in parent_ids] 280 281 parent_dirs = [afile.parent for afile in files[final_idx]] 282 283 final_scars_files = files[final_idx] 284 final_scars_ids = indexes[final_idx] 285 286 return parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files 287 288 289def build_scars( 290 scar_raster: str, 291 scar_poly: str, 292 burn_prob: str, 293 sample_file: Path, 294 W: int, 295 H: int, 296 geotransform: tuple, 297 authid: str, 298 callback=None, 299 feedback=None, 300): 301 """Builds the final scars raster, evolution scars polygons and/or burn probability raster files 302 303 Args: 304 - scar_raster (str): The output file name for the final scars raster 305 - scar_poly (str): The output file name for the evolution scars polygons 306 - burn_prob (str): The output file name for the burn probability raster 307 - sample_file (Path): Any scar sample file usually `results/Grids/Grids1/ForestGrid1.csv` 308 - W (int): Width of the raster 309 - H (int): Height of the raster 310 - geotransform (tuple): The geotransform of the raster 311 - authid (str): The projection of the raster 312 - callback (function): A function to call with the progress percentage 313 - feedback (QgsFeedback): A feedback object 314 315 Returns: 316 - int: 0 if successful, 1 otherwise 317 """ 318 from numpy import any as np_any 319 from numpy import float32 as np_float32 320 from numpy import int8 as np_int8 321 from numpy import loadtxt as np_loadtxt 322 from numpy import zeros as np_zeros 323 from osgeo import gdal, ogr, osr 324 325 from fire2a.processing_utils import get_output_raster_format, get_vector_driver_from_filename 326 327 gdal.UseExceptions() 328 329 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 330 if not retval: 331 fprint(retmsg, level="error", feedback=feedback, logger=logger) 332 return 1 333 parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files = group_scars( 334 root, parent_wo_num, child_wo_num, ext, files, indexes 335 ) 336 337 if burn_prob: 338 burn_prob_arr = np_zeros((H, W), dtype=np_float32) 339 else: 340 burn_prob_arr = None 341 342 if scar_raster: 343 driver_name = get_output_raster_format(scar_raster, feedback=feedback) 344 scar_raster_ds = gdal.GetDriverByName(driver_name).Create( 345 scar_raster, W, H, len(final_scars_ids), gdal.GDT_Byte 346 ) 347 scar_raster_ds.SetGeoTransform(geotransform) 348 scar_raster_ds.SetProjection(authid) 349 else: 350 scar_raster_ds = None 351 352 def final_scar_step(i, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=None): 353 if scar_raster: 354 band = scar_raster_ds.GetRasterBand(i) 355 # band.SetUnitType("burned") 356 if 0 != band.SetNoDataValue(0): 357 fprint( 358 f"Set NoData failed for Final Scar {i}: {afile}", level="warning", feedback=feedback, logger=logger 359 ) 360 if 0 != band.WriteArray(data): 361 fprint( 362 f"WriteArray failed for Final Scar {i}: {afile}", level="warning", feedback=feedback, logger=logger 363 ) 364 if i % 100 == 0: 365 scar_raster_ds.FlushCache() 366 if burn_prob: 367 if np_any(data == -1): 368 mask = data != -1 369 burn_prob_arr[ mask ] += data[ mask ] 370 else: 371 burn_prob_arr += data 372 373 if scar_poly: 374 # raster for each grid 375 src_ds = gdal.GetDriverByName("MEM").Create("", W, H, 1, gdal.GDT_Byte) 376 src_ds.SetGeoTransform(geotransform) 377 src_ds.SetProjection(authid) # export coords to file 378 379 # datasource for shadow geometry vector layer (polygonize output) 380 ogr_ds = ogr.GetDriverByName("Memory").CreateDataSource("") 381 382 # spatial reference 383 sp_ref = osr.SpatialReference() 384 sp_ref.SetFromUserInput(authid) 385 386 if scar_poly.startswith("memory:"): 387 driver_name = "GPKG" 388 fprint(f"Memory layer, using {driver_name} driver", level="info", feedback=feedback, logger=logger) 389 else: 390 driver_name = get_vector_driver_from_filename(scar_poly) 391 fprint(f"NOT Mem lyr, using {driver_name} driver", level="info", feedback=feedback, logger=logger) 392 393 drv = ogr.GetDriverByName(driver_name) 394 if 0 != drv.DeleteDataSource(scar_poly): 395 fprint(f"Failed to delete {scar_poly}", level="error", feedback=feedback, logger=logger) 396 otrods = drv.CreateDataSource(scar_poly) 397 otrolyr = otrods.CreateLayer("propagation_scars", srs=sp_ref, geom_type=ogr.wkbPolygon) 398 otrolyr.CreateField(ogr.FieldDefn("simulation", ogr.OFTInteger)) 399 otrolyr.CreateField(ogr.FieldDefn("time", ogr.OFTInteger)) 400 otrolyr.CreateField(ogr.FieldDefn("area", ogr.OFTInteger)) 401 otrolyr.CreateField(ogr.FieldDefn("perimeter", ogr.OFTInteger)) 402 403 count_evo = 0 404 count_fin = 0 405 total_files = sum(len(files) for files in children_files) 406 407 for sim_id, ids, files in zip(parent_ids, children_ids, children_files): 408 count_fin += 1 409 for (_, per_id), afile in zip(ids, files): 410 count_evo += 1 411 412 try: 413 data = np_loadtxt(root / afile, delimiter=",", dtype=np_int8) 414 except: 415 fprint( 416 f"Error reading {afile}, retrying with nodata = 0", 417 level="error", 418 feedback=feedback, 419 logger=logger, 420 ) 421 data = loadtxt_nodata(root / afile, delimiter=",", dtype=np_int8, no_data=0) 422 423 if not np_any(data == 1): 424 fprint( 425 f"no fire in {afile}, skipping propagation polygon", 426 level="warning", 427 feedback=feedback, 428 logger=logger, 429 ) 430 else: 431 src_band = src_ds.GetRasterBand(1) 432 src_band.SetNoDataValue(0) 433 src_band.WriteArray(data) 434 435 ogr_layer = ogr_ds.CreateLayer("", srs=sp_ref) 436 gdal.Polygonize(src_band, src_band, ogr_layer, -1, ["8CONNECTED=8"]) 437 438 feat = ogr_layer.GetNextFeature() 439 geom = feat.GetGeometryRef() 440 featureDefn = otrolyr.GetLayerDefn() 441 feature = ogr.Feature(featureDefn) 442 feature.SetGeometry(geom) 443 feature.SetField("simulation", int(sim_id)) 444 feature.SetField("time", int(per_id)) 445 feature.SetField("area", int(geom.GetArea())) 446 feature.SetField("perimeter", int(geom.Boundary().Length())) 447 otrolyr.CreateFeature(feature) 448 if count_evo % 100 == 0: 449 otrods.FlushCache() 450 451 if callback: 452 callback(count_evo / len(files) * 100, f"Processed Propagation-Scar {count_evo}/{len(indexes)}") 453 else: 454 fprint( 455 f"Processed Propagation-Scar {count_evo}/{len(indexes)}", 456 level="info", 457 feedback=feedback, 458 logger=logger, 459 ) 460 461 if scar_raster or burn_prob: 462 final_scar_step( 463 count_fin, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=feedback 464 ) 465 if callback: 466 callback(None, f"Processed +Final-Scar {count_evo}/{len(indexes)}") 467 else: 468 fprint( 469 f"Processed Final-Scar {count_fin}/{len(files)}", level="info", feedback=feedback, logger=logger 470 ) 471 # clean up 472 if scar_raster: 473 scar_raster_ds.FlushCache() 474 scar_raster_ds = None 475 otrods.FlushCache() 476 otrods = None 477 # otrolyr.SyncToDisk() CRASHES QGIS 478 # otrolyr.FlushCache() 479 otrolyr = None 480 src_ds.FlushCache() 481 src_ds = None 482 else: 483 # final scar loop 484 count_fin = 0 485 for (sim_id, per_id), afile in zip(final_scars_ids, final_scars_files): 486 count_fin += 1 487 try: 488 data = np_loadtxt(root / afile, delimiter=",", dtype=np_int8) 489 except: 490 fprint( 491 f"Error reading {afile}, retrying with nodata = 0", level="error", feedback=feedback, logger=logger 492 ) 493 data = loadtxt_nodata(root / afile, delimiter=",", dtype=np_int8, no_data=0) 494 if not np_any(data == 1): 495 fprint(f"no fire in Final-Scar {afile}", level="warning", feedback=feedback, logger=logger) 496 final_scar_step( 497 count_fin, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=feedback 498 ) 499 if callback: 500 callback(count_fin / len(final_scars_files) * 100, f"Processed Final-Scar {count_fin}/{len(files)}") 501 else: 502 fprint(f"Processed Final-Scar {count_fin}/{len(files)}", level="info", feedback=feedback, logger=logger) 503 if scar_raster: 504 scar_raster_ds.FlushCache() 505 scar_raster_ds = None 506 507 if burn_prob: 508 driver_name = get_output_raster_format(burn_prob, feedback=feedback) 509 burn_prob_ds = gdal.GetDriverByName(driver_name).Create(burn_prob, W, H, 1, gdal.GDT_Float32) 510 burn_prob_ds.SetGeoTransform(geotransform) 511 burn_prob_ds.SetProjection(authid) 512 band = burn_prob_ds.GetRasterBand(1) 513 # band.SetUnitType("probability") 514 if 0 != band.SetNoDataValue(0): 515 fprint( 516 f"Set NoData failed for Burn Probability {burn_prob}", level="warning", feedback=feedback, logger=logger 517 ) 518 if 0 != band.WriteArray(burn_prob_arr / len(final_scars_files)): # type: ignore 519 fprint( 520 f"WriteArray failed for Burn Probability {burn_prob}", level="warning", feedback=feedback, logger=logger 521 ) 522 burn_prob_ds.FlushCache() 523 burn_prob_ds = None 524 525 return 0 526 527 528def glob_numbered_files(sample_file: Path) -> tuple[list[Path], Path, str, str]: 529 """Get a list of files with the same name (+ any digit) and extension and the directory and name of the sample file 530 531 Args: 532 - sample_file (Path): A sample file to extract the extension, name and directory 533 - e.g., results/Intensity/Intensity1.asc, RateOfSpread/ROSFile2.asc, FlameLength/FL.asc, CrownFractionBurn/Cfb.asc, etc. 534 535 Returns a tuple: 536 - files (list[Path]): sorted list of files 537 - adir (Path): directory of the sample file 538 - aname (str): name of the sample file 539 - ext (str): extension of the sample file 540 """ 541 from re import search 542 543 ext = sample_file.suffix 544 if match := search(r"(\d+)$", sample_file.stem): 545 num = match.group() 546 else: 547 raise ValueError(f"sample_file: {sample_file} does not contain a number at the end") 548 aname = sample_file.stem[: -len(num)] 549 adir = sample_file.absolute().parent 550 files = [] 551 for afile in sorted(adir.glob(aname + "[0-9]*" + ext)): 552 if afile.is_file() and afile.stat().st_size > 0: 553 files += [afile] 554 # QgsMessageLog.logMessage(f"files: {files}, adir: {adir}, aname: {aname}, ext: {ext}", "fire2a", Qgis.Info) 555 return files, adir, aname, ext 556 557 558def build_stats( 559 stat_raster: str, 560 stat_summary: str, 561 sample_file: Path, 562 W: int, 563 H: int, 564 geotransform: tuple, 565 authid: str, 566 callback=None, 567 feedback=None, 568): 569 """Builds final statistics raster (1 band per-simulation) and summary raster (2 bands: mean against pixel burn count and stdev against total number of simulations) files 570 """ 571 from numpy import float32 as np_float32 572 from numpy import loadtxt as np_loadtxt 573 from numpy import sqrt as np_sqrt 574 from numpy import zeros as np_zeros 575 from osgeo import gdal 576 577 from fire2a.processing_utils import get_output_raster_format 578 579 gdal.UseExceptions() 580 581 files, root, aname, ext = glob_numbered_files(sample_file) 582 num_headers = count_header_lines(files[0], sep=" ", feedback=feedback) 583 584 if stat_raster: 585 driver_name = get_output_raster_format(stat_raster, feedback=feedback) 586 stat_raster_ds = gdal.GetDriverByName(driver_name).Create(stat_raster, W, H, len(files), gdal.GDT_Float32) 587 stat_raster_ds.SetGeoTransform(geotransform) 588 stat_raster_ds.SetProjection(authid) 589 else: 590 stat_raster_ds = None 591 592 if stat_summary: 593 summed = np_zeros((H, W), dtype=np_float32) 594 sumsquared = np_zeros((H, W), dtype=np_float32) 595 burncount = np_zeros((H, W), dtype=np_float32) 596 else: 597 summed = None 598 sumsquared = None 599 burncount = None 600 601 count = 0 602 for afile in files: 603 count += 1 604 try: 605 data = np_loadtxt(root / afile, delimiter=" ", dtype=np_float32, skiprows=num_headers) 606 except Exception as e: 607 fprint( 608 f"Error reading {afile}, retrying with nodata = -9999: {e}", 609 level="error", 610 feedback=feedback, 611 logger=logger, 612 ) 613 data = loadtxt_nodata(root / afile, delimiter=" ", dtype=np_float32, skiprows=num_headers, no_data=-9999) 614 615 if stat_raster_ds: 616 band = stat_raster_ds.GetRasterBand(count) 617 if 0 != band.SetNoDataValue(0): 618 fprint( 619 f"Set NoData failed for Statistic {count}: {afile}", 620 level="warning", 621 feedback=feedback, 622 logger=logger, 623 ) 624 if 0 != band.WriteArray(data): 625 fprint( 626 f"WriteArray failed for Statistic {count}: {afile}", 627 level="warning", 628 feedback=feedback, 629 logger=logger, 630 ) 631 if count % 100 == 0: 632 stat_raster_ds.FlushCache() 633 634 if stat_summary: 635 mask = data != -9999 636 tmp = data[mask] 637 summed[mask] += tmp 638 sumsquared[mask] += tmp ** 2 639 burncount[mask & (data != 0)] += 1 640 641 if callback: 642 callback(count / len(files) * 100, f"Processed Statistic {count}/{len(files)}") 643 else: 644 fprint(f"Processed Statistic {count}/{len(files)}", level="info", feedback=feedback, logger=logger) 645 646 if stat_raster_ds: 647 stat_raster_ds.FlushCache() 648 stat_raster_ds = None 649 650 if stat_summary: 651 driver_name = get_output_raster_format(stat_summary, feedback=feedback) 652 stat_summary_ds = gdal.GetDriverByName(driver_name).Create(stat_summary, W, H, 2, gdal.GDT_Float32) 653 stat_summary_ds.SetGeoTransform(geotransform) 654 stat_summary_ds.SetProjection(authid) 655 # mean 656 mask = burncount != 0 657 mean = np_zeros((H, W), dtype=np_float32) - 9999 658 mean[mask] = summed[mask] / burncount[mask] 659 band = stat_summary_ds.GetRasterBand(1) 660 if 0 != band.SetNoDataValue(-9999): 661 fprint( 662 f"Set NoData failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 663 ) 664 if 0 != band.WriteArray(mean): 665 fprint( 666 f"WriteArray failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 667 ) 668 # std 669 # from all simulations 670 N = len(files) 671 stddev = np_sqrt(sumsquared / N - (summed/N)**2) 672 # beacause this is always zero: 673 # stddev = np_zeros((H, W), dtype=np_float32) - 9999 674 # zero_mask = burncount == 0 675 # burncount[zero_mask] = 1 676 # stddev = np_sqrt(sumsquared / burncount - mean ** 2) 677 # stddev[~mask] = -9999 678 band = stat_summary_ds.GetRasterBand(2) 679 if 0 != band.SetNoDataValue(-9999): 680 fprint( 681 f"Set NoData failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 682 ) 683 if 0 != band.WriteArray(stddev): 684 fprint( 685 f"WriteArray failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 686 ) 687 stat_summary_ds.FlushCache() 688 stat_summary_ds = None 689 690 return 0 691 692 693def arg_parser(argv): 694 """Arguments parses to coordinate: scar and stats processing, verbosity, logfile 695 696 Args: 697 - argv (list): list of strings 698 - e.g., interactive: `args = arg_parser(['-vvv'])` 699 700 Returns: 701 - argparse.Namespace: parsed arguments 702 """ 703 import argparse 704 705 parser = argparse.ArgumentParser( 706 description=r"Fire2a-Cell2FireW Related algorithms CLI. Implemented here: Cell2FireW plain scars and statistic outputs into rasters and polygons.", 707 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 708 ) 709 710 scars = parser.add_argument_group( 711 "Scars: Transform C2FW scar outputs into rasters and polygons, i.e.: results/Grids/Grids*/ForestGrid*.csv" 712 ) 713 scars.add_argument( 714 "--scar-sample", 715 help="Matching the pattern 'root/parent(+any digit)/children(+any digit).(any csv extension)' ", 716 ) 717 scars.add_argument( 718 "--scar-raster", default="", help="Output file name for the final scars raster, each band a simulation" 719 ) 720 scars.add_argument( 721 "--scar-poly", 722 default="", 723 help="Output file name for the evolution scars polygons, multiple features with simulation and period attributes", 724 ) 725 scars.add_argument("--burn-prob", default="", help="Output file name for the burn probability raster, one band") 726 727 stats = parser.add_argument_group( 728 "Stats: Transform C2FW outputs into rasters, .e.g.: results/Intensity/Intensity1.asc, RateOfSpread/ROSFile2.asc, FlameLength/FL.asc, CrownFractionBurn/Cfb.asc, etc." 729 ) 730 stats.add_argument( 731 "--stat-sample", 732 default="", 733 help="Matching the pattern 'statistic_name(+any digit).asc' ", 734 ) 735 stats.add_argument( 736 "--stat-raster", default="", help="Output file name for the statistic raster, each band a simulation" 737 ) 738 stats.add_argument( 739 "--stat-summary", default="", help="Output file name for the raster, two bands: mean and std-deviation" 740 ) 741 742 parser.add_argument("--verbose", "-v", action="count", default=0, help="WARNING:1, INFO:2, DEBUG:3") 743 parser.add_argument( 744 "--logfile", 745 "-l", 746 action="store_true", 747 help="enable 5 log files named " + NAME + ".log (verbose must be enabled)", 748 default=None, 749 ) 750 parser.add_argument( 751 "--base-raster", required=True, help="Raster to base the geotransform, width, heigth and authid" 752 ) 753 parser.add_argument( 754 "--authid", required=False, help="Auth id to override (or missing on the base raster (e.g., using .asc raster)" 755 ) 756 args = parser.parse_args(argv) 757 if args.logfile: 758 args.logfile = NAME + ".log" 759 return args 760 761 762def main(argv=None): 763 """ 764 765 args = arg_parser(['-vvv']) 766 """ 767 768 if argv is sys.argv: 769 argv = sys.argv[1:] 770 args = arg_parser(argv) 771 772 if args.verbose != 0: 773 global logger 774 from fire2a import setup_logger 775 776 logger = setup_logger(verbosity=args.verbose, logfile=args.logfile) 777 # set other modules logging level 778 logging.getLogger("asyncio").setLevel(logging.INFO) 779 780 logger.info("args %s", args) 781 782 if not args.base_raster: 783 logger.error("No base raster file name provided") 784 return 1 785 786 if not Path(args.base_raster).is_file(): 787 logger.error("Base raster %s is not a file", args.base_raster) 788 return 1 789 790 from fire2a.raster import read_raster 791 792 _, raster_props = read_raster(args.base_raster, data=False, info=True) 793 794 if not (authid := raster_props["Projection"]): 795 if not (authid := args.authid): 796 logger.error("No authid found on the base raster or provided") 797 return 1 798 logger.info("Read base raster, using authid: %s", authid) 799 800 retval = {} 801 if args.scar_sample and (args.scar_poly or args.scar_raster or args.burn_prob): 802 retval["scar"] = build_scars( 803 args.scar_raster, 804 args.scar_poly, 805 args.burn_prob, 806 Path(args.scar_sample), 807 raster_props["RasterXSize"], 808 raster_props["RasterYSize"], 809 raster_props["Transform"], 810 authid, 811 ) 812 logger.info("built scars return value: %s (0 means sucess)", retval["scar"]) 813 814 if args.stat_sample and args.stat_raster: 815 retval["stat"] = build_stats( 816 args.stat_raster, 817 args.stat_summary, 818 Path(args.stat_sample), 819 raster_props["RasterXSize"], 820 raster_props["RasterYSize"], 821 raster_props["Transform"], 822 authid, 823 ) 824 logger.info("built statistic return value: %s (0 means success)", retval["stat"]) 825 826 print(retval) 827 return sum(retval.values()) 828 829 830if __name__ == "__main__": 831 sys.exit(main(sys.argv))
captures the local logger
45def raster_layer_to_firebreak_csv(layer: QgsRasterLayer, firebreak_val: int = 1, output_file="firebreaks.csv") -> None: 46 """Write a (Cell2) Fire Simulator (C2FW) firebreak csv file from a QGIS raster layer 47 Usage as cli argument `Cell2Fire --FirebreakCells firebreaks.csv ...` 48 49 Args: 50 - layer (QgsRasterLayer): A QGIS raster layer, default is the active layer 51 - firebreak_val (int): The value used to identify the firebreaks, default is 666 52 - output_file (str or Path): The path to the output csv file, default is firebreaks.csv 53 54 QGIS Desktop Example: Choose method A or B, draw with serval (2) 55 56 1.A. Use the 'Create constant raster layer' tool to create one with the same extent extent, crs and pixel size than the fuels raster layer. Recommended: 57 - constant value = 0 58 - (Advanced Parameters) output raster data type = Byte 59 60 1.B. Use the 'raster calculator' with a base layer and 0 in the formula 61 62 2. Use 'Serval' plugin-tool to draw with the mouse the firebreaks on the new raster layer (values =1). Reload or save as to see changes. 63 64 QGIS Python Console Example: 65 ``` 66 layer = iface.activeLayer() 67 from fire2a.firebreaks import raster_layer_to_firebreak_csv 68 raster_layer_to_firebreak_csv(layer) 69 import processing 70 processing.run("fire2a:cell2firesimulator", ... 71 ``` 72 See also: https://fire2a.github.io/docs/docs/qgis-toolbox/c2f_firebreaks.html 73 """ # fmt: skip 74 from numpy import array as np_array 75 from numpy import where as np_where 76 77 from fire2a.raster import get_rlayer_data, xy2id 78 79 width = layer.width() 80 data = get_rlayer_data(layer) 81 82 # numpy is hh,ww indexing 83 yy, xx = np_where(data == firebreak_val) 84 ids = np_array([xy2id(x, y, width) for x, y in zip(xx, yy)]) 85 ids += 1 86 87 with open(output_file, "w") as f: 88 f.write("Year,Ncell\n") 89 f.write(f"1,{','.join(map(str,ids))}\n")
Write a (Cell2) Fire Simulator (C2FW) firebreak csv file from a QGIS raster layer
Usage as cli argument Cell2Fire --FirebreakCells firebreaks.csv ...
Args:
- layer (QgsRasterLayer): A QGIS raster layer, default is the active layer
- firebreak_val (int): The value used to identify the firebreaks, default is 666
- output_file (str or Path): The path to the output csv file, default is firebreaks.csv
QGIS Desktop Example: Choose method A or B, draw with serval (2)
1.A. Use the 'Create constant raster layer' tool to create one with the same extent extent, crs and pixel size than the fuels raster layer. Recommended:
- constant value = 0
- (Advanced Parameters) output raster data type = Byte
1.B. Use the 'raster calculator' with a base layer and 0 in the formula
- Use 'Serval' plugin-tool to draw with the mouse the firebreaks on the new raster layer (values =1). Reload or save as to see changes.
QGIS Python Console Example:
layer = iface.activeLayer()
from fire2a.firebreaks import raster_layer_to_firebreak_csv
raster_layer_to_firebreak_csv(layer)
import processing
processing.run("fire2a:cell2firesimulator", ...
See also: https://fire2a.github.io/docs/docs/qgis-toolbox/c2f_firebreaks.html
92def get_scars_files(sample_file: Path): 93 """Get sorted lists of (non-empty) files matching the pattern `root/parent(+any digit)/children(+any digit).(any extension)` 94 95 Normally used to read Cell2FireW scars `results/Grids/Grids*/ForestGrid*.csv` 96 97 Args: 98 - sample_file (Path): A sample file to extract the name and extension, parent and root directories 99 100 Returns a tuple: 101 - bool: True if successful, False otherwise. 102 - str: Error message, if any. 103 - Path: `root` - all other paths are relative to this and must be used as root/parent or root/child. 104 - list[Path]: `parents` - sorted list of parent directories. 105 - list[int]: `parents_ids` - corresponding simulation ids of these parents. 106 - list[list[Path]]: `children` - sorted list of lists of children files (grouped by simulation) 107 - list[list[int]]: `children_ids` - list of lists of corresponding period ids of each simulation 108 109 Sample Usage: 110 ```python 111 ret_val, msg, root, parent_dirs, parent_ids, children, children_ids = get_scars_files(Path(sample_file)) 112 if not ret_val: 113 logger.error(msg) 114 ``` 115 """ # fmt: skip 116 from re import search as re_search 117 118 ext = sample_file.suffix 119 if match := re_search(r"(\d+)$", sample_file.stem): 120 num = match.group() 121 else: 122 msg = f"sample_file: {sample_file} does not contain a number at the end" 123 return False, msg, None, None, None, None, None 124 file_name_wo_num = sample_file.stem[: -len(num)] 125 parent = sample_file.absolute().parent 126 root = sample_file.absolute().parent.parent 127 parent = parent.relative_to(root) 128 if match := re_search(r"(\d+)$", parent.name): 129 num = match.group() 130 else: 131 msg = f"sample_file:{sample_file} parent:{parent} does not contain a number at the end" 132 return False, msg, root, None, None, None, None 133 134 parent_wo_num = parent.name[: -len(num)] 135 parent_dirs = [] 136 parent_ids = [] 137 for par in root.glob(parent_wo_num + "[0-9]*"): 138 if par.is_dir(): 139 par = par.relative_to(root) 140 parent_ids += [int(re_search(r"(\d+)$", par.name).group(0))] 141 parent_dirs += [par] 142 adict = dict(zip(parent_dirs, parent_ids)) 143 parent_dirs.sort(key=lambda x: adict[x]) 144 parent_ids.sort() 145 146 children = [] 147 children_ids = [] 148 for par in parent_dirs: 149 chl_files = [] 150 chl_ids = [] 151 for afile in (root / par).glob(file_name_wo_num + "[0-9]*" + ext): 152 if afile.is_file() and afile.stat().st_size > 0: 153 afile = afile.relative_to(root) 154 chl_ids += [int(re_search(r"(\d+)$", afile.stem).group(0))] 155 chl_files += [afile] 156 adict = dict(zip(chl_files, chl_ids)) 157 chl_files.sort(key=lambda x: adict[x]) 158 chl_ids.sort() 159 children += [chl_files] 160 children_ids += [chl_ids] 161 162 # msg = f"Got {len(parent_dirs)} parent directories with {sum([len(chl) for chl in children])} children files" 163 msg = "" 164 return True, msg, root, parent_dirs, parent_ids, children, children_ids
Get sorted lists of (non-empty) files matching the pattern root/parent(+any digit)/children(+any digit).(any extension)
Normally used to read Cell2FireW scars results/Grids/Grids*/ForestGrid*.csv
Args:
- sample_file (Path): A sample file to extract the name and extension, parent and root directories
Returns a tuple:
- bool: True if successful, False otherwise.
- str: Error message, if any.
- Path:
root
- all other paths are relative to this and must be used as root/parent or root/child. - list[Path]:
parents
- sorted list of parent directories. - list[int]:
parents_ids
- corresponding simulation ids of these parents. - list[list[Path]]:
children
- sorted list of lists of children files (grouped by simulation) - list[list[int]]:
children_ids
- list of lists of corresponding period ids of each simulation
Sample Usage:
ret_val, msg, root, parent_dirs, parent_ids, children, children_ids = get_scars_files(Path(sample_file))
if not ret_val:
logger.error(msg)
167def get_scars_indexed(sample_file: Path): 168 """Get a sorted list of files with the pattern `root/parent(+any digit)/children(+any digit).(any extension)` 169 170 Args: 171 - sample_file (Path): A sample file to extract the extension, children name (wo ending number), parent (wo ending number) and root directory 172 173 Returns a tuple: 174 - return_value (bool): True if successful, False otherwise 175 - return_message (str): Debug/Error message if any 176 - root (Path): all paths are relative to this and must be used as root/file 177 - parent_wo_num (str): parent name without the ending number 178 - child_wo_num (str): children name without the ending number 179 - extension (str): file extension 180 - files (list[Path]): sorted list of (relative paths) files 181 - indexes (list[Tuple[int,int]]]): list of tuples of simulation and period ids 182 183 Sample Usage 184 ```python 185 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 186 if not ret_val: 187 logger.error(msg) 188 ``` 189 """ 190 from os import sep 191 from re import findall as re_findall 192 from re import search as re_search 193 194 from numpy import array as np_array 195 from numpy import fromiter as np_fromiter 196 197 ext = sample_file.suffix 198 if match := re_search(r"(\d+)$", sample_file.stem): 199 num = match.group() 200 else: 201 msg = f"sample_file: {sample_file} does not contain a number at the end" 202 return False, msg, None, None, None, ext, None, None 203 child_wo_num = sample_file.stem[: -len(num)] 204 parent = sample_file.absolute().parent 205 root = sample_file.absolute().parent.parent 206 parent = parent.relative_to(root) 207 if match := re_search(r"(\d+)$", parent.name): 208 num = match.group() 209 else: 210 msg = f"sample_file:{sample_file} parent:{parent} does not contain a number at the end" 211 return False, msg, root, None, child_wo_num, None, None 212 213 parent_wo_num = parent.name[: -len(num)] 214 215 files = np_array( 216 [ 217 afile.relative_to(root) 218 for afile in root.glob(parent_wo_num + "[0-9]*" + sep + child_wo_num + "[0-9]*" + ext) 219 if afile.is_file() and afile.stat().st_size > 0 220 ] 221 ) 222 223 if sep == "\\": 224 sep = "\\\\" 225 indexes = np_fromiter( 226 # re_findall(parent_wo_num + r"(\d+)" + sep + child_wo_num + r"(\d+)" + ext, " ".join(map(str, files))), 227 re_findall(r"(\d+)" + sep + child_wo_num + r"(\d+)", " ".join(map(str, files))), 228 dtype=[("sim", int), ("per", int)], 229 count=len(files), 230 ) 231 232 files = files[indexes.argsort(order=("sim", "per"))] 233 indexes.sort() 234 235 msg = "" 236 # msg = f"Got {len(files)} files\n" 237 # msg += f"{len(np_unique(indexes['sim']))} simulations\n" 238 # msg += f"{len(np_unique(indexes['per']))} different periods" 239 240 return True, msg, root, parent_wo_num, child_wo_num, ext, files, indexes
Get a sorted list of files with the pattern root/parent(+any digit)/children(+any digit).(any extension)
Args:
- sample_file (Path): A sample file to extract the extension, children name (wo ending number), parent (wo ending number) and root directory
Returns a tuple:
- return_value (bool): True if successful, False otherwise
- return_message (str): Debug/Error message if any
- root (Path): all paths are relative to this and must be used as root/file
- parent_wo_num (str): parent name without the ending number
- child_wo_num (str): children name without the ending number
- extension (str): file extension
- files (list[Path]): sorted list of (relative paths) files
- indexes (list[Tuple[int,int]]]): list of tuples of simulation and period ids
Sample Usage
retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file)
if not ret_val:
logger.error(msg)
243def group_scars(root, parent_wo_num, child_wo_num, ext, files, indexes): 244 """Group scars files by simulation and period 245 246 Args: 247 - root (Path): root directory 248 - parent_wo_num (str): parent name without the ending number 249 - child_wo_num (str): children name without the ending number 250 - ext (str): file extension 251 - files (list[Path]): list of files 252 - indexes (list[Tuple[int,int]]): list of tuples of simulation and period ids 253 254 Returns: 255 - parent_ids (list[int]): list of simulation ids 256 - parent_dirs (list[Path]): list of parent directories 257 - children_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids 258 - children_files (list[Path]): list of children files 259 - final_scars_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids 260 - final_scars_files (list[Path]): list of final scars files 261 262 Sample: 263 ```python 264 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 265 if not retval: 266 logger.error(retmsg) 267 sys.exit(1) 268 parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files = group_scars( 269 root, parent_wo_num, child_wo_num, ext, files, indexes 270 ) 271 ``` 272 """ 273 from numpy import unique as np_unique 274 from numpy import where as np_where 275 276 parent_ids = [sim_id for sim_id in np_unique(indexes["sim"])] 277 children_ids = [[(sim_id, per_id) for per_id in indexes["per"][indexes["sim"] == sim_id]] for sim_id in parent_ids] 278 children_files = [[afile for afile in files[np_where(indexes["sim"] == pid)[0]]] for pid in parent_ids] 279 280 final_idx = [np_where(indexes["sim"] == pid)[0][-1] for pid in parent_ids] 281 282 parent_dirs = [afile.parent for afile in files[final_idx]] 283 284 final_scars_files = files[final_idx] 285 final_scars_ids = indexes[final_idx] 286 287 return parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files
Group scars files by simulation and period
Args:
- root (Path): root directory
- parent_wo_num (str): parent name without the ending number
- child_wo_num (str): children name without the ending number
- ext (str): file extension
- files (list[Path]): list of files
- indexes (list[Tuple[int,int]]): list of tuples of simulation and period ids
Returns:
- parent_ids (list[int]): list of simulation ids
- parent_dirs (list[Path]): list of parent directories
- children_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids
- children_files (list[Path]): list of children files
- final_scars_ids (list[Tuple[int,int]]): list of tuples of simulation and period ids
- final_scars_files (list[Path]): list of final scars files
Sample:
retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file)
if not retval:
logger.error(retmsg)
sys.exit(1)
parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files = group_scars(
root, parent_wo_num, child_wo_num, ext, files, indexes
)
290def build_scars( 291 scar_raster: str, 292 scar_poly: str, 293 burn_prob: str, 294 sample_file: Path, 295 W: int, 296 H: int, 297 geotransform: tuple, 298 authid: str, 299 callback=None, 300 feedback=None, 301): 302 """Builds the final scars raster, evolution scars polygons and/or burn probability raster files 303 304 Args: 305 - scar_raster (str): The output file name for the final scars raster 306 - scar_poly (str): The output file name for the evolution scars polygons 307 - burn_prob (str): The output file name for the burn probability raster 308 - sample_file (Path): Any scar sample file usually `results/Grids/Grids1/ForestGrid1.csv` 309 - W (int): Width of the raster 310 - H (int): Height of the raster 311 - geotransform (tuple): The geotransform of the raster 312 - authid (str): The projection of the raster 313 - callback (function): A function to call with the progress percentage 314 - feedback (QgsFeedback): A feedback object 315 316 Returns: 317 - int: 0 if successful, 1 otherwise 318 """ 319 from numpy import any as np_any 320 from numpy import float32 as np_float32 321 from numpy import int8 as np_int8 322 from numpy import loadtxt as np_loadtxt 323 from numpy import zeros as np_zeros 324 from osgeo import gdal, ogr, osr 325 326 from fire2a.processing_utils import get_output_raster_format, get_vector_driver_from_filename 327 328 gdal.UseExceptions() 329 330 retval, retmsg, root, parent_wo_num, child_wo_num, ext, files, indexes = get_scars_indexed(sample_file) 331 if not retval: 332 fprint(retmsg, level="error", feedback=feedback, logger=logger) 333 return 1 334 parent_ids, parent_dirs, children_ids, children_files, final_scars_ids, final_scars_files = group_scars( 335 root, parent_wo_num, child_wo_num, ext, files, indexes 336 ) 337 338 if burn_prob: 339 burn_prob_arr = np_zeros((H, W), dtype=np_float32) 340 else: 341 burn_prob_arr = None 342 343 if scar_raster: 344 driver_name = get_output_raster_format(scar_raster, feedback=feedback) 345 scar_raster_ds = gdal.GetDriverByName(driver_name).Create( 346 scar_raster, W, H, len(final_scars_ids), gdal.GDT_Byte 347 ) 348 scar_raster_ds.SetGeoTransform(geotransform) 349 scar_raster_ds.SetProjection(authid) 350 else: 351 scar_raster_ds = None 352 353 def final_scar_step(i, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=None): 354 if scar_raster: 355 band = scar_raster_ds.GetRasterBand(i) 356 # band.SetUnitType("burned") 357 if 0 != band.SetNoDataValue(0): 358 fprint( 359 f"Set NoData failed for Final Scar {i}: {afile}", level="warning", feedback=feedback, logger=logger 360 ) 361 if 0 != band.WriteArray(data): 362 fprint( 363 f"WriteArray failed for Final Scar {i}: {afile}", level="warning", feedback=feedback, logger=logger 364 ) 365 if i % 100 == 0: 366 scar_raster_ds.FlushCache() 367 if burn_prob: 368 if np_any(data == -1): 369 mask = data != -1 370 burn_prob_arr[ mask ] += data[ mask ] 371 else: 372 burn_prob_arr += data 373 374 if scar_poly: 375 # raster for each grid 376 src_ds = gdal.GetDriverByName("MEM").Create("", W, H, 1, gdal.GDT_Byte) 377 src_ds.SetGeoTransform(geotransform) 378 src_ds.SetProjection(authid) # export coords to file 379 380 # datasource for shadow geometry vector layer (polygonize output) 381 ogr_ds = ogr.GetDriverByName("Memory").CreateDataSource("") 382 383 # spatial reference 384 sp_ref = osr.SpatialReference() 385 sp_ref.SetFromUserInput(authid) 386 387 if scar_poly.startswith("memory:"): 388 driver_name = "GPKG" 389 fprint(f"Memory layer, using {driver_name} driver", level="info", feedback=feedback, logger=logger) 390 else: 391 driver_name = get_vector_driver_from_filename(scar_poly) 392 fprint(f"NOT Mem lyr, using {driver_name} driver", level="info", feedback=feedback, logger=logger) 393 394 drv = ogr.GetDriverByName(driver_name) 395 if 0 != drv.DeleteDataSource(scar_poly): 396 fprint(f"Failed to delete {scar_poly}", level="error", feedback=feedback, logger=logger) 397 otrods = drv.CreateDataSource(scar_poly) 398 otrolyr = otrods.CreateLayer("propagation_scars", srs=sp_ref, geom_type=ogr.wkbPolygon) 399 otrolyr.CreateField(ogr.FieldDefn("simulation", ogr.OFTInteger)) 400 otrolyr.CreateField(ogr.FieldDefn("time", ogr.OFTInteger)) 401 otrolyr.CreateField(ogr.FieldDefn("area", ogr.OFTInteger)) 402 otrolyr.CreateField(ogr.FieldDefn("perimeter", ogr.OFTInteger)) 403 404 count_evo = 0 405 count_fin = 0 406 total_files = sum(len(files) for files in children_files) 407 408 for sim_id, ids, files in zip(parent_ids, children_ids, children_files): 409 count_fin += 1 410 for (_, per_id), afile in zip(ids, files): 411 count_evo += 1 412 413 try: 414 data = np_loadtxt(root / afile, delimiter=",", dtype=np_int8) 415 except: 416 fprint( 417 f"Error reading {afile}, retrying with nodata = 0", 418 level="error", 419 feedback=feedback, 420 logger=logger, 421 ) 422 data = loadtxt_nodata(root / afile, delimiter=",", dtype=np_int8, no_data=0) 423 424 if not np_any(data == 1): 425 fprint( 426 f"no fire in {afile}, skipping propagation polygon", 427 level="warning", 428 feedback=feedback, 429 logger=logger, 430 ) 431 else: 432 src_band = src_ds.GetRasterBand(1) 433 src_band.SetNoDataValue(0) 434 src_band.WriteArray(data) 435 436 ogr_layer = ogr_ds.CreateLayer("", srs=sp_ref) 437 gdal.Polygonize(src_band, src_band, ogr_layer, -1, ["8CONNECTED=8"]) 438 439 feat = ogr_layer.GetNextFeature() 440 geom = feat.GetGeometryRef() 441 featureDefn = otrolyr.GetLayerDefn() 442 feature = ogr.Feature(featureDefn) 443 feature.SetGeometry(geom) 444 feature.SetField("simulation", int(sim_id)) 445 feature.SetField("time", int(per_id)) 446 feature.SetField("area", int(geom.GetArea())) 447 feature.SetField("perimeter", int(geom.Boundary().Length())) 448 otrolyr.CreateFeature(feature) 449 if count_evo % 100 == 0: 450 otrods.FlushCache() 451 452 if callback: 453 callback(count_evo / len(files) * 100, f"Processed Propagation-Scar {count_evo}/{len(indexes)}") 454 else: 455 fprint( 456 f"Processed Propagation-Scar {count_evo}/{len(indexes)}", 457 level="info", 458 feedback=feedback, 459 logger=logger, 460 ) 461 462 if scar_raster or burn_prob: 463 final_scar_step( 464 count_fin, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=feedback 465 ) 466 if callback: 467 callback(None, f"Processed +Final-Scar {count_evo}/{len(indexes)}") 468 else: 469 fprint( 470 f"Processed Final-Scar {count_fin}/{len(files)}", level="info", feedback=feedback, logger=logger 471 ) 472 # clean up 473 if scar_raster: 474 scar_raster_ds.FlushCache() 475 scar_raster_ds = None 476 otrods.FlushCache() 477 otrods = None 478 # otrolyr.SyncToDisk() CRASHES QGIS 479 # otrolyr.FlushCache() 480 otrolyr = None 481 src_ds.FlushCache() 482 src_ds = None 483 else: 484 # final scar loop 485 count_fin = 0 486 for (sim_id, per_id), afile in zip(final_scars_ids, final_scars_files): 487 count_fin += 1 488 try: 489 data = np_loadtxt(root / afile, delimiter=",", dtype=np_int8) 490 except: 491 fprint( 492 f"Error reading {afile}, retrying with nodata = 0", level="error", feedback=feedback, logger=logger 493 ) 494 data = loadtxt_nodata(root / afile, delimiter=",", dtype=np_int8, no_data=0) 495 if not np_any(data == 1): 496 fprint(f"no fire in Final-Scar {afile}", level="warning", feedback=feedback, logger=logger) 497 final_scar_step( 498 count_fin, data, afile, scar_raster, scar_raster_ds, burn_prob, burn_prob_arr, feedback=feedback 499 ) 500 if callback: 501 callback(count_fin / len(final_scars_files) * 100, f"Processed Final-Scar {count_fin}/{len(files)}") 502 else: 503 fprint(f"Processed Final-Scar {count_fin}/{len(files)}", level="info", feedback=feedback, logger=logger) 504 if scar_raster: 505 scar_raster_ds.FlushCache() 506 scar_raster_ds = None 507 508 if burn_prob: 509 driver_name = get_output_raster_format(burn_prob, feedback=feedback) 510 burn_prob_ds = gdal.GetDriverByName(driver_name).Create(burn_prob, W, H, 1, gdal.GDT_Float32) 511 burn_prob_ds.SetGeoTransform(geotransform) 512 burn_prob_ds.SetProjection(authid) 513 band = burn_prob_ds.GetRasterBand(1) 514 # band.SetUnitType("probability") 515 if 0 != band.SetNoDataValue(0): 516 fprint( 517 f"Set NoData failed for Burn Probability {burn_prob}", level="warning", feedback=feedback, logger=logger 518 ) 519 if 0 != band.WriteArray(burn_prob_arr / len(final_scars_files)): # type: ignore 520 fprint( 521 f"WriteArray failed for Burn Probability {burn_prob}", level="warning", feedback=feedback, logger=logger 522 ) 523 burn_prob_ds.FlushCache() 524 burn_prob_ds = None 525 526 return 0
Builds the final scars raster, evolution scars polygons and/or burn probability raster files
Args:
- scar_raster (str): The output file name for the final scars raster
- scar_poly (str): The output file name for the evolution scars polygons
- burn_prob (str): The output file name for the burn probability raster
- sample_file (Path): Any scar sample file usually
results/Grids/Grids1/ForestGrid1.csv
- W (int): Width of the raster
- H (int): Height of the raster
- geotransform (tuple): The geotransform of the raster
- authid (str): The projection of the raster
- callback (function): A function to call with the progress percentage
- feedback (QgsFeedback): A feedback object
Returns:
- int: 0 if successful, 1 otherwise
529def glob_numbered_files(sample_file: Path) -> tuple[list[Path], Path, str, str]: 530 """Get a list of files with the same name (+ any digit) and extension and the directory and name of the sample file 531 532 Args: 533 - sample_file (Path): A sample file to extract the extension, name and directory 534 - e.g., results/Intensity/Intensity1.asc, RateOfSpread/ROSFile2.asc, FlameLength/FL.asc, CrownFractionBurn/Cfb.asc, etc. 535 536 Returns a tuple: 537 - files (list[Path]): sorted list of files 538 - adir (Path): directory of the sample file 539 - aname (str): name of the sample file 540 - ext (str): extension of the sample file 541 """ 542 from re import search 543 544 ext = sample_file.suffix 545 if match := search(r"(\d+)$", sample_file.stem): 546 num = match.group() 547 else: 548 raise ValueError(f"sample_file: {sample_file} does not contain a number at the end") 549 aname = sample_file.stem[: -len(num)] 550 adir = sample_file.absolute().parent 551 files = [] 552 for afile in sorted(adir.glob(aname + "[0-9]*" + ext)): 553 if afile.is_file() and afile.stat().st_size > 0: 554 files += [afile] 555 # QgsMessageLog.logMessage(f"files: {files}, adir: {adir}, aname: {aname}, ext: {ext}", "fire2a", Qgis.Info) 556 return files, adir, aname, ext
Get a list of files with the same name (+ any digit) and extension and the directory and name of the sample file
Args:
- sample_file (Path): A sample file to extract the extension, name and directory
- e.g., results/Intensity/Intensity1.asc, RateOfSpread/ROSFile2.asc, FlameLength/FL.asc, CrownFractionBurn/Cfb.asc, etc.
Returns a tuple:
- files (list[Path]): sorted list of files
- adir (Path): directory of the sample file
- aname (str): name of the sample file
- ext (str): extension of the sample file
559def build_stats( 560 stat_raster: str, 561 stat_summary: str, 562 sample_file: Path, 563 W: int, 564 H: int, 565 geotransform: tuple, 566 authid: str, 567 callback=None, 568 feedback=None, 569): 570 """Builds final statistics raster (1 band per-simulation) and summary raster (2 bands: mean against pixel burn count and stdev against total number of simulations) files 571 """ 572 from numpy import float32 as np_float32 573 from numpy import loadtxt as np_loadtxt 574 from numpy import sqrt as np_sqrt 575 from numpy import zeros as np_zeros 576 from osgeo import gdal 577 578 from fire2a.processing_utils import get_output_raster_format 579 580 gdal.UseExceptions() 581 582 files, root, aname, ext = glob_numbered_files(sample_file) 583 num_headers = count_header_lines(files[0], sep=" ", feedback=feedback) 584 585 if stat_raster: 586 driver_name = get_output_raster_format(stat_raster, feedback=feedback) 587 stat_raster_ds = gdal.GetDriverByName(driver_name).Create(stat_raster, W, H, len(files), gdal.GDT_Float32) 588 stat_raster_ds.SetGeoTransform(geotransform) 589 stat_raster_ds.SetProjection(authid) 590 else: 591 stat_raster_ds = None 592 593 if stat_summary: 594 summed = np_zeros((H, W), dtype=np_float32) 595 sumsquared = np_zeros((H, W), dtype=np_float32) 596 burncount = np_zeros((H, W), dtype=np_float32) 597 else: 598 summed = None 599 sumsquared = None 600 burncount = None 601 602 count = 0 603 for afile in files: 604 count += 1 605 try: 606 data = np_loadtxt(root / afile, delimiter=" ", dtype=np_float32, skiprows=num_headers) 607 except Exception as e: 608 fprint( 609 f"Error reading {afile}, retrying with nodata = -9999: {e}", 610 level="error", 611 feedback=feedback, 612 logger=logger, 613 ) 614 data = loadtxt_nodata(root / afile, delimiter=" ", dtype=np_float32, skiprows=num_headers, no_data=-9999) 615 616 if stat_raster_ds: 617 band = stat_raster_ds.GetRasterBand(count) 618 if 0 != band.SetNoDataValue(0): 619 fprint( 620 f"Set NoData failed for Statistic {count}: {afile}", 621 level="warning", 622 feedback=feedback, 623 logger=logger, 624 ) 625 if 0 != band.WriteArray(data): 626 fprint( 627 f"WriteArray failed for Statistic {count}: {afile}", 628 level="warning", 629 feedback=feedback, 630 logger=logger, 631 ) 632 if count % 100 == 0: 633 stat_raster_ds.FlushCache() 634 635 if stat_summary: 636 mask = data != -9999 637 tmp = data[mask] 638 summed[mask] += tmp 639 sumsquared[mask] += tmp ** 2 640 burncount[mask & (data != 0)] += 1 641 642 if callback: 643 callback(count / len(files) * 100, f"Processed Statistic {count}/{len(files)}") 644 else: 645 fprint(f"Processed Statistic {count}/{len(files)}", level="info", feedback=feedback, logger=logger) 646 647 if stat_raster_ds: 648 stat_raster_ds.FlushCache() 649 stat_raster_ds = None 650 651 if stat_summary: 652 driver_name = get_output_raster_format(stat_summary, feedback=feedback) 653 stat_summary_ds = gdal.GetDriverByName(driver_name).Create(stat_summary, W, H, 2, gdal.GDT_Float32) 654 stat_summary_ds.SetGeoTransform(geotransform) 655 stat_summary_ds.SetProjection(authid) 656 # mean 657 mask = burncount != 0 658 mean = np_zeros((H, W), dtype=np_float32) - 9999 659 mean[mask] = summed[mask] / burncount[mask] 660 band = stat_summary_ds.GetRasterBand(1) 661 if 0 != band.SetNoDataValue(-9999): 662 fprint( 663 f"Set NoData failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 664 ) 665 if 0 != band.WriteArray(mean): 666 fprint( 667 f"WriteArray failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 668 ) 669 # std 670 # from all simulations 671 N = len(files) 672 stddev = np_sqrt(sumsquared / N - (summed/N)**2) 673 # beacause this is always zero: 674 # stddev = np_zeros((H, W), dtype=np_float32) - 9999 675 # zero_mask = burncount == 0 676 # burncount[zero_mask] = 1 677 # stddev = np_sqrt(sumsquared / burncount - mean ** 2) 678 # stddev[~mask] = -9999 679 band = stat_summary_ds.GetRasterBand(2) 680 if 0 != band.SetNoDataValue(-9999): 681 fprint( 682 f"Set NoData failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 683 ) 684 if 0 != band.WriteArray(stddev): 685 fprint( 686 f"WriteArray failed for Statistic {count}: {afile}", level="warning", feedback=feedback, logger=logger 687 ) 688 stat_summary_ds.FlushCache() 689 stat_summary_ds = None 690 691 return 0
Builds final statistics raster (1 band per-simulation) and summary raster (2 bands: mean against pixel burn count and stdev against total number of simulations) files
694def arg_parser(argv): 695 """Arguments parses to coordinate: scar and stats processing, verbosity, logfile 696 697 Args: 698 - argv (list): list of strings 699 - e.g., interactive: `args = arg_parser(['-vvv'])` 700 701 Returns: 702 - argparse.Namespace: parsed arguments 703 """ 704 import argparse 705 706 parser = argparse.ArgumentParser( 707 description=r"Fire2a-Cell2FireW Related algorithms CLI. Implemented here: Cell2FireW plain scars and statistic outputs into rasters and polygons.", 708 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 709 ) 710 711 scars = parser.add_argument_group( 712 "Scars: Transform C2FW scar outputs into rasters and polygons, i.e.: results/Grids/Grids*/ForestGrid*.csv" 713 ) 714 scars.add_argument( 715 "--scar-sample", 716 help="Matching the pattern 'root/parent(+any digit)/children(+any digit).(any csv extension)' ", 717 ) 718 scars.add_argument( 719 "--scar-raster", default="", help="Output file name for the final scars raster, each band a simulation" 720 ) 721 scars.add_argument( 722 "--scar-poly", 723 default="", 724 help="Output file name for the evolution scars polygons, multiple features with simulation and period attributes", 725 ) 726 scars.add_argument("--burn-prob", default="", help="Output file name for the burn probability raster, one band") 727 728 stats = parser.add_argument_group( 729 "Stats: Transform C2FW outputs into rasters, .e.g.: results/Intensity/Intensity1.asc, RateOfSpread/ROSFile2.asc, FlameLength/FL.asc, CrownFractionBurn/Cfb.asc, etc." 730 ) 731 stats.add_argument( 732 "--stat-sample", 733 default="", 734 help="Matching the pattern 'statistic_name(+any digit).asc' ", 735 ) 736 stats.add_argument( 737 "--stat-raster", default="", help="Output file name for the statistic raster, each band a simulation" 738 ) 739 stats.add_argument( 740 "--stat-summary", default="", help="Output file name for the raster, two bands: mean and std-deviation" 741 ) 742 743 parser.add_argument("--verbose", "-v", action="count", default=0, help="WARNING:1, INFO:2, DEBUG:3") 744 parser.add_argument( 745 "--logfile", 746 "-l", 747 action="store_true", 748 help="enable 5 log files named " + NAME + ".log (verbose must be enabled)", 749 default=None, 750 ) 751 parser.add_argument( 752 "--base-raster", required=True, help="Raster to base the geotransform, width, heigth and authid" 753 ) 754 parser.add_argument( 755 "--authid", required=False, help="Auth id to override (or missing on the base raster (e.g., using .asc raster)" 756 ) 757 args = parser.parse_args(argv) 758 if args.logfile: 759 args.logfile = NAME + ".log" 760 return args
Arguments parses to coordinate: scar and stats processing, verbosity, logfile
Args:
- argv (list): list of strings
- e.g., interactive:
args = arg_parser(['-vvv'])
- e.g., interactive:
Returns:
- argparse.Namespace: parsed arguments
763def main(argv=None): 764 """ 765 766 args = arg_parser(['-vvv']) 767 """ 768 769 if argv is sys.argv: 770 argv = sys.argv[1:] 771 args = arg_parser(argv) 772 773 if args.verbose != 0: 774 global logger 775 from fire2a import setup_logger 776 777 logger = setup_logger(verbosity=args.verbose, logfile=args.logfile) 778 # set other modules logging level 779 logging.getLogger("asyncio").setLevel(logging.INFO) 780 781 logger.info("args %s", args) 782 783 if not args.base_raster: 784 logger.error("No base raster file name provided") 785 return 1 786 787 if not Path(args.base_raster).is_file(): 788 logger.error("Base raster %s is not a file", args.base_raster) 789 return 1 790 791 from fire2a.raster import read_raster 792 793 _, raster_props = read_raster(args.base_raster, data=False, info=True) 794 795 if not (authid := raster_props["Projection"]): 796 if not (authid := args.authid): 797 logger.error("No authid found on the base raster or provided") 798 return 1 799 logger.info("Read base raster, using authid: %s", authid) 800 801 retval = {} 802 if args.scar_sample and (args.scar_poly or args.scar_raster or args.burn_prob): 803 retval["scar"] = build_scars( 804 args.scar_raster, 805 args.scar_poly, 806 args.burn_prob, 807 Path(args.scar_sample), 808 raster_props["RasterXSize"], 809 raster_props["RasterYSize"], 810 raster_props["Transform"], 811 authid, 812 ) 813 logger.info("built scars return value: %s (0 means sucess)", retval["scar"]) 814 815 if args.stat_sample and args.stat_raster: 816 retval["stat"] = build_stats( 817 args.stat_raster, 818 args.stat_summary, 819 Path(args.stat_sample), 820 raster_props["RasterXSize"], 821 raster_props["RasterYSize"], 822 raster_props["Transform"], 823 authid, 824 ) 825 logger.info("built statistic return value: %s (0 means success)", retval["stat"]) 826 827 print(retval) 828 return sum(retval.values())
args = arg_parser(['-vvv'])