Base Optimization Project

Copyright © 2024, United States Government, as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.

The “"Fault Model Design tools - fmdtools version 2"” software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Model Overview

from fmdtools.sim import propagate
from fmdtools.define.architecture.function import FunctionArchitectureGraph
from examples.airspacelib.wildfireresponse.environment import FireMap, FireEnvironment, FirePropagation, sim_properties
from examples.airspacelib.wildfireresponse.aircraft import FireAircraft
from examples.airspacelib.wildfireresponse.wildfiresim import WildfireSim, plot_combined_response_from, double_size_p
from examples.airspacelib.wildfireresponse.wildfiresim import BasePlacementProblem
from IPython.display import HTML
import inspect

Fire Map/Propagation

FireMap defines the map that the fire propagates over.

sim_properties
{'grass': {'color': 'lightgreen'},
 'forest': {'color': 'darkgreen'},
 'scrub': {'color': 'gold'},
 'burning': {'color': 'red', 'as_bool': True, 'alpha': 0.5},
 'base': {'color': 'black'},
 'to_burn': {'color': 'yellow', 'as_bool': True, 'alpha': 0.5},
 'extinguished': {'color': 'grey'}}
fm = FireMap()
fm

fm.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/df29279642b8384befe033ade9c5500c7d805237dca69cdc44a82046ef9dcb94.png

FireMap has a number of parameters that can be modified to simulate different types of fires:

fm.p
print(inspect.getsource(fm.p.__class__))
class FireMapParam(CoordsParam):
    """
    Parameter defining the fire map.

    Parameters
    ----------
    x_size: int
        Number of grid cells in the x. Default is 10.
    y_size: int
        Number of grid cells in the y. Default is 10.
    blocksize: float
        Size of grid cels. Default is 5.0, or 5 kilometers.
    base_locations: tuple
        Locations in the grid to put a base. Default is  ((0.0, 0.0),).
    num_strikes: int
        Number of strikes to initiate in the grid. Default is 1.
    map_type: str
        Type of map, specified as "uniform-xx","split-xx-yy", or "xx-yy-zz", where
        xx, yy, and zz are "grass", "forest", or "scrub" fuels
    grass_ig_time : float
        Grass cell ignition time. Default is 50.0 minutes
    grass_ex_time : float
        Grass cell extinguish time. Default is 90.0 minutes.
    scrub_ig_time : float
        Scrub cell ignition time. Default is 75.0 minutes
    scrub_ex_time : float
        Scrub cell extinguish time. Default is 200.0 minutes.
    forest_ig_time : float
        Forest cell ignition time. Default is 100.0 minutes
    grass_ex_time : float
        Forest cell extinguish time. Default is 400.0 minutes.
    """

    x_size: int = 10
    y_size: int = 10
    blocksize: float = 5.0  # 5 kilometers
    base_locations: tuple = ((0.0, 0.0),)
    num_strikes: int = 1
    map_type: str = "uniform-grass"
    grass_ig_time: float = 50.0  # 5 km every 50 timesteps (~3mph)
    grass_ex_time: float = 90.0
    scrub_ig_time: float = 75.0
    scrub_ex_time: float = 200.0
    forest_ig_time: float = 100.0
    forest_ex_time: float = 400.0

The strike location is determined at the start of the simulation to be random based on the number of strikes.

We can chose between a few different types of firemaps as follows:

fm_bi = FireMap(p={'map_type': 'split-forest-grass'})
fm_bi.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/3bce7abaed94b122b06d5be3f35eefdfd38ed7e7e7d4fbc325a2f18a022b1ee0.png
fm_tri = FireMap(p={'map_type': 'forest-grass-scrub'})
fm_tri.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/afba06c2a8f525bffee76bee3608935aa39c11ba02b25273c5325549bc297ccb.png

we can also add base locations:

