diff --git a/configs/historical_config.yml b/configs/historical_config.yml index d1eddb7..9096e4a 100644 --- a/configs/historical_config.yml +++ b/configs/historical_config.yml @@ -16,7 +16,7 @@ simulation: headless: false draw_spread_graph: false record: true - save_data: true + save_data: false data_type: "npy" sf_home: "~/.simfire" @@ -27,8 +27,8 @@ operational: seed: latitude: 38.422 # top left corner longitude: -118.266 # top left corner - height: 2000 # in meters - width: 2000 # in meters + height: 12_000 # in meters + width: 12_000 # in meters resolution: 30 # in meters year: 2020 @@ -38,10 +38,12 @@ historical: year: 2020 state: "California" fire: "mineral" + height: 12_000 # in meters + width: 12_000 # in meters terrain: topography: - type: operational + type: historical functional: function: perlin perlin: @@ -58,13 +60,13 @@ terrain: sigma_x: 50 sigma_y: 50 fuel: - type: operational + type: historical functional: function: chaparral chaparral: seed: 1113 burn_probability: - type: operational + type: historical functional: function: perlin perlin: @@ -96,7 +98,7 @@ environment: moisture: 0.001 wind: - function: perlin + function: simple cfd: time_to_train: 1000 result_accuracy: 1 @@ -108,8 +110,8 @@ wind: speed: 19.0 direction: north simple: - speed: 7 - direction: 90.0 + speed: 15 + direction: 135 perlin: speed: seed: 2345 diff --git a/simfire/utils/config.py b/simfire/utils/config.py index 1ddcc7f..0435084 100644 --- a/simfire/utils/config.py +++ b/simfire/utils/config.py @@ -150,6 +150,8 @@ class HistoricalConfig: year: int state: str fire: str + height: int + width: int @dataclasses.dataclass @@ -225,6 +227,12 @@ def __init__( # operational to functional self.original_screen_size = self.yaml_data["area"]["screen_size"] + if ( + self.yaml_data["terrain"]["topography"]["type"] == "historical" + or self.yaml_data["terrain"]["fuel"]["type"] == "historical" + ): + self.historical = self._load_historical() + self.historical_layer = self._create_historical_layer() # This can take up to 30 seconds to pull LandFire data directly from source self.landfire_lat_long_box = self._make_lat_long_box() @@ -233,7 +241,6 @@ def __init__( self.simulation = self._load_simulation() self.mitigation = self._load_mitigation() self.operational = self._load_operational() - self.historical = self._load_historical() self.terrain = self._load_terrain() self.fire = self._load_fire() self.environment = self._load_environment() @@ -333,6 +340,11 @@ def _make_lat_long_box(self) -> Optional[LandFireLatLongBox]: width=width, ) return landfire_lat_long_box + elif ( + self.yaml_data["terrain"]["topography"]["type"] == "historical" + or self.yaml_data["terrain"]["fuel"]["type"] == "historical" + ): + return self.historical_layer.lat_lon_box else: return None @@ -580,6 +592,10 @@ def _create_topography_layer( fn, fn_name, ) + elif topo_type == "historical": + topo_layer = self.historical_layer.topography + fn_name = None + kwargs = None else: raise ConfigError( f"The specified topography type ({topo_type}) " "is not supported" @@ -650,6 +666,10 @@ def _create_burn_probability_layer( fn, fn_name, ) + elif bp_type == "historical": + burn_prob_layer = None + fn_name = None + kwargs = None else: raise ConfigError( f"The specified topography type ({bp_type}) " "is not supported" @@ -702,6 +722,10 @@ def _create_fuel_layer( fn, fn_name, ) + elif fuel_type == "historical": + fuel_layer = self.historical_layer.fuel + fn_name = None + kwargs = None else: raise ConfigError( f"The specified fuel type ({fuel_type}) " "is not supported" @@ -714,15 +738,15 @@ def _create_historical_layer(self): This is an optional dataclass. Returns: - A HIstoricalLayer that sets the screen size, area, and fire start location. + A HistoricalLayer that utilizes the data specified in the config. """ - historical_config = self._load_historical() historical_layer = HistoricalLayer( - historical_config.year, - historical_config.state, - historical_config.fire, - historical_config.path, - self.yaml_data["area"]["screen_size"], + self.historical.year, + self.historical.state, + self.historical.fire, + self.historical.path, + self.historical.height, + self.historical.width, ) return historical_layer @@ -804,7 +828,7 @@ def _load_wind(self) -> WindConfig: self.yaml_data["area"]["screen_size"][0], self.yaml_data["area"]["screen_size"][1], ) - speed = self.yaml_data["wind"]["simple"]["speed"] + speed = mph_to_ftpm(self.yaml_data["wind"]["simple"]["speed"]) direction = self.yaml_data["wind"]["simple"]["direction"] speed_arr = np.full(arr_shape, speed) direction_arr = np.full(arr_shape, direction) diff --git a/simfire/utils/layers.py b/simfire/utils/layers.py index a1fff16..40a46ea 100644 --- a/simfire/utils/layers.py +++ b/simfire/utils/layers.py @@ -24,6 +24,7 @@ FuelModelToFuel, ) from ..utils.log import create_logger +from ..utils.units import meters_to_feet from ..world.elevation_functions import ElevationFn from ..world.fuel_array_functions import FuelArrayFn from ..world.parameters import Fuel @@ -117,7 +118,7 @@ def __init__( self.fuel = self.fuel[:pixel_height, :pixel_width] self.topography = self.topography[:pixel_height, :pixel_width] - print( + log.debug( f"Output shape of Fire Map: {height}m x {width}m " f"--> {self.fuel.shape} in pixel space" ) @@ -267,8 +268,8 @@ def _make_data(self): def create_lat_lon_array(self) -> np.ndarray: """ We will need to be able to map between the geospatial data and - the numpy arrays that SimFire uses. To do this, we will create a - secondary np.ndarray of tuples of lat/lon data. + the numpy arrays that SimFire uses. To do this, we will create a + secondary np.ndarray of tuples of lat/lon data. This will especially get used with the HistoricalLayer @@ -539,7 +540,7 @@ def _get_data(self): """Functionality to get the raw elevation data for the SimHarness""" # Convert from meters to feet for use with simulator - data_array = 3.28084 * self.LandFireLatLongBox.topography + data_array = meters_to_feet(self.LandFireLatLongBox.topography) return data_array @@ -776,22 +777,22 @@ def __init__( state: str, fire: str, path: Union[Path, str], - screen_size: Tuple[int, int], height: int, width: int, ) -> None: - """Functionality to load the apporpriate/available historical data + """ + Functionality to load the apporpriate/available historical data given a fire name, the state, and year of the fire. NOTE: Currently, this layer supports adding ALL mitigations to the firemap at initialization - TODO: Add mitigations based upon runtime and implementation time + TODO: Add mitigations based upon runtime and implementation time NOTE: This layer includes functionality to calculate IoU of final perimeter - given the BurnMD timestamp and simulation duration for validation - purposes. - TODO: Move this functionality to a utilities file - TODO: Include intermediary perimeters and simulation runtime + given the BurnMD timestamp and simulation duration for validation + purposes. + TODO: Move this functionality to a utilities file + TODO: Include intermediary perimeters and simulation runtime Arguments: year: The year of the historical fire. @@ -799,8 +800,8 @@ def __init__( and is case-sensitive. fire: The individual fire given the year and state. This is case-sensitive. path: The path to the BurnMD dataset. - screen_size: The size of the screen in pixels. This is a tuple of - (height, width). + height: The height of the screen size (meters). + width: The width of the screen size (meters). """ self.year = year self.state = state @@ -809,8 +810,6 @@ def __init__( self.height = height self.width = width - self.screen_size = screen_size - # Format to convert BurnMD timestamp to datetime object self.strptime_fmt = "%Y/%m/%d %H:%M:%S.%f" # set the path @@ -824,7 +823,10 @@ def __init__( self.lat_lon_box = LandFireLatLongBox( self.points, year=self.year, height=self.height, width=self.width ) + self.topography = OperationalTopographyLayer(self.lat_lon_box) + self.fuel = OperationalFuelLayer(self.lat_lon_box) self.lat_lon_array = self.lat_lon_box.create_lat_lon_array() + self.screen_size = self.lat_lon_array.shape[:2] self.start_time = self.polygons_df.iloc[0]["DateStart"] self.end_time = self.polygons_df.iloc[0]["DateContai"] # get the duraton of fire specified @@ -840,7 +842,7 @@ def _get_historical_data(self) -> None: ) except ValueError: polygons_df = geopandas.GeoDataFrame() - print("There is no perimeter data for this wildfire.") + log.warning("There is no perimeter data for this wildfire.") try: lines_df = geopandas.read_file( @@ -848,7 +850,7 @@ def _get_historical_data(self) -> None: ) except ValueError: lines_df = geopandas.GeoDataFrame() - print("There is no mitigation data for this wildfire.") + log.warning("There is no mitigation data for this wildfire.") # Add a column with a datetime object for ease of future computation polygons_df.insert( @@ -867,44 +869,37 @@ def _get_historical_data(self) -> None: self.lines_df = lines_df def _get_bounds_of_screen(self): - """Collect the extent of the historical data to set screen + """ + Collect the extent of the historical data to set screen Returns: ((maxy, maxx), (miny, minx)) """ if len(self.lines_df) > 0: - # Calculate the bounds of the operational data using mitigations - # most left (west) bound - maxx = min( - min(self.lines_df.geometry.bounds.minx), - min(self.lines_df.geometry.bounds.maxx), - ) - # most right (east) bound - minx = max( - max(self.lines_df.geometry.bounds.maxx), - max(self.lines_df.geometry.bounds.minx), - ) - # most bottom (south) bound - miny = min(self.lines_df.geometry.bounds.miny) - # most top (north) bound - maxy = max(self.lines_df.geometry.bounds.maxy) + df = self.lines_df else: - # Calculate the bounds of the operational data using polygon data - maxx = min( - min(self.polygons_df.geometry.bounds.minx), - min(self.polygons_df.geometry.bounds.maxx), - ) - minx = max( - max(self.polygons_df.geometry.bounds.maxx), - max(self.polygons_df.geometry.bounds.minx), - ) - miny = min(self.polygons_df.geometry.bounds.miny) - maxy = max(self.polygons_df.geometry.bounds.maxy) + df = self.polygons_df + + # most left (west) bound + maxx = min( + min(df.geometry.bounds.minx), + min(df.geometry.bounds.maxx), + ) + # most right (east) bound + minx = max( + max(df.geometry.bounds.maxx), + max(df.geometry.bounds.minx), + ) + # most bottom (south) bound + miny = min(df.geometry.bounds.miny) + # most top (north) bound + maxy = max(df.geometry.bounds.maxy) return ((maxy, maxx), (miny, minx)) def _get_fire_init_pos(self): - """Get the embedded fire initial position (approximation) + """ + Get the embedded fire initial position (approximation) Returns: (latitude, longitude) @@ -918,7 +913,8 @@ def _get_fire_init_pos(self): def make_mitigations( self, start_time: datetime.datetime, end_time: datetime.datetime ) -> np.ndarray: - """Method to add mitigation locations to the firemap at the start of the + """ + Method to add mitigation locations to the firemap at the start of the simulation. Return an array of the mitigation type as an enum that gets passed into the @@ -973,33 +969,34 @@ def _get_geometry(df): points = hand_lines.iloc[i] for p in points: y, x = get_closest_indice(self.lat_lon_array, (p[1], p[0])) - array_points.append((x, y)) - mitigation_array[x, y] = BurnStatus.SCRATCHLINE + array_points.append((y, x)) + mitigation_array[y, x] = BurnStatus.SCRATCHLINE # need to interpolate points in this line for idx in range(len(array_points) - 1): coords = np.linspace(array_points[idx], array_points[idx + 1]) coords = np.unique(coords.astype(int), axis=0) - for x, y in coords: - mitigation_array[x, y] = BurnStatus.SCRATCHLINE + for y, x in coords: + mitigation_array[y, x] = BurnStatus.SCRATCHLINE for i in range(len(dozer_lines)): array_points = [] points = dozer_lines.iloc[i] for p in points: y, x = get_closest_indice(self.lat_lon_array, (p[1], p[0])) - array_points.append((x, y)) - mitigation_array[x, y] = BurnStatus.FIRELINE + array_points.append((y, x)) + mitigation_array[y, x] = BurnStatus.FIRELINE # need to interpolate points in this line for idx in range(len(array_points) - 1): coords = np.linspace(array_points[idx], array_points[idx + 1]) coords = np.unique(coords.astype(int), axis=0) - for x, y in coords: - mitigation_array[x, y] = BurnStatus.FIRELINE + for y, x in coords: + mitigation_array[y, x] = BurnStatus.FIRELINE return mitigation_array def _calc_time_elapsed(self, start_time: str, end_time: str) -> str: - """Calculate the time between each timestamp with format: + """ + Calculate the time between each timestamp with format: YYYY/MM/DD HRS:MIN:SEC.0000 Arguments: @@ -1068,7 +1065,8 @@ def _get_geometry(df): return out_image def _get_perimeter_time_deltas(self): - """Use `_calc_time_elapsed` functionality to get a list of time elapsed between + """ + Use `_calc_time_elapsed` functionality to get a list of time elapsed between perimeters. This can be used in `simulation.run()`to incremently add time to the simulation. @@ -1102,16 +1100,17 @@ def _get_perimeter_time_deltas(self): def get_closest_indice( lat_lon_data: np.ndarray, point: Tuple[float, float] ) -> Tuple[int, int]: - """Utility function to help find the closest index for the geospatial point. + """ + Utility function to help find the closest index for the geospatial point. Arguments: - lat_lon_data: array of the (h, w, (lat, lon)) data of the screen_size - of the simulation + lat_lon_data: array of the (h, w, (lat, lon)) data of the screen_size of the + simulation point: a tuple pair of lat/lon point. [longitude, latitude] Returns: - x, y: tuple pair of index in lat/lon array that corresponds to - the simulation array index + y, x: tuple pair of index in lat/lon array that corresponds to + the simulation array index """ idx = np.argmin( diff --git a/simfire/utils/units.py b/simfire/utils/units.py index 21b176a..71f1742 100644 --- a/simfire/utils/units.py +++ b/simfire/utils/units.py @@ -85,6 +85,21 @@ def str_to_minutes(string: str) -> int: ) +def meters_to_feet( + meters: Union[int, float, np.ndarray] +) -> Union[int, float, np.ndarray]: + """ + Convert meters to feet + + Arguments: + meters: The distance in meters. + + Returns: + The distance in feet. + """ + return meters * 3.28084 + + def chains_to_feet_handline(chains: float) -> Tuple[int, int]: """ Convert "chains" to (width x hieght) / hour per individual firefighters. diff --git a/tests/historical_test.py b/tests/historical_test.py index 2f6dae6..816389a 100644 --- a/tests/historical_test.py +++ b/tests/historical_test.py @@ -1,21 +1,17 @@ from datetime import timedelta +import numpy as np + +from simfire.enums import BurnStatus from simfire.sim.simulation import FireSimulation from simfire.utils.config import Config -from simfire.utils.layers import HistoricalLayer config = Config("configs/historical_config.yml") sim = FireSimulation(config) -hist_layer = HistoricalLayer( - str(config.historical.year), - config.historical.state, - config.historical.fire, - config.historical.path, - config.area.screen_size, - config.operational.height, - config.operational.width, -) +sim.rendering = True + +hist_layer = config.historical_layer update_minutes = 1 * 60 update_interval = f"{update_minutes}m" @@ -23,9 +19,17 @@ current_time = hist_layer.convert_to_datetime(hist_layer.start_time) end_time = hist_layer.convert_to_datetime(hist_layer.end_time) while current_time < end_time: + mitigation_iterable = [] mitigations = hist_layer.make_mitigations( current_time, current_time + update_interval_datetime ) - sim.fire_map[mitigations != 0] = mitigations[mitigations != 0] + locations = np.argwhere(mitigations != 0) + try: + mitigation_iterable = np.insert(locations, 2, BurnStatus.FIRELINE.value, axis=1) + except IndexError: + mitigation_iterable = [] + sim.update_mitigation(mitigation_iterable) sim.run(update_interval) current_time += update_interval_datetime + +sim.save_gif()