From 45e173bb7ae4cfee441e9d34184304574c7a5d59 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:02:23 -0500 Subject: [PATCH 01/12] Create Running_on_HPC.txt --- Running_on_HPC.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 Running_on_HPC.txt diff --git a/Running_on_HPC.txt b/Running_on_HPC.txt new file mode 100644 index 00000000..84546e62 --- /dev/null +++ b/Running_on_HPC.txt @@ -0,0 +1 @@ +How to run on HPC From 62a8dd1d36fe654921fe1529d4241844c5242bde Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:47:04 -0500 Subject: [PATCH 02/12] Fix package initialization for cell2fire --- cell2fire/__init__.py | 10 ++++++++++ setup.py | 1 + 2 files changed, 11 insertions(+) create mode 100644 cell2fire/__init__.py diff --git a/cell2fire/__init__.py b/cell2fire/__init__.py new file mode 100644 index 00000000..8f921a17 --- /dev/null +++ b/cell2fire/__init__.py @@ -0,0 +1,10 @@ +"""Cell2Fire package initialization.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("Cell2Fire") +except PackageNotFoundError: # pragma: no cover - fallback for editable/dev installs + __version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/setup.py b/setup.py index 50be41b1..b50fa3b4 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ author='Cristobal Pais, Jaime Carrasco, David Martell, David L. Woodruff, Andres Weintraub', author_email='dlwoodruff@ucdavis.edu', packages=packages, + package_dir={'': '.'}, install_requires=['numpy', 'pandas', 'matplotlib', 'seaborn', 'tqdm', 'opencv-python', 'networkx', 'deap'], extras_require={ 'doc': [ From 837084d781d6197e8f5e8f1f04b41d92fcb3670f Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:58:45 -0500 Subject: [PATCH 03/12] Add Cell2Fire compatibility package alias --- Cell2Fire/__init__.py | 14 ++++++++++++++ cell2fire/__init__.py | 10 ++++++++++ setup.py | 1 + 3 files changed, 25 insertions(+) create mode 100644 Cell2Fire/__init__.py create mode 100644 cell2fire/__init__.py diff --git a/Cell2Fire/__init__.py b/Cell2Fire/__init__.py new file mode 100644 index 00000000..ad69b09a --- /dev/null +++ b/Cell2Fire/__init__.py @@ -0,0 +1,14 @@ +"""Compatibility package exposing the historical `Cell2Fire` import path.""" + +from importlib import import_module +import sys + +_pkg = import_module("cell2fire") + +# Re-export symbols and share package search path so submodules resolve: +# e.g. `from Cell2Fire.utils.ParseInputs import ParseInputs`. +globals().update(_pkg.__dict__) +__path__ = _pkg.__path__ + +# Ensure both names reference the same loaded package instance. +sys.modules[__name__] = _pkg diff --git a/cell2fire/__init__.py b/cell2fire/__init__.py new file mode 100644 index 00000000..8f921a17 --- /dev/null +++ b/cell2fire/__init__.py @@ -0,0 +1,10 @@ +"""Cell2Fire package initialization.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("Cell2Fire") +except PackageNotFoundError: # pragma: no cover - fallback for editable/dev installs + __version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/setup.py b/setup.py index 50be41b1..b50fa3b4 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ author='Cristobal Pais, Jaime Carrasco, David Martell, David L. Woodruff, Andres Weintraub', author_email='dlwoodruff@ucdavis.edu', packages=packages, + package_dir={'': '.'}, install_requires=['numpy', 'pandas', 'matplotlib', 'seaborn', 'tqdm', 'opencv-python', 'networkx', 'deap'], extras_require={ 'doc': [ From 414b7e26c1e06b385955dafafa5c519f6ae066fe Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:18:08 -0500 Subject: [PATCH 04/12] Auto-build C++ core when executable is missing --- Cell2Fire/__init__.py | 14 +++++++++++ cell2fire/Cell2FireC_class.py | 46 ++++++++++++++++++++++++++++++----- cell2fire/__init__.py | 10 ++++++++ setup.py | 3 ++- 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 Cell2Fire/__init__.py create mode 100644 cell2fire/__init__.py diff --git a/Cell2Fire/__init__.py b/Cell2Fire/__init__.py new file mode 100644 index 00000000..ad69b09a --- /dev/null +++ b/Cell2Fire/__init__.py @@ -0,0 +1,14 @@ +"""Compatibility package exposing the historical `Cell2Fire` import path.""" + +from importlib import import_module +import sys + +_pkg = import_module("cell2fire") + +# Re-export symbols and share package search path so submodules resolve: +# e.g. `from Cell2Fire.utils.ParseInputs import ParseInputs`. +globals().update(_pkg.__dict__) +__path__ = _pkg.__path__ + +# Ensure both names reference the same loaded package instance. +sys.modules[__name__] = _pkg diff --git a/cell2fire/Cell2FireC_class.py b/cell2fire/Cell2FireC_class.py index e7b9d3f9..6d520538 100644 --- a/cell2fire/Cell2FireC_class.py +++ b/cell2fire/Cell2FireC_class.py @@ -16,10 +16,42 @@ from cell2fire.utils.Stats import * from cell2fire.utils.Heuristics import * import cell2fire # for path finding -p = str(cell2fire.__path__) -l = p.find("'") -r = p.find("'", l+1) -cell2fire_path = p[l+1:r] +cell2fire_path = os.path.dirname(cell2fire.__file__) + + + +def _core_binary_path(): + return os.path.join(cell2fire_path, 'Cell2FireC', 'Cell2Fire') + + +def _ensure_core_binary_exists(): + core_bin = _core_binary_path() + if os.path.isfile(core_bin): + return core_bin + + build_dir = os.path.join(cell2fire_path, 'Cell2FireC') + if shutil.which('make') is None: + raise RuntimeError( + f"Cell2Fire core executable not found at {core_bin}. " + "Install make and build it manually with: " + f"cd {build_dir} && make" + ) + + try: + subprocess.check_call(['make'], cwd=build_dir) + except subprocess.CalledProcessError as exc: + raise RuntimeError( + "Failed to build the Cell2Fire C++ core automatically. " + f"Run manually: cd {build_dir} && make" + ) from exc + + if not os.path.isfile(core_bin): + raise RuntimeError( + f"Build completed but executable is still missing at {core_bin}. " + "Please inspect the Makefile/toolchain on this system." + ) + + return core_bin class Cell2FireC: # Constructor and initial run @@ -55,7 +87,8 @@ def __init__(self, args): def run(self): # Parse args for calling C++ via subprocess # old: execArray=[os.path.join(os.getcwd(),'Cell2FireC/Cell2Fire'), - execArray=[os.path.join(cell2fire_path,'Cell2FireC/Cell2Fire'), + core_bin = _ensure_core_binary_exists() + execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', self.args.OutFolder if (self.args.OutFolder is not None) else '', '--ignitions' if (self.args.ignitions) else '', @@ -98,7 +131,8 @@ def run(self): # Run C++ Sim with heuristic treatment def run_Heur(self, OutFolder, HarvestPlanFile): # Parse args for calling C++ via subprocess - execArray=[os.path.join(cell2fire_path,'Cell2FireC/Cell2Fire'), + core_bin = _ensure_core_binary_exists() + execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', OutFolder if (OutFolder is not None) else '', '--ignitions' if (self.args.ignitions) else '', diff --git a/cell2fire/__init__.py b/cell2fire/__init__.py new file mode 100644 index 00000000..8f921a17 --- /dev/null +++ b/cell2fire/__init__.py @@ -0,0 +1,10 @@ +"""Cell2Fire package initialization.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("Cell2Fire") +except PackageNotFoundError: # pragma: no cover - fallback for editable/dev installs + __version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/setup.py b/setup.py index 50be41b1..4abd4e99 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ author='Cristobal Pais, Jaime Carrasco, David Martell, David L. Woodruff, Andres Weintraub', author_email='dlwoodruff@ucdavis.edu', packages=packages, - install_requires=['numpy', 'pandas', 'matplotlib', 'seaborn', 'tqdm', 'opencv-python', 'networkx', 'deap'], + package_dir={'': '.'}, + install_requires=['numpy', 'pandas', 'matplotlib', 'seaborn', 'tqdm', 'opencv-python', 'networkx', 'deap', 'scipy'], extras_require={ 'doc': [ 'sphinx_rtd_theme', From bd5a40890b7821545157283e30fbc20e0476ee66 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:46:54 -0500 Subject: [PATCH 05/12] Add inventory of Cell2Fire simulation parameters and inputs --- doc/simulation_parameters_inventory.md | 132 +++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 doc/simulation_parameters_inventory.md diff --git a/doc/simulation_parameters_inventory.md b/doc/simulation_parameters_inventory.md new file mode 100644 index 00000000..a8675434 --- /dev/null +++ b/doc/simulation_parameters_inventory.md @@ -0,0 +1,132 @@ +# Cell2Fire simulation inputs and tunable parameters + +This note inventories what you can change when running Cell2Fire, based on the Python argument parser, the Python-to-C++ wrapper command construction, the C++ argument reader, and CSV/ASC readers. + +## 1) Command-line parameters (Python entrypoint: `python cell2fire/main.py ...`) + +Defined in `cell2fire/utils/ParseInputs.py`. + +### Paths and run scope +- `--input-instance-folder` (`InFolder`, str, default `None`): folder with simulation inputs. +- `--output-folder` (`OutFolder`, str, default `None`): output folder. +- `--sim-years` (`sim_years`, int, default `1`): years per simulation. +- `--nsims` (`nsims`, int, default `1`): number of simulation replications. +- `--seed` (`seed`, int, default `123`): RNG seed. +- `--nweathers` (`nweathers`, int, default `1`): maximum weather index used in random-weather mode. +- `--nthreads` (`nthreads`, int, default `1`): Python-side argument (not currently forwarded by wrapper to C++ core). +- `--max-fire-periods` (`max_fire_periods`, int, default `1000`): hard cap on fire periods. +- `--IgnitionRad` (`IgRadius`, int, default `0`): neighborhood radius around ignition points. +- `--gridsStep` (`gridsStep`, int, default `60`): grid generation period step. +- `--gridsFreq` (`gridsFreq`, int, default `-1`): grid generation simulation frequency. + +### Heuristic/treatment planning parameters +- `--heuristic` (`heuristic`, int, default `-1`): heuristic mode (`-1` disables heuristic flow). +- `--MessagesPath` (`messages_path`, str, default `None`): path to message files. +- `--GASelection` (`GASelection`, bool flag): use genetic algorithm selection. +- `--HarvestedCells` (`HCells`, str, default `None`): path to initial harvested cells CSV. +- `--msgheur` (`msgHeur`, str, default `""`): path to heuristic message files. +- `--applyPlan` (`planPath`, str, default `""`): path to treatment/harvesting plan. +- `--DFraction` (`TFraction`, float, default `1.0`): demand fraction. +- `--GPTree` (`GPTree`, bool flag): use global propagation tree. +- `--customValue` (`valueFile`, str, default `None`): custom objective/value file. +- `--noEvaluation` (`noEvaluation`, bool flag): generate plans without evaluation. + +### Genetic algorithm hyperparameters +- `--ngen` (`ngen`, int, default `500`): generations. +- `--npop` (`npop`, int, default `100`): population size. +- `--tsize` (`tSize`, int, default `3`): tournament size. +- `--cxpb` (`cxpb`, float, default `0.8`): crossover probability. +- `--mutpb` (`mutpb`, float, default `0.2`): mutation probability. +- `--indpb` (`indpb`, float, default `0.5`): per-individual probability. + +### Simulation behavior, outputs, and post-processing flags +- `--weather` (`WeatherOpt`, str, default `rows`): weather mode (`constant`, `random`, `rows`). +- `--spreadPlots`, `--finalGrid`, `--verbose`, `--ignitions`, `--grids`, `--simPlots`, `--allPlots`, `--combine`, `--no-output`, `--gen-data`, `--output-messages`, `--Prometheus-tuned`, `--trajectories`, `--stats`, `--correctedStats`, `--onlyProcessing`, `--bbo`, `--fdemand`, `--pdfOutputs`: boolean flags controlling simulation behavior and outputs. + +### Core fire spread / intensity parameters +- `--Fire-Period-Length` (`input_PeriodLen`, float, default `60` min). +- `--Weather-Period-Length` (`weather_period_len`, float, default `60` min). +- `--ROS-Threshold` (`ROS_Threshold`, float, default `0.1` m/min). +- `--HFI-Threshold` (`HFI_Threshold`, float, default `0.1` kW/m in code help text typo says 10 default). +- `--ROS-CV` (`ROS_CV`, float, default `0.0`): stochastic ROS coefficient of variation. +- `--HFactor` (`HFactor`, float, default `1.0`): multiplier for head ROS. +- `--FFactor` (`FFactor`, float, default `1.0`): multiplier for flank ROS. +- `--BFactor` (`BFactor`, float, default `1.0`): multiplier for back ROS. +- `--EFactor` (`EFactor`, float, default `1.0`): ellipse adjustment factor. +- `--BurningLen` (`BurningLen`, float, default `-1.0`): burn duration in periods. + +## 2) What actually reaches the C++ simulator from Python + +The Python wrapper (`cell2fire/Cell2FireC_class.py`) forwards these options into the C++ binary call: + +- `--input-instance-folder`, `--output-folder` +- `--ignitions` +- `--sim-years`, `--nsims` +- `--grids`, `--final-grid` +- `--Fire-Period-Length` +- `--output-messages` +- `--weather`, `--nweathers` +- `--ROS-CV` +- `--IgnitionRad` +- `--seed` +- `--ROS-Threshold`, `--HFI-Threshold` +- `--bbo` +- `--HarvestPlan` (populated from `--HarvestedCells` path) +- `--verbose` + +Not currently forwarded in this wrapper despite being parsed in Python: `--HFactor`, `--FFactor`, `--BFactor`, `--EFactor`, `--Weather-Period-Length`, `--max-fire-periods`, `--nthreads`, many plotting/postprocessing flags (which are used in Python-side postprocess), and heuristic-only fields used by Python logic. + +## 3) C++ CLI options accepted by the core binary + +`cell2fire/Cell2FireC/ReadArgs.cpp` parses these options directly: + +- Strings: `--input-instance-folder`, `--output-folder`, `--weather`, `--HarvestPlan` +- Boolean flags: `--output-messages`, `--trajectories`, `--no-output`, `--verbose`, `--ignitions`, `--grids`, `--final-grid`, `--PromTuned`, `--statistics`, `--bbo` +- Numeric: `--sim-years`, `--nsims`, `--Weather-Period-Length`, `--nweathers`, `--Fire-Period-Length`, `--IgnitionRad`, `--ROS-Threshold`, `--HFI-Threshold`, `--HFactor`, `--FFactor`, `--BFactor`, `--EFactor`, `--ROS-CV`, `--max-fire-periods`, `--seed` + +## 4) Required/optional instance input files and their fields + +The C++ constructor reads: + +- `Forest.asc` (grid geometry and fuel raster) +- `Data.csv` (per-cell FBP inputs) +- `Weather.csv` (weather-by-period values) +- Optional `Ignitions.csv` (if `--ignitions`) +- Optional harvest plan CSV via `--HarvestPlan` +- Optional `BBOFuels.csv` (if `--bbo`) + +### `Data.csv` fields (per cell) +Header expected (example in datasets): + +`fueltype,mon,jd,M,jd_min,lat,lon,elev,ffmc,ws,waz,bui,ps,saz,pc,pdf,gfl,cur,time,pattern` + +These are parsed into the FBP `inputs` struct values used by spread calculations. + +### `Weather.csv` fields (per weather period) +Header expected: + +`Scenario,datetime,APCP,TMP,RH,WS,WD,FFMC,DMC,DC,ISI,BUI,FWI` + +This is where you control FFMC and other moisture/fire-weather drivers by period. + +### `Ignitions.csv` fields +Typical header: + +`Year,Ncell` + +Each row fixes ignition cell per year (when `--ignitions` is enabled). + +### `BBOFuels.csv` fields +Parsed as per-fuel factors (read when `--bbo` is active), used to tune spread/intensity behavior by fuel type. + +## 5) Moisture-related parameters you can change + +If you specifically want fuel-moisture controls: + +- `ffmc` in `Data.csv` (cell baseline field, if used/populated). +- `FFMC`, `DMC`, `DC` columns in `Weather.csv` (time-varying moisture codes). +- `BUI` in both `Data.csv` and `Weather.csv` (build-up index). +- `RH`, `TMP`, and `APCP` in `Weather.csv` indirectly influence fire behavior and moisture context. +- `ROS-Threshold`, `HFI-Threshold`, `ROS-CV`, and ROS factor multipliers (`HFactor/FFactor/BFactor`) are direct simulation controls that modulate spread and continuation. + +Note: foliar moisture content (FMC) is computed internally (`foliar_moisture(...)` in FBP code) rather than read as an explicit top-level CLI parameter. From eda5ced7fefac313a687ffedc527994457601ccb Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:40:44 -0500 Subject: [PATCH 06/12] Add 100x100 power-line loss test script --- scratch/powerline_loss_test.py | 227 +++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 scratch/powerline_loss_test.py diff --git a/scratch/powerline_loss_test.py b/scratch/powerline_loss_test.py new file mode 100644 index 00000000..9345c1c6 --- /dev/null +++ b/scratch/powerline_loss_test.py @@ -0,0 +1,227 @@ +"""Run a simple 100x100 Cell2Fire test with a power line in the middle. + +This script does three things: +1. Creates a synthetic 100x100 dataset from the Sub40x40 toy inputs. +2. Runs Cell2Fire on that dataset. +3. Reports how often the middle power-line cells are burned and total losses. + +The loss model uses Cell2Fire's native per-cell custom value input (--customValue). +""" + +from __future__ import annotations + +import csv +import shutil +import subprocess +from pathlib import Path +from typing import List + +import numpy as np + + +# Keep paths explicit and simple. +REPO_ROOT = Path(__file__).resolve().parents[1] +BASE_TOY_FOLDER = REPO_ROOT / "data" / "Sub40x40" +GENERATED_DATA_FOLDER = REPO_ROOT / "data" / "PowerLine100x100" +OUTPUT_FOLDER = REPO_ROOT / "outputs" / "PowerLine100x100" + + +def copy_common_input_files() -> None: + """Copy lookup table and weather from Sub40x40 toy data.""" + GENERATED_DATA_FOLDER.mkdir(parents=True, exist_ok=True) + + files_to_copy = [ + "fbp_lookup_table.csv", + "Weather.csv", + ] + + for file_name in files_to_copy: + src = BASE_TOY_FOLDER / file_name + dst = GENERATED_DATA_FOLDER / file_name + shutil.copy2(src, dst) + + +def build_forest_asc(nrows: int = 100, ncols: int = 100) -> None: + """Create a flat forest grid with one fuel type (C1 => grid value 1).""" + forest_path = GENERATED_DATA_FOLDER / "Forest.asc" + + with forest_path.open("w", encoding="utf-8") as out: + out.write(f"ncols {ncols}\n") + out.write(f"nrows {nrows}\n") + out.write("xllcorner 0\n") + out.write("yllcorner 0\n") + out.write("cellsize 100\n") + out.write("NODATA_value -9999\n") + + # Single vegetation type everywhere. + row_values = " ".join(["1"] * ncols) + for _ in range(nrows): + out.write(row_values + "\n") + + +def build_data_csv(nrows: int = 100, ncols: int = 100) -> None: + """Create Data.csv with C1 and flat terrain for all cells.""" + data_path = GENERATED_DATA_FOLDER / "Data.csv" + + header = [ + "fueltype", + "mon", + "jd", + "M", + "jd_min", + "lat", + "lon", + "elev", + "ffmc", + "ws", + "waz", + "bui", + "ps", + "saz", + "pc", + "pdf", + "gfl", + "cur", + "time", + "pattern", + ] + + total_cells = nrows * ncols + + with data_path.open("w", newline="", encoding="utf-8") as out: + writer = csv.writer(out) + writer.writerow(header) + + for _ in range(total_cells): + # Very simple and explicit values: + # - fueltype C1 everywhere + # - elev, slope (ps), and aspect (saz) set to 0 for flat terrain + row = [ + "C1", "", "", "", "", 51.0, -115.0, 0, + "", "", "", "", 0, 0, + "", "", 0.75, "", 20, "", + ] + writer.writerow(row) + + +def build_powerline_values(nrows: int = 100, ncols: int = 100) -> np.ndarray: + """Create custom per-cell value map and save it as values100x100.csv. + + We treat the middle column as the power line. Normal cells have value 1. + Power-line cells have value 100 to represent higher consequence loss. + """ + values = np.ones((nrows, ncols), dtype=np.float32) + + # Middle column index for a 100-column grid is 49 (0-based), i.e., column 50 (1-based). + powerline_col_index = (ncols // 2) - 1 + values[:, powerline_col_index] = 100.0 + + values_path = GENERATED_DATA_FOLDER / "values100x100.csv" + np.savetxt(values_path, values, fmt="%.1f", delimiter=" ") + return values + + +def run_cell2fire(nsims: int = 20, sim_years: int = 1) -> None: + """Run the simulator with the generated dataset.""" + if OUTPUT_FOLDER.exists(): + shutil.rmtree(OUTPUT_FOLDER) + + cmd: List[str] = [ + "python", + "-m", + "cell2fire.main", + "--input-instance-folder", + str(GENERATED_DATA_FOLDER), + "--output-folder", + str(OUTPUT_FOLDER), + "--sim-years", + str(sim_years), + "--nsims", + str(nsims), + "--finalGrid", + "--weather", + "random", + "--nweathers", + "100", + "--Fire-Period-Length", + "1.0", + "--ROS-CV", + "0.0", + "--seed", + "123", + "--IgnitionRad", + "0", + "--grids", + "--output-messages", + "--stats", + "--ROS-Threshold", + "0", + "--HFI-Threshold", + "0", + "--customValue", + str(GENERATED_DATA_FOLDER / "values100x100.csv"), + ] + + subprocess.run(cmd, check=True, cwd=REPO_ROOT) + + +def get_last_forest_grid_for_sim(sim_index: int) -> Path: + """Find the final ForestGrid file for one simulation output folder.""" + sim_grid_folder = OUTPUT_FOLDER / "Grids" / f"Grids{sim_index}" + forest_files = sorted(sim_grid_folder.glob("ForestGrid*.csv")) + if not forest_files: + raise RuntimeError(f"No ForestGrid files found in {sim_grid_folder}") + return forest_files[-1] + + +def evaluate_losses(values: np.ndarray, nsims: int = 20) -> None: + """Calculate hit frequency and loss on the power-line cells.""" + nrows, ncols = values.shape + powerline_col_index = (ncols // 2) - 1 + powerline_values = values[:, powerline_col_index] + + hit_count = 0 + losses = [] + + for sim in range(1, nsims + 1): + grid_file = get_last_forest_grid_for_sim(sim) + final_grid = np.loadtxt(grid_file, delimiter=",") + + # Burned cells are encoded as 1 in final grid outputs. + burned_powerline = final_grid[:, powerline_col_index] == 1 + loss_this_sim = float(np.sum(powerline_values[burned_powerline])) + losses.append(loss_this_sim) + + if np.any(burned_powerline): + hit_count += 1 + + hit_rate = hit_count / float(nsims) + avg_loss = float(np.mean(losses)) + max_loss = float(np.max(losses)) + + print("\n=== Power line test summary ===") + print(f"Number of simulations: {nsims}") + print(f"Power line cells per sim: {nrows}") + print(f"Power line hit count: {hit_count}") + print(f"Power line hit rate: {hit_rate:.2%}") + print(f"Average power line loss: {avg_loss:.2f}") + print(f"Maximum power line loss: {max_loss:.2f}") + + +def main() -> None: + """Create dataset, run Cell2Fire, and report power line losses.""" + nrows = 100 + ncols = 100 + nsims = 20 + + copy_common_input_files() + build_forest_asc(nrows=nrows, ncols=ncols) + build_data_csv(nrows=nrows, ncols=ncols) + values = build_powerline_values(nrows=nrows, ncols=ncols) + + run_cell2fire(nsims=nsims, sim_years=1) + evaluate_losses(values=values, nsims=nsims) + + +if __name__ == "__main__": + main() From e3ed347d4070032bff60ca05d3437b75b3c9f090 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:27:57 -0500 Subject: [PATCH 07/12] Make burn probability map vary across simulations --- scratch/powerline_loss_test.py | 318 +++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 scratch/powerline_loss_test.py diff --git a/scratch/powerline_loss_test.py b/scratch/powerline_loss_test.py new file mode 100644 index 00000000..610add80 --- /dev/null +++ b/scratch/powerline_loss_test.py @@ -0,0 +1,318 @@ +"""Run a simple 100x100 Cell2Fire test with a power line in the middle. + +This script does four things: +1. Creates a synthetic 100x100 dataset from the Sub40x40 toy inputs. +2. Runs the Cell2Fire C++ core on that dataset. +3. Reports how often the middle power-line cells are burned and total losses. +4. Builds a burn-probability map figure and highlights the power-line column. + +The script uses weather mode "rows" by default for stability on custom datasets. + +Important: +- We do NOT call `python -m cell2fire.main` here. +- Calling `main.py` imports plotting/stat modules (cv2), which can fail on headless HPC nodes. +- This script calls the C++ executable directly, so it is better suited for HPC batch runs. +""" + +from __future__ import annotations + +import csv +import shutil +import subprocess +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +REPO_ROOT = Path(__file__).resolve().parents[1] +BASE_TOY_FOLDER = REPO_ROOT / "data" / "Sub40x40" +GENERATED_DATA_FOLDER = REPO_ROOT / "data" / "PowerLine100x100" +OUTPUT_FOLDER = REPO_ROOT / "outputs" / "PowerLine100x100" +CORE_BINARY = REPO_ROOT / "cell2fire" / "Cell2FireC" / "Cell2Fire" +WEATHER_MODE = "rows" +NWEATHERS = 1 +USE_PREDEFINED_IGNITIONS = False + + +def as_c2f_folder_arg(folder: Path) -> str: + """Return folder path string with trailing separator for Cell2Fire C++ CLI.""" + folder_str = str(folder) + if folder_str.endswith("/"): + return folder_str + return folder_str + "/" + + +def copy_common_input_files() -> None: + """Copy required inputs from the Sub40x40 toy dataset.""" + GENERATED_DATA_FOLDER.mkdir(parents=True, exist_ok=True) + + files_to_copy = [ + "fbp_lookup_table.csv", + "Weather.csv", + ] + + # Optional: copy fixed ignitions. + # If this is enabled and sim_years=1, all simulations use the same ignition. + if USE_PREDEFINED_IGNITIONS: + files_to_copy.append("Ignitions.csv") + + for file_name in files_to_copy: + src = BASE_TOY_FOLDER / file_name + dst = GENERATED_DATA_FOLDER / file_name + shutil.copy2(src, dst) + + +def build_forest_asc(nrows: int = 100, ncols: int = 100) -> None: + """Create a flat forest grid with one fuel type (C1 => grid value 1).""" + forest_path = GENERATED_DATA_FOLDER / "Forest.asc" + + with forest_path.open("w", encoding="utf-8") as out: + out.write(f"ncols {ncols}\n") + out.write(f"nrows {nrows}\n") + out.write("xllcorner 0\n") + out.write("yllcorner 0\n") + out.write("cellsize 100\n") + out.write("NODATA_value -9999\n") + + row_values = " ".join(["1"] * ncols) + for _ in range(nrows): + out.write(row_values + "\n") + + +def build_data_csv(nrows: int = 100, ncols: int = 100) -> None: + """Create Data.csv with C1 and flat terrain for all cells.""" + data_path = GENERATED_DATA_FOLDER / "Data.csv" + + header = [ + "fueltype", + "mon", + "jd", + "M", + "jd_min", + "lat", + "lon", + "elev", + "ffmc", + "ws", + "waz", + "bui", + "ps", + "saz", + "pc", + "pdf", + "gfl", + "cur", + "time", + "pattern", + ] + + total_cells = nrows * ncols + + with data_path.open("w", newline="", encoding="utf-8") as out: + writer = csv.writer(out) + writer.writerow(header) + + for _ in range(total_cells): + # Simple, explicit values for a uniform/flat setup. + row = [ + "C1", "", "", "", "", 51.0, -115.0, 0, + "", "", "", "", 0, 0, + "", "", 0.75, "", 20, "", + ] + writer.writerow(row) + + +def build_powerline_values(nrows: int = 100, ncols: int = 100) -> np.ndarray: + """Create per-cell value map where the middle column is the power line. + + Normal cells = 1 + Power-line cells = 100 + """ + values = np.ones((nrows, ncols), dtype=np.float32) + + # For 100 columns, we use column 50 (1-based index) as the power line. + powerline_col_index = (ncols // 2) - 1 + values[:, powerline_col_index] = 100.0 + + values_path = GENERATED_DATA_FOLDER / "values100x100.csv" + np.savetxt(values_path, values, fmt="%.1f", delimiter=" ") + return values + + +def ensure_core_binary_exists() -> None: + """Ensure the C++ Cell2Fire executable exists.""" + if CORE_BINARY.exists(): + return + + print("Cell2Fire core binary not found. Building it with make...") + subprocess.run(["make", "-C", str(CORE_BINARY.parent)], check=True, cwd=REPO_ROOT) + + if not CORE_BINARY.exists(): + raise RuntimeError(f"Expected binary not found after build: {CORE_BINARY}") + + +def print_logfile_tail(logfile: Path, max_lines: int = 60) -> None: + """Print the tail of the C++ logfile to make HPC errors easier to debug.""" + if not logfile.exists(): + print(f"Log file does not exist: {logfile}") + return + + print("\n--- Tail of Cell2Fire log ---") + lines = logfile.read_text(encoding="utf-8", errors="replace").splitlines() + for line in lines[-max_lines:]: + print(line) + print("--- End of log tail ---\n") + + +def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: + """Run Cell2Fire C++ core directly (HPC-friendly path).""" + if OUTPUT_FOLDER.exists(): + shutil.rmtree(OUTPUT_FOLDER) + OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True) + + cmd = [ + str(CORE_BINARY), + "--input-instance-folder", as_c2f_folder_arg(GENERATED_DATA_FOLDER), + "--output-folder", as_c2f_folder_arg(OUTPUT_FOLDER), + "--sim-years", str(sim_years), + "--nsims", str(nsims), + "--grids", + "--final-grid", + "--Fire-Period-Length", "1.0", + "--output-messages", + "--weather", WEATHER_MODE, + "--nweathers", str(NWEATHERS), + "--ROS-CV", "0.0", + "--IgnitionRad", "0", + "--seed", "123", + "--ROS-Threshold", "0", + "--HFI-Threshold", "0", + ] + + if USE_PREDEFINED_IGNITIONS: + cmd.append("--ignitions") + + logfile = OUTPUT_FOLDER / "LogFile.txt" + try: + with logfile.open("w", encoding="utf-8") as log: + subprocess.run(cmd, check=True, cwd=REPO_ROOT, stdout=log, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + print_logfile_tail(logfile) + raise + + +def get_last_forest_grid_for_sim(sim_index: int) -> Path: + """Find the final ForestGrid file for one simulation output folder.""" + sim_grid_folder = OUTPUT_FOLDER / "Grids" / f"Grids{sim_index}" + forest_files = sorted(sim_grid_folder.glob("ForestGrid*.csv")) + if not forest_files: + raise RuntimeError(f"No ForestGrid files found in {sim_grid_folder}") + return forest_files[-1] + + +def load_final_grids(nsims: int = 20) -> list[np.ndarray]: + """Load final burn grids (0/1) for all simulations.""" + final_grids: list[np.ndarray] = [] + + for sim in range(1, nsims + 1): + grid_file = get_last_forest_grid_for_sim(sim) + final_grid = np.loadtxt(grid_file, delimiter=",") + final_grids.append(final_grid) + + return final_grids + + +def compute_burn_probability_map(final_grids: list[np.ndarray]) -> np.ndarray: + """Compute burn probability at each cell from final simulation grids.""" + if len(final_grids) == 0: + raise RuntimeError("No simulation grids were loaded to compute burn probability") + + grid_stack = np.stack(final_grids, axis=0) + burn_probability_map = np.mean(grid_stack == 1, axis=0) + + # Helpful debug information: if all runs are identical, this list is often [0.0, 1.0]. + unique_values = np.unique(burn_probability_map) + print(f"Unique burn-probability values in map: {unique_values[:10]}") + + return burn_probability_map + + +def plot_burn_probability_map(burn_prob: np.ndarray, output_png: Path) -> None: + """Plot burn probability map and highlight the power-line column.""" + nrows, ncols = burn_prob.shape + powerline_col_index = (ncols // 2) - 1 + + plt.figure(figsize=(8, 7)) + image = plt.imshow(burn_prob, cmap="hot", vmin=0.0, vmax=1.0, origin="upper") + plt.colorbar(image, label="Burn probability") + + # Draw vertical line through the center power-line column. + plt.axvline(x=powerline_col_index, color="cyan", linestyle="--", linewidth=2, label="Power line") + + plt.title("Burn probability map (100x100)") + plt.xlabel("Column index") + plt.ylabel("Row index") + plt.legend(loc="upper right") + plt.tight_layout() + plt.savefig(output_png, dpi=200) + plt.close() + + print(f"Saved burn probability map: {output_png}") + + +def evaluate_losses(values: np.ndarray, final_grids: list[np.ndarray]) -> None: + """Calculate hit frequency and loss on power-line cells.""" + nrows, ncols = values.shape + powerline_col_index = (ncols // 2) - 1 + powerline_values = values[:, powerline_col_index] + + hit_count = 0 + losses = [] + + for final_grid in final_grids: + burned_powerline = final_grid[:, powerline_col_index] == 1 + loss_this_sim = float(np.sum(powerline_values[burned_powerline])) + losses.append(loss_this_sim) + + if np.any(burned_powerline): + hit_count += 1 + + nsims = len(final_grids) + hit_rate = hit_count / float(nsims) + avg_loss = float(np.mean(losses)) + max_loss = float(np.max(losses)) + + print("\n=== Power line test summary ===") + print(f"Number of simulations: {nsims}") + print(f"Power line cells per sim: {nrows}") + print(f"Power line hit count: {hit_count}") + print(f"Power line hit rate: {hit_rate:.2%}") + print(f"Average power line loss: {avg_loss:.2f}") + print(f"Maximum power line loss: {max_loss:.2f}") + + +def main() -> None: + """Create dataset, run Cell2Fire core, summarize losses, and plot burn probability.""" + nrows = 100 + ncols = 100 + nsims = 20 + + copy_common_input_files() + build_forest_asc(nrows=nrows, ncols=ncols) + build_data_csv(nrows=nrows, ncols=ncols) + values = build_powerline_values(nrows=nrows, ncols=ncols) + + ensure_core_binary_exists() + run_cell2fire_core(nsims=nsims, sim_years=1) + + final_grids = load_final_grids(nsims=nsims) + evaluate_losses(values=values, final_grids=final_grids) + + burn_prob = compute_burn_probability_map(final_grids) + plot_path = OUTPUT_FOLDER / "BurnProbabilityMap.png" + plot_burn_probability_map(burn_prob=burn_prob, output_png=plot_path) + + +if __name__ == "__main__": + main() From 2d87879225badc3a582b7cabc20eb3ec64ac5640 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:21:24 -0500 Subject: [PATCH 08/12] Clarify global weather assumption in powerline test script --- scratch/powerline_loss_test.py | 321 +++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 scratch/powerline_loss_test.py diff --git a/scratch/powerline_loss_test.py b/scratch/powerline_loss_test.py new file mode 100644 index 00000000..9195f699 --- /dev/null +++ b/scratch/powerline_loss_test.py @@ -0,0 +1,321 @@ +"""Run a simple 100x100 Cell2Fire test with a power line in the middle. + +This script does four things: +1. Creates a synthetic 100x100 dataset from the Sub40x40 toy inputs. +2. Runs the Cell2Fire C++ core on that dataset. +3. Reports how often the middle power-line cells are burned and total losses. +4. Builds a burn-probability map figure and highlights the power-line column. + +The script uses weather mode "rows" by default for stability on custom datasets. + +Note: weather (including wind speed/direction) is read from Weather.csv as a global +weather period signal, not as a per-cell wind field in this experiment script. + +Important: +- We do NOT call `python -m cell2fire.main` here. +- Calling `main.py` imports plotting/stat modules (cv2), which can fail on headless HPC nodes. +- This script calls the C++ executable directly, so it is better suited for HPC batch runs. +""" + +from __future__ import annotations + +import csv +import shutil +import subprocess +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +REPO_ROOT = Path(__file__).resolve().parents[1] +BASE_TOY_FOLDER = REPO_ROOT / "data" / "Sub40x40" +GENERATED_DATA_FOLDER = REPO_ROOT / "data" / "PowerLine100x100" +OUTPUT_FOLDER = REPO_ROOT / "outputs" / "PowerLine100x100" +CORE_BINARY = REPO_ROOT / "cell2fire" / "Cell2FireC" / "Cell2Fire" +WEATHER_MODE = "rows" +NWEATHERS = 1 +USE_PREDEFINED_IGNITIONS = False + + +def as_c2f_folder_arg(folder: Path) -> str: + """Return folder path string with trailing separator for Cell2Fire C++ CLI.""" + folder_str = str(folder) + if folder_str.endswith("/"): + return folder_str + return folder_str + "/" + + +def copy_common_input_files() -> None: + """Copy required inputs from the Sub40x40 toy dataset.""" + GENERATED_DATA_FOLDER.mkdir(parents=True, exist_ok=True) + + files_to_copy = [ + "fbp_lookup_table.csv", + "Weather.csv", + ] + + # Optional: copy fixed ignitions. + # If this is enabled and sim_years=1, all simulations use the same ignition. + if USE_PREDEFINED_IGNITIONS: + files_to_copy.append("Ignitions.csv") + + for file_name in files_to_copy: + src = BASE_TOY_FOLDER / file_name + dst = GENERATED_DATA_FOLDER / file_name + shutil.copy2(src, dst) + + +def build_forest_asc(nrows: int = 100, ncols: int = 100) -> None: + """Create a flat forest grid with one fuel type (C1 => grid value 1).""" + forest_path = GENERATED_DATA_FOLDER / "Forest.asc" + + with forest_path.open("w", encoding="utf-8") as out: + out.write(f"ncols {ncols}\n") + out.write(f"nrows {nrows}\n") + out.write("xllcorner 0\n") + out.write("yllcorner 0\n") + out.write("cellsize 100\n") + out.write("NODATA_value -9999\n") + + row_values = " ".join(["1"] * ncols) + for _ in range(nrows): + out.write(row_values + "\n") + + +def build_data_csv(nrows: int = 100, ncols: int = 100) -> None: + """Create Data.csv with C1 and flat terrain for all cells.""" + data_path = GENERATED_DATA_FOLDER / "Data.csv" + + header = [ + "fueltype", + "mon", + "jd", + "M", + "jd_min", + "lat", + "lon", + "elev", + "ffmc", + "ws", + "waz", + "bui", + "ps", + "saz", + "pc", + "pdf", + "gfl", + "cur", + "time", + "pattern", + ] + + total_cells = nrows * ncols + + with data_path.open("w", newline="", encoding="utf-8") as out: + writer = csv.writer(out) + writer.writerow(header) + + for _ in range(total_cells): + # Simple, explicit values for a uniform/flat setup. + row = [ + "C1", "", "", "", "", 51.0, -115.0, 0, + "", "", "", "", 0, 0, + "", "", 0.75, "", 20, "", + ] + writer.writerow(row) + + +def build_powerline_values(nrows: int = 100, ncols: int = 100) -> np.ndarray: + """Create per-cell value map where the middle column is the power line. + + Normal cells = 1 + Power-line cells = 100 + """ + values = np.ones((nrows, ncols), dtype=np.float32) + + # For 100 columns, we use column 50 (1-based index) as the power line. + powerline_col_index = (ncols // 2) - 1 + values[:, powerline_col_index] = 100.0 + + values_path = GENERATED_DATA_FOLDER / "values100x100.csv" + np.savetxt(values_path, values, fmt="%.1f", delimiter=" ") + return values + + +def ensure_core_binary_exists() -> None: + """Ensure the C++ Cell2Fire executable exists.""" + if CORE_BINARY.exists(): + return + + print("Cell2Fire core binary not found. Building it with make...") + subprocess.run(["make", "-C", str(CORE_BINARY.parent)], check=True, cwd=REPO_ROOT) + + if not CORE_BINARY.exists(): + raise RuntimeError(f"Expected binary not found after build: {CORE_BINARY}") + + +def print_logfile_tail(logfile: Path, max_lines: int = 60) -> None: + """Print the tail of the C++ logfile to make HPC errors easier to debug.""" + if not logfile.exists(): + print(f"Log file does not exist: {logfile}") + return + + print("\n--- Tail of Cell2Fire log ---") + lines = logfile.read_text(encoding="utf-8", errors="replace").splitlines() + for line in lines[-max_lines:]: + print(line) + print("--- End of log tail ---\n") + + +def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: + """Run Cell2Fire C++ core directly (HPC-friendly path).""" + if OUTPUT_FOLDER.exists(): + shutil.rmtree(OUTPUT_FOLDER) + OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True) + + cmd = [ + str(CORE_BINARY), + "--input-instance-folder", as_c2f_folder_arg(GENERATED_DATA_FOLDER), + "--output-folder", as_c2f_folder_arg(OUTPUT_FOLDER), + "--sim-years", str(sim_years), + "--nsims", str(nsims), + "--grids", + "--final-grid", + "--Fire-Period-Length", "1.0", + "--output-messages", + "--weather", WEATHER_MODE, + "--nweathers", str(NWEATHERS), + "--ROS-CV", "0.0", + "--IgnitionRad", "0", + "--seed", "123", + "--ROS-Threshold", "0", + "--HFI-Threshold", "0", + ] + + if USE_PREDEFINED_IGNITIONS: + cmd.append("--ignitions") + + logfile = OUTPUT_FOLDER / "LogFile.txt" + try: + with logfile.open("w", encoding="utf-8") as log: + subprocess.run(cmd, check=True, cwd=REPO_ROOT, stdout=log, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + print_logfile_tail(logfile) + raise + + +def get_last_forest_grid_for_sim(sim_index: int) -> Path: + """Find the final ForestGrid file for one simulation output folder.""" + sim_grid_folder = OUTPUT_FOLDER / "Grids" / f"Grids{sim_index}" + forest_files = sorted(sim_grid_folder.glob("ForestGrid*.csv")) + if not forest_files: + raise RuntimeError(f"No ForestGrid files found in {sim_grid_folder}") + return forest_files[-1] + + +def load_final_grids(nsims: int = 20) -> list[np.ndarray]: + """Load final burn grids (0/1) for all simulations.""" + final_grids: list[np.ndarray] = [] + + for sim in range(1, nsims + 1): + grid_file = get_last_forest_grid_for_sim(sim) + final_grid = np.loadtxt(grid_file, delimiter=",") + final_grids.append(final_grid) + + return final_grids + + +def compute_burn_probability_map(final_grids: list[np.ndarray]) -> np.ndarray: + """Compute burn probability at each cell from final simulation grids.""" + if len(final_grids) == 0: + raise RuntimeError("No simulation grids were loaded to compute burn probability") + + grid_stack = np.stack(final_grids, axis=0) + burn_probability_map = np.mean(grid_stack == 1, axis=0) + + # Helpful debug information: if all runs are identical, this list is often [0.0, 1.0]. + unique_values = np.unique(burn_probability_map) + print(f"Unique burn-probability values in map: {unique_values[:10]}") + + return burn_probability_map + + +def plot_burn_probability_map(burn_prob: np.ndarray, output_png: Path) -> None: + """Plot burn probability map and highlight the power-line column.""" + nrows, ncols = burn_prob.shape + powerline_col_index = (ncols // 2) - 1 + + plt.figure(figsize=(8, 7)) + image = plt.imshow(burn_prob, cmap="hot", vmin=0.0, vmax=1.0, origin="upper") + plt.colorbar(image, label="Burn probability") + + # Draw vertical line through the center power-line column. + plt.axvline(x=powerline_col_index, color="cyan", linestyle="--", linewidth=2, label="Power line") + + plt.title("Burn probability map (100x100)") + plt.xlabel("Column index") + plt.ylabel("Row index") + plt.legend(loc="upper right") + plt.tight_layout() + plt.savefig(output_png, dpi=200) + plt.close() + + print(f"Saved burn probability map: {output_png}") + + +def evaluate_losses(values: np.ndarray, final_grids: list[np.ndarray]) -> None: + """Calculate hit frequency and loss on power-line cells.""" + nrows, ncols = values.shape + powerline_col_index = (ncols // 2) - 1 + powerline_values = values[:, powerline_col_index] + + hit_count = 0 + losses = [] + + for final_grid in final_grids: + burned_powerline = final_grid[:, powerline_col_index] == 1 + loss_this_sim = float(np.sum(powerline_values[burned_powerline])) + losses.append(loss_this_sim) + + if np.any(burned_powerline): + hit_count += 1 + + nsims = len(final_grids) + hit_rate = hit_count / float(nsims) + avg_loss = float(np.mean(losses)) + max_loss = float(np.max(losses)) + + print("\n=== Power line test summary ===") + print(f"Number of simulations: {nsims}") + print(f"Power line cells per sim: {nrows}") + print(f"Power line hit count: {hit_count}") + print(f"Power line hit rate: {hit_rate:.2%}") + print(f"Average power line loss: {avg_loss:.2f}") + print(f"Maximum power line loss: {max_loss:.2f}") + + +def main() -> None: + """Create dataset, run Cell2Fire core, summarize losses, and plot burn probability.""" + nrows = 100 + ncols = 100 + nsims = 20 + + copy_common_input_files() + build_forest_asc(nrows=nrows, ncols=ncols) + build_data_csv(nrows=nrows, ncols=ncols) + values = build_powerline_values(nrows=nrows, ncols=ncols) + + ensure_core_binary_exists() + run_cell2fire_core(nsims=nsims, sim_years=1) + + final_grids = load_final_grids(nsims=nsims) + evaluate_losses(values=values, final_grids=final_grids) + + burn_prob = compute_burn_probability_map(final_grids) + plot_path = OUTPUT_FOLDER / "BurnProbabilityMap.png" + plot_burn_probability_map(burn_prob=burn_prob, output_png=plot_path) + + +if __name__ == "__main__": + main() From eb10b5720d0e577fd2ca45f57a6bdfbe0a76f5c5 Mon Sep 17 00:00:00 2001 From: Michelin25 <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:53:38 -0500 Subject: [PATCH 09/12] merging 'main' into 'main' --- cell2fire/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cell2fire/__init__.py b/cell2fire/__init__.py index 8f921a17..ad69b09a 100644 --- a/cell2fire/__init__.py +++ b/cell2fire/__init__.py @@ -1,10 +1,14 @@ -"""Cell2Fire package initialization.""" +"""Compatibility package exposing the historical `Cell2Fire` import path.""" -from importlib.metadata import PackageNotFoundError, version +from importlib import import_module +import sys -try: - __version__ = version("Cell2Fire") -except PackageNotFoundError: # pragma: no cover - fallback for editable/dev installs - __version__ = "0.0.0" +_pkg = import_module("cell2fire") -__all__ = ["__version__"] +# Re-export symbols and share package search path so submodules resolve: +# e.g. `from Cell2Fire.utils.ParseInputs import ParseInputs`. +globals().update(_pkg.__dict__) +__path__ = _pkg.__path__ + +# Ensure both names reference the same loaded package instance. +sys.modules[__name__] = _pkg From fd15a72b95c347139c2adfc18e666e274fb264dd Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:39:53 -0500 Subject: [PATCH 10/12] Revert ParseInputs.py documentation edits --- README.md | 15 +++++++++++++++ cell2fire/Cell2FireC/Cell2Fire.cpp | 15 +++++++++------ cell2fire/Cell2FireC/CellsFBP.cpp | 4 ++++ cell2fire/Cell2FireC_class.py | 9 +++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2875d9c1..45ba725f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,21 @@ For the full list of arguments and their explanation use: $ python main.py -h ``` +For ignition and ROS variability controls: +- `--ROS-CV`: controls stochastic spread-rate variability (`0.0` = deterministic ROS). +- `--ignitions`: reads fixed ignition locations from `Ignitions.csv` in the input folder. +- `--IgnitionRad`: if `--ignitions` is active, samples around each fixed ignition cell. + - `0` => exact CSV cell. + - `>0` => random cell inside the radius neighborhood. + +`Ignitions.csv` is where you directly change ignition location per year (cell IDs): +``` +year,cell +1,930 +2,280 +``` + + In addition, both the C++ core and Python scripts can be used separately: ## C ++ Only simulation and generate evolution grids (no stats or plots). diff --git a/cell2fire/Cell2FireC/Cell2Fire.cpp b/cell2fire/Cell2FireC/Cell2Fire.cpp index 42911635..f428d76a 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.cpp +++ b/cell2fire/Cell2FireC/Cell2Fire.cpp @@ -253,7 +253,10 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv this->CSVWeather.parseWeatherDF(wdf_ptr, this->WeatherDF, WPeriods); //DEBUGthis->CSVWeather.printData(this->WeatherDF); - /* Ignitions */ + /* Ignitions + * - To force ignition location per year: enable --ignitions and edit /Ignitions.csv + * - To randomize ignition location per year: do not pass --ignitions + */ int IgnitionYears; std::vector IgnitionPoints; @@ -277,14 +280,14 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv args.TotalYears = std::min(args.TotalYears, IgnitionYears); //DEBUGstd::cout << "Setting TotalYears to " << args.TotalYears << " for consistency with Ignitions file" << std::endl; - // Ignition points + // Ignition points loaded from Ignitions.csv (year -> cell id) this->IgnitionPoints = std::vector(IgnitionYears, 0); CSVIgnitions.parseIgnitionDF(this->IgnitionPoints, IgnitionsDF, IgnitionYears); //this->IgnitionSets = std::vector>(this->IgnitionPoints.size()); this->IgnitionSets = std::vector>(this->args.TotalYears); - // Ignition radius + // Ignition radius (IgnitionRad): expands each yearly ignition cell to a candidate neighborhood if (this->args.IgnitionRadius > 0){ // Aux int i, a, igVal; @@ -577,7 +580,7 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ std::unordered_map::iterator it; std::uniform_int_distribution distribution(1, this->nCells); - // No Ignitions provided + // No Ignitions.csv mode: ignition cell is sampled uniformly at random each year. if (this->args.Ignitions == 0) { while (true) { // Pick any cell (uniform distribution [a,b]) @@ -629,11 +632,11 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ } } - // Ignitions with provided points from CSV + // Ignitions.csv mode: start from yearly fixed cell and optionally randomize within IgnitionRadius else { int temp = IgnitionPoints[this->year-1]; - // If ignition Radius != 0, sample from the Radius set + // If IgnitionRadius > 0, sample a random candidate from the precomputed neighborhood set if (this->args.IgnitionRadius > 0){ // Pick any at random and set temp with that cell std::uniform_int_distribution udistribution(0, this->IgnitionSets[this->year - 1].size()-1); diff --git a/cell2fire/Cell2FireC/CellsFBP.cpp b/cell2fire/Cell2FireC/CellsFBP.cpp index 8b5ecdda..6915613c 100644 --- a/cell2fire/Cell2FireC/CellsFBP.cpp +++ b/cell2fire/Cell2FireC/CellsFBP.cpp @@ -382,6 +382,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai cartesianAngle += 360; } + // ROSCV controls stochastic spread variation. + // ROSRV is a standard-normal random draw passed from the simulation loop. double ROSRV = 0; if (args->ROSCV > 0) { //std::srand(args->seed); @@ -401,6 +403,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai } // If cell cannot send (thresholds), then it will be burned out in the main loop + // Key formula: scaled ROS = (1 + ROSCV * ROSRV) * deterministic FBP ROS. + // Increase --ROS-CV to increase variability around the deterministic ROS. double HROS = (1 + args->ROSCV * ROSRV) * headstruct.ros * args->HFactor; // Extra debug step for sanity checks diff --git a/cell2fire/Cell2FireC_class.py b/cell2fire/Cell2FireC_class.py index 6d520538..91e12595 100644 --- a/cell2fire/Cell2FireC_class.py +++ b/cell2fire/Cell2FireC_class.py @@ -91,6 +91,10 @@ def run(self): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', self.args.OutFolder if (self.args.OutFolder is not None) else '', + # IGNITION LOCATION CONTROL: + # --ignitions ON => read fixed yearly ignition locations from /Ignitions.csv + # (edit that file to choose the exact ignition cells per year) + # --ignitions OFF => C++ core samples ignition cell uniformly at random each year '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -99,7 +103,10 @@ def run(self): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # ROS VARIABILITY CONTROL: main stochastic spread knob (0.0 = deterministic ROS, >0 adds variability) '--ROS-CV', str(self.args.ROS_CV), + # If --ignitions is enabled, expands each CSV ignition cell to a radius neighborhood + # and samples one cell from that neighborhood each simulation. '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), '--ROS-Threshold', str(self.args.ROS_Threshold), @@ -135,6 +142,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', OutFolder if (OutFolder is not None) else '', + # Same ignition control as in run(): fixed CSV points when enabled, random ignition otherwise. '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -143,6 +151,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # Same ROS variability controls as in run(). '--ROS-CV', str(self.args.ROS_CV), '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), From 252308996327f7a12050ffb7f8a261b3de7200f1 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:58:42 -0500 Subject: [PATCH 11/12] Comment powerline test knobs for ROS-CV and ignition control --- README.md | 15 +++++++++++++++ cell2fire/Cell2FireC/Cell2Fire.cpp | 15 +++++++++------ cell2fire/Cell2FireC/CellsFBP.cpp | 4 ++++ cell2fire/Cell2FireC_class.py | 9 +++++++++ scratch/powerline_loss_test.py | 19 +++++++++++++++++-- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2875d9c1..45ba725f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,21 @@ For the full list of arguments and their explanation use: $ python main.py -h ``` +For ignition and ROS variability controls: +- `--ROS-CV`: controls stochastic spread-rate variability (`0.0` = deterministic ROS). +- `--ignitions`: reads fixed ignition locations from `Ignitions.csv` in the input folder. +- `--IgnitionRad`: if `--ignitions` is active, samples around each fixed ignition cell. + - `0` => exact CSV cell. + - `>0` => random cell inside the radius neighborhood. + +`Ignitions.csv` is where you directly change ignition location per year (cell IDs): +``` +year,cell +1,930 +2,280 +``` + + In addition, both the C++ core and Python scripts can be used separately: ## C ++ Only simulation and generate evolution grids (no stats or plots). diff --git a/cell2fire/Cell2FireC/Cell2Fire.cpp b/cell2fire/Cell2FireC/Cell2Fire.cpp index 42911635..f428d76a 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.cpp +++ b/cell2fire/Cell2FireC/Cell2Fire.cpp @@ -253,7 +253,10 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv this->CSVWeather.parseWeatherDF(wdf_ptr, this->WeatherDF, WPeriods); //DEBUGthis->CSVWeather.printData(this->WeatherDF); - /* Ignitions */ + /* Ignitions + * - To force ignition location per year: enable --ignitions and edit /Ignitions.csv + * - To randomize ignition location per year: do not pass --ignitions + */ int IgnitionYears; std::vector IgnitionPoints; @@ -277,14 +280,14 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv args.TotalYears = std::min(args.TotalYears, IgnitionYears); //DEBUGstd::cout << "Setting TotalYears to " << args.TotalYears << " for consistency with Ignitions file" << std::endl; - // Ignition points + // Ignition points loaded from Ignitions.csv (year -> cell id) this->IgnitionPoints = std::vector(IgnitionYears, 0); CSVIgnitions.parseIgnitionDF(this->IgnitionPoints, IgnitionsDF, IgnitionYears); //this->IgnitionSets = std::vector>(this->IgnitionPoints.size()); this->IgnitionSets = std::vector>(this->args.TotalYears); - // Ignition radius + // Ignition radius (IgnitionRad): expands each yearly ignition cell to a candidate neighborhood if (this->args.IgnitionRadius > 0){ // Aux int i, a, igVal; @@ -577,7 +580,7 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ std::unordered_map::iterator it; std::uniform_int_distribution distribution(1, this->nCells); - // No Ignitions provided + // No Ignitions.csv mode: ignition cell is sampled uniformly at random each year. if (this->args.Ignitions == 0) { while (true) { // Pick any cell (uniform distribution [a,b]) @@ -629,11 +632,11 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ } } - // Ignitions with provided points from CSV + // Ignitions.csv mode: start from yearly fixed cell and optionally randomize within IgnitionRadius else { int temp = IgnitionPoints[this->year-1]; - // If ignition Radius != 0, sample from the Radius set + // If IgnitionRadius > 0, sample a random candidate from the precomputed neighborhood set if (this->args.IgnitionRadius > 0){ // Pick any at random and set temp with that cell std::uniform_int_distribution udistribution(0, this->IgnitionSets[this->year - 1].size()-1); diff --git a/cell2fire/Cell2FireC/CellsFBP.cpp b/cell2fire/Cell2FireC/CellsFBP.cpp index 8b5ecdda..6915613c 100644 --- a/cell2fire/Cell2FireC/CellsFBP.cpp +++ b/cell2fire/Cell2FireC/CellsFBP.cpp @@ -382,6 +382,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai cartesianAngle += 360; } + // ROSCV controls stochastic spread variation. + // ROSRV is a standard-normal random draw passed from the simulation loop. double ROSRV = 0; if (args->ROSCV > 0) { //std::srand(args->seed); @@ -401,6 +403,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai } // If cell cannot send (thresholds), then it will be burned out in the main loop + // Key formula: scaled ROS = (1 + ROSCV * ROSRV) * deterministic FBP ROS. + // Increase --ROS-CV to increase variability around the deterministic ROS. double HROS = (1 + args->ROSCV * ROSRV) * headstruct.ros * args->HFactor; // Extra debug step for sanity checks diff --git a/cell2fire/Cell2FireC_class.py b/cell2fire/Cell2FireC_class.py index 6d520538..91e12595 100644 --- a/cell2fire/Cell2FireC_class.py +++ b/cell2fire/Cell2FireC_class.py @@ -91,6 +91,10 @@ def run(self): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', self.args.OutFolder if (self.args.OutFolder is not None) else '', + # IGNITION LOCATION CONTROL: + # --ignitions ON => read fixed yearly ignition locations from /Ignitions.csv + # (edit that file to choose the exact ignition cells per year) + # --ignitions OFF => C++ core samples ignition cell uniformly at random each year '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -99,7 +103,10 @@ def run(self): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # ROS VARIABILITY CONTROL: main stochastic spread knob (0.0 = deterministic ROS, >0 adds variability) '--ROS-CV', str(self.args.ROS_CV), + # If --ignitions is enabled, expands each CSV ignition cell to a radius neighborhood + # and samples one cell from that neighborhood each simulation. '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), '--ROS-Threshold', str(self.args.ROS_Threshold), @@ -135,6 +142,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', OutFolder if (OutFolder is not None) else '', + # Same ignition control as in run(): fixed CSV points when enabled, random ignition otherwise. '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -143,6 +151,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # Same ROS variability controls as in run(). '--ROS-CV', str(self.args.ROS_CV), '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), diff --git a/scratch/powerline_loss_test.py b/scratch/powerline_loss_test.py index 9195f699..cc7219ca 100644 --- a/scratch/powerline_loss_test.py +++ b/scratch/powerline_loss_test.py @@ -35,7 +35,7 @@ CORE_BINARY = REPO_ROOT / "cell2fire" / "Cell2FireC" / "Cell2Fire" WEATHER_MODE = "rows" NWEATHERS = 1 -USE_PREDEFINED_IGNITIONS = False +USE_PREDEFINED_IGNITIONS = False # True => read yearly ignition cell(s) from GENERATED_DATA_FOLDER/Ignitions.csv def as_c2f_folder_arg(folder: Path) -> str: @@ -169,7 +169,16 @@ def print_logfile_tail(logfile: Path, max_lines: int = 60) -> None: def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: - """Run Cell2Fire C++ core directly (HPC-friendly path).""" + """Run Cell2Fire C++ core directly (HPC-friendly path). + + Quick tuning knobs for this experiment: + - ROS variability: change the value passed to --ROS-CV in `cmd` below. + * 0.0 => deterministic ROS + * >0 => stochastic ROS scaling in the C++ core + - Ignition location mode: toggle USE_PREDEFINED_IGNITIONS (module constant). + * False => random ignition location per simulation/year + * True => fixed ignition location(s) from Ignitions.csv (optionally with --IgnitionRad) + """ if OUTPUT_FOLDER.exists(): shutil.rmtree(OUTPUT_FOLDER) OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True) @@ -186,7 +195,10 @@ def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: "--output-messages", "--weather", WEATHER_MODE, "--nweathers", str(NWEATHERS), + # CHANGE HERE for ROS spread variability between runs (CV of ROS). "--ROS-CV", "0.0", + # CHANGE HERE only when USE_PREDEFINED_IGNITIONS=True: + # 0 => exact cell from Ignitions.csv, >0 => sample around that cell radius. "--IgnitionRad", "0", "--seed", "123", "--ROS-Threshold", "0", @@ -194,6 +206,9 @@ def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: ] if USE_PREDEFINED_IGNITIONS: + # IGNITION LOCATION CONTROL: + # with --ignitions, core reads GENERATED_DATA_FOLDER/Ignitions.csv to pick yearly ignition cell(s). + # without it, ignition location is sampled randomly by the core. cmd.append("--ignitions") logfile = OUTPUT_FOLDER / "LogFile.txt" From 573b52f7b91191fa6fa225c52ca84d773b8f0142 Mon Sep 17 00:00:00 2001 From: Michal Demeter Tvrdon <156027010+Michelin25@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:09:51 -0500 Subject: [PATCH 12/12] Expand powerline test comments for CVROS and ignition controls --- README.md | 15 +++++++++ cell2fire/Cell2FireC/Cell2Fire.cpp | 15 +++++---- cell2fire/Cell2FireC/CellsFBP.cpp | 4 +++ cell2fire/Cell2FireC_class.py | 9 ++++++ scratch/powerline_loss_test.py | 52 ++++++++++++++++++++++++++---- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2875d9c1..45ba725f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,21 @@ For the full list of arguments and their explanation use: $ python main.py -h ``` +For ignition and ROS variability controls: +- `--ROS-CV`: controls stochastic spread-rate variability (`0.0` = deterministic ROS). +- `--ignitions`: reads fixed ignition locations from `Ignitions.csv` in the input folder. +- `--IgnitionRad`: if `--ignitions` is active, samples around each fixed ignition cell. + - `0` => exact CSV cell. + - `>0` => random cell inside the radius neighborhood. + +`Ignitions.csv` is where you directly change ignition location per year (cell IDs): +``` +year,cell +1,930 +2,280 +``` + + In addition, both the C++ core and Python scripts can be used separately: ## C ++ Only simulation and generate evolution grids (no stats or plots). diff --git a/cell2fire/Cell2FireC/Cell2Fire.cpp b/cell2fire/Cell2FireC/Cell2Fire.cpp index 42911635..f428d76a 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.cpp +++ b/cell2fire/Cell2FireC/Cell2Fire.cpp @@ -253,7 +253,10 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv this->CSVWeather.parseWeatherDF(wdf_ptr, this->WeatherDF, WPeriods); //DEBUGthis->CSVWeather.printData(this->WeatherDF); - /* Ignitions */ + /* Ignitions + * - To force ignition location per year: enable --ignitions and edit /Ignitions.csv + * - To randomize ignition location per year: do not pass --ignitions + */ int IgnitionYears; std::vector IgnitionPoints; @@ -277,14 +280,14 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv args.TotalYears = std::min(args.TotalYears, IgnitionYears); //DEBUGstd::cout << "Setting TotalYears to " << args.TotalYears << " for consistency with Ignitions file" << std::endl; - // Ignition points + // Ignition points loaded from Ignitions.csv (year -> cell id) this->IgnitionPoints = std::vector(IgnitionYears, 0); CSVIgnitions.parseIgnitionDF(this->IgnitionPoints, IgnitionsDF, IgnitionYears); //this->IgnitionSets = std::vector>(this->IgnitionPoints.size()); this->IgnitionSets = std::vector>(this->args.TotalYears); - // Ignition radius + // Ignition radius (IgnitionRad): expands each yearly ignition cell to a candidate neighborhood if (this->args.IgnitionRadius > 0){ // Aux int i, a, igVal; @@ -577,7 +580,7 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ std::unordered_map::iterator it; std::uniform_int_distribution distribution(1, this->nCells); - // No Ignitions provided + // No Ignitions.csv mode: ignition cell is sampled uniformly at random each year. if (this->args.Ignitions == 0) { while (true) { // Pick any cell (uniform distribution [a,b]) @@ -629,11 +632,11 @@ bool Cell2Fire::RunIgnition(std::default_random_engine generator){ } } - // Ignitions with provided points from CSV + // Ignitions.csv mode: start from yearly fixed cell and optionally randomize within IgnitionRadius else { int temp = IgnitionPoints[this->year-1]; - // If ignition Radius != 0, sample from the Radius set + // If IgnitionRadius > 0, sample a random candidate from the precomputed neighborhood set if (this->args.IgnitionRadius > 0){ // Pick any at random and set temp with that cell std::uniform_int_distribution udistribution(0, this->IgnitionSets[this->year - 1].size()-1); diff --git a/cell2fire/Cell2FireC/CellsFBP.cpp b/cell2fire/Cell2FireC/CellsFBP.cpp index 8b5ecdda..6915613c 100644 --- a/cell2fire/Cell2FireC/CellsFBP.cpp +++ b/cell2fire/Cell2FireC/CellsFBP.cpp @@ -382,6 +382,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai cartesianAngle += 360; } + // ROSCV controls stochastic spread variation. + // ROSRV is a standard-normal random draw passed from the simulation loop. double ROSRV = 0; if (args->ROSCV > 0) { //std::srand(args->seed); @@ -401,6 +403,8 @@ std::vector CellsFBP::manageFire(int period, std::unordered_set & Avai } // If cell cannot send (thresholds), then it will be burned out in the main loop + // Key formula: scaled ROS = (1 + ROSCV * ROSRV) * deterministic FBP ROS. + // Increase --ROS-CV to increase variability around the deterministic ROS. double HROS = (1 + args->ROSCV * ROSRV) * headstruct.ros * args->HFactor; // Extra debug step for sanity checks diff --git a/cell2fire/Cell2FireC_class.py b/cell2fire/Cell2FireC_class.py index 6d520538..91e12595 100644 --- a/cell2fire/Cell2FireC_class.py +++ b/cell2fire/Cell2FireC_class.py @@ -91,6 +91,10 @@ def run(self): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', self.args.OutFolder if (self.args.OutFolder is not None) else '', + # IGNITION LOCATION CONTROL: + # --ignitions ON => read fixed yearly ignition locations from /Ignitions.csv + # (edit that file to choose the exact ignition cells per year) + # --ignitions OFF => C++ core samples ignition cell uniformly at random each year '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -99,7 +103,10 @@ def run(self): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # ROS VARIABILITY CONTROL: main stochastic spread knob (0.0 = deterministic ROS, >0 adds variability) '--ROS-CV', str(self.args.ROS_CV), + # If --ignitions is enabled, expands each CSV ignition cell to a radius neighborhood + # and samples one cell from that neighborhood each simulation. '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), '--ROS-Threshold', str(self.args.ROS_Threshold), @@ -135,6 +142,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): execArray=[core_bin, '--input-instance-folder', self.args.InFolder, '--output-folder', OutFolder if (OutFolder is not None) else '', + # Same ignition control as in run(): fixed CSV points when enabled, random ignition otherwise. '--ignitions' if (self.args.ignitions) else '', '--sim-years', str(self.args.sim_years), '--nsims', str(self.args.nsims), @@ -143,6 +151,7 @@ def run_Heur(self, OutFolder, HarvestPlanFile): '--output-messages' if (self.args.OutMessages) else '', '--weather', self.args.WeatherOpt, '--nweathers', str(self.args.nweathers), + # Same ROS variability controls as in run(). '--ROS-CV', str(self.args.ROS_CV), '--IgnitionRad', str(self.args.IgRadius), '--seed', str(int(self.args.seed)), diff --git a/scratch/powerline_loss_test.py b/scratch/powerline_loss_test.py index 9195f699..c49caa74 100644 --- a/scratch/powerline_loss_test.py +++ b/scratch/powerline_loss_test.py @@ -35,7 +35,11 @@ CORE_BINARY = REPO_ROOT / "cell2fire" / "Cell2FireC" / "Cell2Fire" WEATHER_MODE = "rows" NWEATHERS = 1 -USE_PREDEFINED_IGNITIONS = False + +# === Experiment knobs (edit these first) === +# True -> pass --ignitions and read year->cell ignition mapping from Ignitions.csv +# False -> do not pass --ignitions, so C++ picks random ignition locations +USE_PREDEFINED_IGNITIONS = False # True => read yearly ignition cell(s) from GENERATED_DATA_FOLDER/Ignitions.csv def as_c2f_folder_arg(folder: Path) -> str: @@ -56,7 +60,9 @@ def copy_common_input_files() -> None: ] # Optional: copy fixed ignitions. - # If this is enabled and sim_years=1, all simulations use the same ignition. + # Important: this only copies the file. The core will actually use it only if + # USE_PREDEFINED_IGNITIONS=True (which appends --ignitions in run_cell2fire_core). + # If sim_years=1, all simulations reference year 1 from Ignitions.csv. if USE_PREDEFINED_IGNITIONS: files_to_copy.append("Ignitions.csv") @@ -169,11 +175,22 @@ def print_logfile_tail(logfile: Path, max_lines: int = 60) -> None: def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: - """Run Cell2Fire C++ core directly (HPC-friendly path).""" + """Run Cell2Fire C++ core directly (HPC-friendly path). + + Quick tuning knobs for this experiment: + - ROS variability: change the value passed to --ROS-CV in `cmd` below. + * 0.0 => deterministic ROS + * >0 => stochastic ROS scaling in the C++ core + - Ignition location mode: toggle USE_PREDEFINED_IGNITIONS (module constant). + * False => random ignition location per simulation/year + * True => fixed ignition location(s) from Ignitions.csv (optionally with --IgnitionRad) + """ if OUTPUT_FOLDER.exists(): shutil.rmtree(OUTPUT_FOLDER) OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True) + # Build CLI args as a plain list so subprocess can run without shell parsing. + # This is easier to debug than a single command string. cmd = [ str(CORE_BINARY), "--input-instance-folder", as_c2f_folder_arg(GENERATED_DATA_FOLDER), @@ -186,7 +203,10 @@ def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: "--output-messages", "--weather", WEATHER_MODE, "--nweathers", str(NWEATHERS), + # CHANGE HERE for ROS spread variability between runs (CV of ROS). "--ROS-CV", "0.0", + # CHANGE HERE only when USE_PREDEFINED_IGNITIONS=True: + # 0 => exact cell from Ignitions.csv, >0 => sample around that cell radius. "--IgnitionRad", "0", "--seed", "123", "--ROS-Threshold", "0", @@ -194,6 +214,9 @@ def run_cell2fire_core(nsims: int = 20, sim_years: int = 1) -> None: ] if USE_PREDEFINED_IGNITIONS: + # IGNITION LOCATION CONTROL: + # with --ignitions, core reads GENERATED_DATA_FOLDER/Ignitions.csv to pick yearly ignition cell(s). + # without it, ignition location is sampled randomly by the core. cmd.append("--ignitions") logfile = OUTPUT_FOLDER / "LogFile.txt" @@ -215,7 +238,11 @@ def get_last_forest_grid_for_sim(sim_index: int) -> Path: def load_final_grids(nsims: int = 20) -> list[np.ndarray]: - """Load final burn grids (0/1) for all simulations.""" + """Load final burn grids (0/1) for all simulations. + + Each final grid is a 2D numpy array with one value per cell. + Convention used here: 1 means burned, other values mean not burned. + """ final_grids: list[np.ndarray] = [] for sim in range(1, nsims + 1): @@ -265,7 +292,11 @@ def plot_burn_probability_map(burn_prob: np.ndarray, output_png: Path) -> None: def evaluate_losses(values: np.ndarray, final_grids: list[np.ndarray]) -> None: - """Calculate hit frequency and loss on power-line cells.""" + """Calculate hit frequency and loss on power-line cells. + + `values` stores economic weight per cell (power-line column has high value). + For each simulation, we sum values only on burned power-line cells. + """ nrows, ncols = values.shape powerline_col_index = (ncols // 2) - 1 powerline_values = values[:, powerline_col_index] @@ -274,6 +305,7 @@ def evaluate_losses(values: np.ndarray, final_grids: list[np.ndarray]) -> None: losses = [] for final_grid in final_grids: + # Boolean mask over rows in the power-line column. burned_powerline = final_grid[:, powerline_col_index] == 1 loss_this_sim = float(np.sum(powerline_values[burned_powerline])) losses.append(loss_this_sim) @@ -296,7 +328,15 @@ def evaluate_losses(values: np.ndarray, final_grids: list[np.ndarray]) -> None: def main() -> None: - """Create dataset, run Cell2Fire core, summarize losses, and plot burn probability.""" + """Create dataset, run Cell2Fire core, summarize losses, and plot burn probability. + + High-level flow: + 1) Build synthetic inputs in data/PowerLine100x100 + 2) Run the C++ core executable + 3) Read final grids and compute loss statistics + 4) Save a burn-probability PNG + """ + # You can change scenario size/replications here. nrows = 100 ncols = 100 nsims = 20