fm_bases = FireMap(p={'map_type': 'forest-grass-scrub', 'base_locations': ((20, 20),( 40, 40), (20, 40), (40, 20))})
fm_bases.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/4cfe695d8437f23864f3ada44cbdc32282c1a1ddd7851d3357270c1746411d52.png

and add more or less strike locations:

fm_three_strike = FireMap(p={'map_type': 'forest-grass-scrub', 'num_strikes': 3})
fm_three_strike.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/7dc4e0489f34edac6737a7f2f2d65e3f05c1772f2b03285103f84e5eade0b556.png

Different samples can further be drawn by changing the random seed:

fm_three_strike = FireMap(p={'map_type': 'forest-grass-scrub', 'num_strikes': 3}, r={'seed': 250})
fm_three_strike.show(sim_properties)
(<Figure size 500x500 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../../../_images/81658fa34bf8eaf7d80f575b42df350148388e54ab3b47e9336d291d2ef0a47a.png

Propagation of the fire is called from FirePropagation.

fp = FirePropagation(track='all', fireenvironment=FireEnvironment(c={'p': {'map_type': 'forest-grass-scrub', 'num_strikes': 3}}))
fp
firepropagation FirePropagation
- t=Time(time=-0.1, timers={})
- s=FirePropagationState(perc_burned=0.0, leading_edge_length=0)
- fireenvironment=FireEnvironment(c=(), ga=())
fig, ax = fp.fireenvironment.c.show(properties=sim_properties)
../../../_images/7dc4e0489f34edac6737a7f2f2d65e3f05c1772f2b03285103f84e5eade0b556.png

Below we simulate and visualize the propagation of the fire over time:

res, hist = propagate.nominal(fp, protect=False)
ani = fp.fireenvironment.c.animate(hist.fireenvironment.c, properties=sim_properties)
# HTML(ani.to_jshtml())
../../../_images/3973bc95ea8b0aa48f520a633cc652d871b3e91aa5eefa337809b7d73425e90b.png

Aircraft

Aircraft fly from bases to mitigate the fire. They are defined in the Aircraft module.class

fe = FireEnvironment(c={"p": {"base_locations": ((40.0, 20.0),), "num_strikes": 3}})
fe.prop_time()
a1 = FireAircraft(s={'goal_x': 30, 'goal_y': 40}, fireenvironment=fe, track="all")

Below we simulate the aircraft responding to a (non-propagating) fire:

res, hist = propagate.nominal(a1, protect=False)
fig, ax = hist.plot_line('s.fuel_status', 's.x', 's.y', 'm.mode')
../../../_images/b6f85506b34da778602bc66af37b200b28b2357435481c857b20465413cf9044.png

As shown above, the aircraft alternates between flying, mitigating, and resupplying, and resupply can take some time.

fig, ax = a1.fireenvironment.c.show_from(10, hist.fireenvironment.c, properties=sim_properties)
hist2 = hist.cut(10, newcopy=True)
hist2.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)
../../../_images/62b00544b96ad3809e15b28e32f712be804150119eac1887616ea61de5f119be.png
fig, ax = a1.fireenvironment.c.show_from(20, hist.fireenvironment.c, properties=sim_properties)
hist2 = hist.cut(20, newcopy=True)
hist2.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)
../../../_images/39dc26011ff3a9f786ca2aad7e184f7388e80bdbff93f38a850aafdd568b2917.png
fig, ax = a1.fireenvironment.c.show_from(45, hist.fireenvironment.c, properties=sim_properties)
hist2 = hist.cut(45, newcopy=True)
hist2.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)
../../../_images/a6c210f7bece101afba83bd7657a75af69b752ffd17da408c0f8677645165807.png

Combined Simulation

mdl = WildfireSim()

This integrated model has both firepropagation as well as aircraft. We can change the number of aircraft by changing the number of bases.

