diff --git a/cell2fire/Cell2FireC/Cell2Fire.cpp b/cell2fire/Cell2FireC/Cell2Fire.cpp index 8184f62..69c5526 100644 --- a/cell2fire/Cell2FireC/Cell2Fire.cpp +++ b/cell2fire/Cell2FireC/Cell2Fire.cpp @@ -203,6 +203,7 @@ Cell2Fire::Cell2Fire(arguments _args) : CSVWeather(_args.InFolder + "Weather.csv } // Harvested cells + // FIXME: WTF does this do? if(strcmp(this->args.HarvestPlan.c_str(), EM) != 0){ std::string sep = ","; CSVReader CSVHPlan(this->args.HarvestPlan, sep); diff --git a/cell2fire/Cell2FireC/Makefile_willshen b/cell2fire/Cell2FireC/Makefile_willshen new file mode 100644 index 0000000..ef5257a --- /dev/null +++ b/cell2fire/Cell2FireC/Makefile_willshen @@ -0,0 +1,47 @@ +EIGENDIR = /Users/admin/workspace/eigen-3.4.0/ +CC = g++ +MPCC = g++ +OPENMP = -openmp +CFLAGS = -std=c++11 -O3 -I$(EIGENDIR) +LIBS = -m64 -fPIC -fno-strict-aliasing -fexceptions -DNDEBUG -DIL_STD -lm -lpthread -ldl + +TARGETS = Cell2Fire + +all: $(TARGETS) + +Cell2Fire: Cell2Fire.o CellsFBP.o FBPfunc5_NoDebug.o SpottingFBP.o ReadCSV.o ReadArgs.o Lightning.o WriteCSV.o Ellipse.o + $(CC) -o $@ $(LIBS) -Xclang -fopenmp -lomp Cell2Fire.o CellsFBP.o FBPfunc5_NoDebug.o SpottingFBP.o ReadCSV.o ReadArgs.o Lightning.o WriteCSV.o Ellipse.o + +Cell2Fire.o: Cell2Fire.cpp CellsFBP.o FBPfunc5_NoDebug.o SpottingFBP.o ReadCSV.o ReadArgs.o WriteCSV.o + $(CC) -c $(CFLAGS) -Xclang -fopenmp -lomp Cell2Fire.cpp + +SpottingFBP.o: SpottingFBP.cpp SpottingFBP.h CellsFBP.h + $(CC) -c $(CFLAGS) SpottingFBP.cpp CellsFBP.h + +CellsFBP.o: CellsFBP.cpp CellsFBP.h FBPfunc5_NoDebug.o + $(CC) -c $(CFLAGS) CellsFBP.cpp + +FBPfunc5_NoDebug.o: FBPfunc5_NoDebug.c FBP5.0.h + $(CC) -c $(CFLAGS) FBPfunc5_NoDebug.c + +ReadCSV.o: ReadCSV.cpp ReadCSV.h FBPfunc5_NoDebug.o + $(CC) -c $(CFLAGS) ReadCSV.cpp + +ReadArgs.o: ReadArgs.cpp ReadArgs.h + $(CC) -c $(CFLAGS) ReadArgs.cpp + +Lightning.o: Lightning.cpp Lightning.h + $(CC) -c $(CFLAGS) Lightning.cpp + +Forest.o: Forest.cpp Forest.h + $(CC) -c $(CFLAGS) Forest.cpp + +WriteCSV.o: WriteCSV.cpp WriteCSV.h + $(CC) -c $(CFLAGS) WriteCSV.cpp + +Ellipse.o: Ellipse.cpp Ellipse.h + $(CC) -c $(LIBS) $(CFLAGS) Ellipse.cpp + + +clean: + rm Lightning.o ReadArgs.o ReadCSV.o FBPfunc5_NoDebug.o Cell2Fire.o CellsFBP.o Cell2Fire SpottingFBP.o Forest.o WriteCSV.o Ellipse.o *.gch diff --git a/cell2fire/firehose/models.py b/cell2fire/firehose/models.py index 1bb2e71..3d9a474 100644 --- a/cell2fire/firehose/models.py +++ b/cell2fire/firehose/models.py @@ -1,19 +1,46 @@ import os +from dataclasses import dataclass from functools import cached_property -from typing import NamedTuple +from typing import NamedTuple, List, ClassVar import numpy as np - from utils.ReadDataPrometheus import Dictionary -class IgnitionPoint(NamedTuple): - x: int = 0 - y: int = 0 - radius: int = 0 +@dataclass(frozen=True) +class IgnitionPoint: + idx: int + year: int + + +@dataclass(frozen=True) +class IgnitionPoints: + points: List[IgnitionPoint] + + # This is a class variable as it's fixed as input for + # all ignition points. 0 is default used in parser. + RADIUS: ClassVar[int] = 0 + CSV_NAME: ClassVar[str] = "Ignitions.csv" + + @property + def year(self) -> int: + year = [p.year for p in self.points] + assert len(set(year)) == 1, "All ignition points must have the same year" + return year[0] + + def get_csv(self) -> str: + csv_list = ["Year,Ncell"] + for point in self.points: + csv_list.append(f"{point.year},{point.idx}") + return "\n".join(csv_list) + + def write_to_csv(self, path: str) -> None: + with open(path, "w") as f: + f.write(self.get_csv()) -class ExperimentHelper(NamedTuple): +@dataclass(frozen=True) +class ExperimentHelper: base_dir: str map: str @@ -33,7 +60,8 @@ def forest_datafile(self) -> str: def output_folder(self) -> str: return "{}/../results/{}/".format(self.base_dir, self.map) - def load_forest_image(self) -> np.ndarray: + @cached_property + def forest_image(self) -> np.ndarray: # Load in the raw forest image forest_image_data = np.loadtxt(self.forest_datafile, skiprows=6) @@ -51,3 +79,33 @@ def load_forest_image(self) -> np.ndarray: forest_image[x, y] = fb_dict[str(int(forest_image_data[x, y]))][:3] return forest_image + + def generate_random_ignition_points( + self, num_points: int = 1, year: int = 1, radius: int = IgnitionPoints.RADIUS + ) -> IgnitionPoints: + """ + Generates random ignition points. + + :param num_points: number of ignition points to generate + :param year: the year, we only support one year for now + :param radius: the radius of the ignition point, default to 0 + :return: List of ignition points, which are tuples with + (year, index of cell with the ignition point) + """ + height, width = self.forest_image.shape[:2] + available_idxs = np.arange(width * height) + + # Set radius class variable + IgnitionPoints.RADIUS = radius + + # TODO: check type of vegetation in forest image or do we not care? + # we should probably otherwise there could be no ignition and loop fails + ignition_points = np.random.choice(available_idxs, num_points, replace=False) + + ignition_points = IgnitionPoints( + points=[ + IgnitionPoint(point, year + idx) + for idx, point in enumerate(ignition_points) + ] + ) + return ignition_points diff --git a/cell2fire/firehose/process.py b/cell2fire/firehose/process.py index 9b4aa22..4896f2a 100644 --- a/cell2fire/firehose/process.py +++ b/cell2fire/firehose/process.py @@ -1,25 +1,58 @@ +import os +import shutil import subprocess -from typing import Optional +from datetime import datetime +from typing import Optional, TYPE_CHECKING -from firehose.models import ExperimentHelper +from firehose.models import IgnitionPoints -_COMMAND_STR = "{} --input-instance-folder {} --output-folder {} --ignitions --sim-years 1 \ +if TYPE_CHECKING: + # Workaround for circular imports as I can't be bothered refactoring + from gym_env import FireEnv + +_COMMAND_STR = "{binary} --input-instance-folder {input} --output-folder {output} --ignitions --sim-years {sim_years} \ --nsims 1 --grids --final-grid --Fire-Period-Length 1.0 --output-messages \ - --weather rows --nweathers 1 --ROS-CV 0.5 --IgnitionRad 0 --seed 123 --nthreads 1 \ + --weather rows --nweathers 1 --ROS-CV 0.5 --IgnitionRad {ignition_radius} --seed 123 --nthreads 1 \ --ROS-Threshold 0.1 --HFI-Threshold 0.1 --HarvestPlan" class Cell2FireProcess: # TODO: detect if process throws an error? - def __init__(self, helper: ExperimentHelper): + def __init__(self, env: "FireEnv"): + command_str = _COMMAND_STR.format( - helper.binary_path, helper.data_folder, helper.output_folder + binary=env.helper.binary_path, + input=self.manipulate_input_data_folder(env), + output=env.helper.output_folder, + ignition_radius=IgnitionPoints.RADIUS, + sim_years=1, ) + self._command_str_args = command_str.split(" ") self.process: Optional[subprocess.Popen] = None + def manipulate_input_data_folder( + self, env: "FireEnv", experiment_dir="/tmp/firehose" + ) -> str: + # Copy input data folder to new one we generate + datetime_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + tmp_dir = os.path.join(experiment_dir, f"{env.helper.map}_{datetime_str}") + shutil.copytree(env.helper.data_folder, tmp_dir) + + # Delete existing ignition points and write our ignition points + ignition_points_csv = os.path.join(tmp_dir, IgnitionPoints.CSV_NAME) + os.remove(ignition_points_csv) + env.ignition_points.write_to_csv(ignition_points_csv) + + print(f"Copied modified input data folder to {tmp_dir}") + # This adds a trailing slash if its required + tmp_dir = os.path.join(tmp_dir, "") + return tmp_dir + def spawn(self): + print("Spawning cell2fire process") + print("Command:", " ".join(self._command_str_args)) self.process = subprocess.Popen( self._command_str_args, stdout=subprocess.PIPE, diff --git a/cell2fire/gym_env.py b/cell2fire/gym_env.py index d4e20e7..b724719 100644 --- a/cell2fire/gym_env.py +++ b/cell2fire/gym_env.py @@ -8,47 +8,62 @@ from gym import Env, spaces from gym.spaces import Discrete, Box -from firehose.models import IgnitionPoint, ExperimentHelper +from firehose.models import IgnitionPoint, ExperimentHelper, IgnitionPoints from firehose.process import Cell2FireProcess ENVS = [] _MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) + def fire_size_reward(state, forest, scale=10): idxs = np.where(state > 0) - return -len(idxs[0])/(forest.shape[0]*forest.shape[1])*scale + return -len(idxs[0]) / (forest.shape[0] * forest.shape[1]) * scale + class FireEnv(Env): def __init__( self, - fire_map: str = "Sub20x20", + fire_map: str = "Harvest40x40", max_steps: int = 200, - ignition_points: Optional[List[IgnitionPoint]] = None, - reward_func = fire_size_reward + ignition_points: Optional[IgnitionPoints] = None, + reward_func=fire_size_reward, + num_ignition_points: int = 5, # if ignition_points is specified this is ignored ): - if not ignition_points: - ignition_points = [IgnitionPoint()] # TODO: Create the process with the input map self.iter = 0 self.max_steps = max_steps # Helper code self.helper = ExperimentHelper(base_dir=_MODULE_DIR, map=fire_map) - self.forest_image = self.helper.load_forest_image() + self.forest_image = self.helper.forest_image - self.action_space = Discrete(self.forest_image.shape[0]*self.forest_image.shape[1]) - self.observation_space = spaces.Box(low=0, high=255, - shape=(self.forest_image.shape[0], self.forest_image.shape[1]), dtype=np.uint8) + # Randomly generate ignition points if required + if not ignition_points: + self.ignition_points = self.helper.generate_random_ignition_points( + num_points=num_ignition_points, + ) + else: + self.ignition_points = ignition_points + + self.action_space = Discrete( + self.forest_image.shape[0] * self.forest_image.shape[1] + ) + self.observation_space = spaces.Box( + low=0, + high=255, + shape=(self.forest_image.shape[0], self.forest_image.shape[1]), + dtype=np.uint8, + ) self.state = np.zeros((self.forest_image.shape[0], self.forest_image.shape[1])) - # Cell2Fire Process - self.fire_process = Cell2FireProcess(self.helper) - - # TODO: pass these into the binary - self.ignition_points = ignition_points # Reward function self.reward_func = reward_func + # NOTE: this should be the last thing that is done after we have set all the + # relevant properties in this instance + # Cell2Fire Process + self.fire_process = Cell2FireProcess(self) + def step(self, action, debug: bool = True): # if debug: # print(action, "step") @@ -60,7 +75,7 @@ def step(self, action, debug: bool = True): # print(result) # assert len(result)>0 - value = str(action+1) + "\n" + value = str(action + 1) + "\n" value = bytes(value, "UTF-8") self.fire_process.write_action(value)