mdl_graph = FunctionArchitectureGraph(mdl)
mdl_graph.draw()
(<Figure size 1200x1000 with 1 Axes>, <Axes: >)
../../../_images/1be380a575991a3f7db67dcc993b4b8443ea5c591832ad4a8fa488bc739e8ca6.png
mdl2 = WildfireSim(p = {'firemapparam': {'base_locations': ((30, 30), (40, 40))}})
mdl2_graph = FunctionArchitectureGraph(mdl2)
mdl2_pos = {'wildfiresim.flows.fireenvironment': [-0.07, 0.05], 'wildfiresim.fxns.aircraft_0': [0.58, -0.62], 'wildfiresim.fxns.aircraft_1': [-0.65, -0.65], 'wildfiresim.fxns.firepropagation': [-0.02, 0.71]}
mdl2_graph.set_pos(**mdl2_pos)
fig, ax = mdl2_graph.draw(figsize=(4,4))
fig.savefig("response_structure.svg", bbox_inches='tight')
../../../_images/eec3b90b6091f93e34d34d77d50aee4fbe521ebbb5acd199b84f9b7ff11ea724.png
# %matplotlib qt5
# mdl2_graph.move_nodes()
# %matplotlib inline

Below we simulate the combined propagation and suppression of the fire

p = {'firemapparam': {**double_size_p, **{'num_strikes': 3, 'base_locations': ((0, 45),), 'map_type': 'forest-grass-scrub'}}}
mdl = WildfireSim(p = p)
res, hist = propagate.nominal(mdl, protect=False)
fig, ax = plot_combined_response_from(20, history=hist, mdl=mdl)
../../../_images/25fd366f27cf556cb09dfef31929787edc444cf1e8a4abcbc46a5db5c048f7e4.png
ani = hist.animate(plot_combined_response_from, mdl=mdl)
# HTML(ani.to_jshtml())
../../../_images/75c482309a6afd95568834de2c1eca6dd6c2b35ea47fa778eaf5c7625ec3be9a.png
ani.save("single_aircraft_response.gif")
MovieWriter ffmpeg unavailable; using Pillow instead.
ani = hist.animate("plot_line_from",
                   plot_values=('fxns.aircraft_0.m.mode',),
                   legend_loc=False, t_line=True)
# HTML(ani.to_jshtml())
../../../_images/796641adcb4e9f0166a136d5235c8706a0fc59234055be0d99aa60bb635f1292.png
ani.save("single_aircraft_modes.gif")
MovieWriter ffmpeg unavailable; using Pillow instead.
<Figure size 640x480 with 0 Axes>
fig, ax = mdl.flows['fireenvironment'].c.show_from(20, hist.flows.fireenvironment.c,
                                                   properties=sim_properties)
hist20 = hist.cut(20, newcopy=True)
hist20.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)

fig, ax = mdl.flows['fireenvironment'].c.show_from(80, hist.flows.fireenvironment.c,
                                                   properties=sim_properties)
hist20 = hist.cut(80, newcopy=True)
hist20.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)

fig, ax = mdl.flows['fireenvironment'].c.show_from(120, hist.flows.fireenvironment.c,
                                                   properties=sim_properties)
hist20 = hist.cut(120, newcopy=True)
hist20.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)
../../../_images/bffbfb3efde48c72fe51005e93668616e91ca106d5cec1eb9c0d60599d4398c5.png ../../../_images/f3e88b27e2016770bf2b410b61da4c4aa726084e10ed7990385ae0149831577a.png ../../../_images/5aad551df74475421be1bb77fef918c2ea74fffde3d1340124a9aca87ff98621.png

With two aircraft bases, the fire is put out more quickly:

p = {'firemapparam': {**double_size_p, **{'num_strikes': 3, 'base_locations': ((0, 45), (0, 0)), 'map_type': 'forest-grass-scrub'}}}
mdl = WildfireSim(p=p)
res, hist = propagate.nominal(mdl)
ani = hist.animate(plot_combined_response_from, mdl=mdl)
# HTML(ani.to_jshtml())
../../../_images/d9a7aa01b0241d0d54998d6180d457e706c5f65a9112a8e5f0bc65c8ffd4083f.png
ani.save("multi_drone_response.gif")
MovieWriter ffmpeg unavailable; using Pillow instead.
fig, ax = mdl.flows['fireenvironment'].c.show_from(20, hist.flows.fireenvironment.c,
                                                   properties=sim_properties)
hist20 = hist.cut(20, newcopy=True)
hist20.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)

fig, ax = mdl.flows['fireenvironment'].c.show_from(25, hist.flows.fireenvironment.c,
                                                   properties=sim_properties)
hist80 = hist.cut(80, newcopy=True)
hist80.plot_trajectory('s.x', 's.y', fig=fig, ax=ax)
../../../_images/99d9324a1dab7f25ab6524f7e4a8e2301d3ff185235d0782a63365a9ecbfac82.png ../../../_images/815df79fab7b46eda13a60d8f5f26e71cd3a2ed3b801ff683af6f5148c30fa54.png

Alternative Scenarios

p = {'firemapparam': {**double_size_p, **{'num_strikes': 3, 'base_locations': ((22.5, 22.5),), 'map_type': 'uniform-grass'}}}
mdl = WildfireSim(p = p)
res, hist = propagate.nominal(mdl)
ani = hist.animate(plot_combined_response_from, mdl=mdl)
# HTML(ani.to_jshtml())
../../../_images/b3ddfa1c6ef9de11f5f9a2aabfc37a8c32509e09f863e0f630182c0ce05cef87.png
ani.save("three_strike_grass.gif")
MovieWriter ffmpeg unavailable; using Pillow instead.
p = {'firemapparam': {**double_size_p, **{'num_strikes': 3, 'base_locations': ((22.5, 22.5),), 'map_type': 'uniform-forest'}}}
mdl = WildfireSim(p = p)
res, hist = propagate.nominal(mdl)
ani = hist.animate(plot_combined_response_from, mdl=mdl)
# HTML(ani.to_jshtml())
../../../_images/b8e892c73a5dbbefd0c93c2c9f2f3c8ac749bfb02f39c3d3dcc0ff06ef54b081.png
ani.save("three_strike_forest.gif")
MovieWriter ffmpeg unavailable; using Pillow instead.

Optimization

We can perform optimization by defining a problem to solve. Below the problem is to minimize burned area with a single base placement given random strikes.

psp = BasePlacementProblem(track='all', p={'firemapparam': {'num_strikes': 4}}, seed=1)
psp.perc_burned(10, 10)
np.float64(0.168)
psp
BasePlacementProblem with:
VARIABLES
 -x                                                         10.0000
 -y                                                         10.0000
OBJECTIVES
 -perc_burned                                                0.1680

The above number is the average percent burned over 10 random seeds. We can see the objective over all scenarios below:

psp.res
rep0_var_0.tend.classify.perc_burned: 0.09
rep0_var_0.tend.classify.burn_pts: array(10)
rep0_var_0.tend.fxns                0.09
rep1_var_1.tend.classify.perc_burned: 0.08
rep1_var_1.tend.classify.burn_pts: array(10)
rep1_var_1.tend.fxns                0.08
rep2_var_2.tend.classify.perc_burned: 0.2
rep2_var_2.tend.classify.burn_pts: array(10)
rep2_var_2.tend.fxns                 0.2
rep3_var_3.tend.classify.perc_burned: 0.14
rep3_var_3.tend.classify.burn_pts: array(10)
rep3_var_3.tend.fxns                0.14
rep4_var_4.tend.classify.perc_burned: 0.19
rep4_var_4.tend.classify.burn_pts: array(10)
rep4_var_4.tend.fxns                0.19
rep5_var_5.tend.classify.perc_burned: 0.24
rep5_var_5.tend.classify.burn_pts: array(10)
rep5_var_5.tend.fxns                0.24
rep6_var_6.tend.classify.perc_burned: 0.3
rep6_var_6.tend.classify.burn_pts: array(10)
rep6_var_6.tend.fxns                 0.3
rep7_var_7.tend.classify.perc_burned: 0.22
rep7_var_7.tend.classify.burn_pts: array(10)
rep7_var_7.tend.fxns                0.22
rep8_var_8.tend.classify.perc_burned: 0.11
rep8_var_8.tend.classify.burn_pts: array(10)
rep8_var_8.tend.fxns                0.11
rep9_var_9.tend.classify.perc_burned: 0.11
rep9_var_9.tend.classify.burn_pts: array(10)
rep9_var_9.tend.fxns                0.11

as well as evaluate the function over different base placements:

psp.perc_burned(40, 10)
np.float64(0.22500000000000003)
psp.perc_burned(40, 20)
np.float64(0.15700000000000003)
psp.perc_burned(20, 20)
np.float64(0.10400000000000001)
psp.res
rep0_var_0.tend.classify.perc_burned: 0.09
rep0_var_0.tend.classify.burn_pts: array(10)
rep0_var_0.tend.fxns                0.09
rep1_var_1.tend.classify.perc_burned: 0.06
rep1_var_1.tend.classify.burn_pts: array(10)
rep1_var_1.tend.fxns                0.06
rep2_var_2.tend.classify.perc_burned: 0.09
rep2_var_2.tend.classify.burn_pts: array(10)
rep2_var_2.tend.fxns                0.09
rep3_var_3.tend.classify.perc_burned: 0.13
rep3_var_3.tend.classify.burn_pts: array(10)
rep3_var_3.tend.fxns                0.13
rep4_var_4.tend.classify.perc_burned: 0.08
rep4_var_4.tend.classify.burn_pts: array(10)
rep4_var_4.tend.fxns                0.08
rep5_var_5.tend.classify.perc_burned: 0.23
rep5_var_5.tend.classify.burn_pts: array(10)
rep5_var_5.tend.fxns                0.23
rep6_var_6.tend.classify.perc_burned: 0.12
rep6_var_6.tend.classify.burn_pts: array(10)
rep6_var_6.tend.fxns                0.12
rep7_var_7.tend.classify.perc_burned: 0.07
rep7_var_7.tend.classify.burn_pts: array(10)
rep7_var_7.tend.fxns                0.07
rep8_var_8.tend.classify.perc_burned: 0.08
rep8_var_8.tend.classify.burn_pts: array(10)
rep8_var_8.tend.fxns                0.08
rep9_var_9.tend.classify.perc_burned: 0.09
rep9_var_9.tend.classify.burn_pts: array(10)
rep9_var_9.tend.fxns                0.09
psp.parameterdomain(10, 10)
WildFireSimParameter(firemapparam=FireMapParam(x_size=10, y_size=10, blocksize=5.0, gapwidth=0.0))
psp.sim_mdl
<bound method ParameterSimProblem.sim_mdl of BasePlacementProblem with:
VARIABLES
 -x                                                         20.0000
 -y                                                         20.0000
OBJECTIVES
 -perc_burned                                                0.1040>
show_properties = {k:v for k,v in sim_properties.items() if k!="base"}
psp.mdl.flows['fireenvironment'].c.show_from(10, psp.hist.rep0_var_0.flows.fireenvironment.c, show_properties)
(<Figure size 500x500 with 1 Axes>,
 <Axes: title={'center': ' t=10'}, xlabel='x', ylabel='y'>)
../../../_images/ade9f930b326320e437ecdd4dda6ba8a503f8640ef3057a3470da3b97569e73e.png
psp.hist.rep0_var_0.flows.fireenvironment.c.burning[0].shape
(10, 10)
psp.mdl.flows['fireenvironment'].c.show_from(35, psp.hist.rep0_var_0.flows.fireenvironment.c, show_properties)
(<Figure size 500x500 with 1 Axes>,
 <Axes: title={'center': ' t=35'}, xlabel='x', ylabel='y'>)
../../../_images/35edc20b3de14c74ce58901dd146f051943727f34dde61116d29e176c50cacb4.png