#!python2
################################################################################
#---------#
# wfds.py #
#---------#
"""
The wfds module is for configuring and running the Wildland-urban interface Fire
Dynamics Simulator (WFDS). The ``WFDS``class is used to setup the run. This
class inherits the ``Mesh`` class, so meshes can be dealt with there. The main
purpose of the WFDS class is to create a WFDS input file. The ``Execute`` class
runs a WFDS simulation. The ``GenerateBinaryGrid`` class is used to generate
grids for the capsis module.
See the FDS Users Guide: https://pages.nist.gov/fds-smv/manuals.html and the
WFDS Users Guide: https://www.fs.fed.us/pnw/fera/wfds/wfds_user_guide.pdf
for further information.
"""
# meta
__authors__ = "Team STANDFIRE"
__copyright__ = "Copyright 2017, STANDFIRE"
__credits__ = ["Greg Cohn", "Brett Davis", "Matt Jolly", "Russ Parsons", "Lucas Wells"]
__license__ = "GPL"
__maintainer__ = "Lucas Wells"
__email__ = "bluegrassforestry@gmail.com"
__status__ = "Development"
__version__ = "1.1.4a" # Previous version: "1.1.3a"
# module imports
import platform
import subprocess
import os
[docs]class Mesh(object):
"""
The Mesh class can be used to easily and quickly calculate the input
parameters needed to define FDS meshes. It is inherited by the ``WFDS`` and
``GenerateBinaryGrid`` classes. It has two callable (non-private) methods:
``stretch_mesh()`` and ``format_mesh()``
:param x_dom: X dimension of the simulation domain
:type x_dom: integer
:param y_dom: Y dimension of the simulation domain
:type y_dom: integer
:param z_dom: Z dimension of the simulation domain
:type z_dom: integer
:param res: 3-space resolution prior to mesh stretching
:type res: float
:param num_meshes: Number of meshes
:type num_meshes: integer
:Example:
>>> import wfds
>>> mesh = wfds.Mesh(160,90,50,1,9)
>>> mesh.stretch_mesh([3,33], [1,31])
>>> print mesh.format_mesh()
"""
def __init__(self, x_dom, y_dom, z_dom, res, num_meshes):
"""
Constructor
"""
# instance variables
self.res = res # unused as of v 1.1.4a
self.num_cells = []
self.mesh_domain = []
self.num_meshes = num_meshes
self.stretch = None
# calculate cell size and mesh domain
self._calc_num_cells(x_dom, y_dom, z_dom)
self._calc_mesh_domain(x_dom, y_dom, z_dom)
def _calc_num_cells(self, x_dom, y_dom, z_dom):
"""
Pseudo-private method. Called when a Mesh object is instantiated.
Calculates the number of cells in the x, y and z directions for each
mesh using x, y and z domains and the number of meshes. When multiple
meshes are requested the domain is divided along the y axis. For example,
in a mesh domain where x=160, y=90 and z=50, and only one mesh is
requested the the number of cells would be 160, 90 and 50. If two meshes
are requested, the number of cells for each would be 160, 45 and 50.
These numbers act as divisors for the mesh domain dimensions to define
cell sizes. These three numbers populate the IJK parameter in the WFDS
input file.
"""
remainder = y_dom % self.num_meshes
interval = (y_dom-remainder)/self.num_meshes
for i in range(self.num_meshes):
if i == self.num_meshes - 1:
self.num_cells.append([x_dom, interval+remainder, z_dom])
else:
self.num_cells.append([x_dom, interval, z_dom])
def _calc_mesh_domain(self, x_dom, y_dom, z_dom):
"""
Pseudo-private method. Called when a Mesh object is instantiated.
Calculates mesh domains based on the x,y,z domain and the number of
meshes.
A mesh domain is defined by a set of six coordinates. The first,
third and fifth coordinates define the origin point of the mesh and the
second, fourth and sixth coordinates define the opposite corner of the
mesh. For example, coordinates (0,160,0,90,0,50) define a mesh that
extends 160 units in the x direction, 90 units in the y direction and 50
units in the z direction. These six numbers populate the XB parameter in
the WFDS input file.
"""
remainder = y_dom % self.num_meshes
interval = (y_dom-remainder)/self.num_meshes
acumm = 0
for i in range(self.num_meshes):
if i == self.num_meshes - 1:
self.mesh_domain.append([0, x_dom, acumm, acumm+interval+remainder, 0, z_dom])
else:
self.mesh_domain.append([0, x_dom, acumm, acumm+interval, 0, z_dom])
acumm += interval
[docs] def stretch_mesh(self, comp_coord, phys_coord):
"""
Defines the parameters for stretching a the mesh along the z axis,
distorting the cell sizes. Passes the two sets of coordinates to the
self.stretch variable which will be accessed by the
``format_mesh()`` method to format the WFDS input file.
:param comp_coord: computation coordinates
:type comp_coord: python list
:param phys_coord: physical coordinates
:type phys_coord: python list
.. note:: comp_coord and phys_coord must have an equal number of
elements.See the FDS users guide for more information on
stretching:
https://pages.nist.gov/fds-smv/manuals.html
:Example:
>>> mesh = wfds.Mesh(200, 150, 100, 1, 1)
>>> mesh.stretch_mesh([3,33], [1,31])
>>> print mesh.format_mesh()
"""
if len(comp_coord) != len(phys_coord):
print "Computational coordinates and physical coordinates are not \
of equal length"
return -1
self.stretch = [comp_coord, phys_coord]
[docs]class WFDS(Mesh):
"""
This class configures a WFDS simulation. It gathers and calculates WFDS
paramters and writes them to a WFDS input file.
``WFDS()`` is a subclass of the ``Mesh()`` class and meshes are dealt with
implicitly.
:param x: scene size in the x dimension
:type x: integer
:param y: scene size in the y dimension
:type y: integer
:param z: scene size in the z dimension
:type z: integer
:param xA: x dimension of the Area Of Interest
:type xA: integer
:param xO: x offset
:type xO: integer
:param res: simulation resolution
:type res: double
:param n: number of WFDS meshes
:type n: integer
:param fuels: fuels object (fuels.py, class: FVSfuels)
:type fuels: object
The following example assumes that a fuels object has been created using
the fuels module.
:Example:
>>> fds = wfds.WFDS(160, 90, 50, 64, 83, 1, 1, fuels)
>>> fds.create_mesh(stretch={"CC":[3, 33], "PC":[1, 31]})
>>> fds.create_ignition(30, 50, 24, 29, 13, 77)
>>> fds.set_hrrpua(1000)
>>> fds.set_wind_speed(9)
>>> fds.set_init_temp(30)
>>> fds.set_simulation_time(300)
>>> fds.set_run_name("standard_run")
>>> fds.save_input("C:/temp/standard_run.txt")
"""
def __init__(self, x, y, z, xA, xO, res, n, fuels):
"""
Constructor
:param x: scene size in the x dimension
:type x: integer
:param y: scene size in the y dimension
:type y: integer
:param z: scene size in the z dimension
:type z: integer
:param xA: x dimension of the Area Of Interest
:type xA: integer
:param xO: x offset
:type xO: integer
:param res: simulation resolution
:type res: double
:param n: number of WFDS meshes
:type n: integer
:param fuels: fuels object (fuels.py, class: FVSfuels)
:type fuels: object
"""
# call the super class constructor
super(WFDS, self).__init__(x, y, z, res, n)
# auto-calculate the position of the "area of interest"
aoi_x_center = (xO+(xA/2))
# WFDS run configuration parameters
self.params = {"run_name" : "Default",
"mesh" : None,
"time" : 0,
"init_temp" : 0,
"wind_speed" : 0,
"ign" : {"hrrpua" : 0,
"coords" : [0, 0, 0, 0],
"ramp" : [0, 0, 0, 0, 0]},
"bounds" : {"x" : x,
"y" : y,
"z" : z},
"fuels" : fuels,
"dump" : {"y_center" : y/2.0,
"aoa_x_center" : aoi_x_center,
"aoa_x_front" : aoi_x_center - 10,
"aoa_x_back" : aoi_x_center + 10}}
self.mesh = None # will be defined in create_mesh()
[docs] def create_mesh(self, stretch=False):
"""
Collates and adds mesh parameters to the params dictionary.
Conditionally calls the ``stretch_mesh()`` method of the Mesh class
depending on the state of the stretch argument. Calls the
``format_mesh()`` method of the Mesh class.
:param stretch: to stretch or not to stretch
:type stretch: False or dictionary of stretch parameters
Example dictionary of stretch parameters:
{"CC":[3, 33], "PC":[1, 31]}
.. note:: See ``Mesh.stretch_mesh()`` for setting the stretch arguments
"""
if stretch:
self.stretch_mesh(stretch["CC"], stretch["PC"])
self.mesh = self.format_mesh()
self.params["mesh"] = self.mesh
[docs] def create_ignition(self, start_time, end_time, x_strt, x_end, y_strt, y_end):
"""
Places an ignition strip at the specified location and generates fire
at the specified Heat Release Rate Per Unit Area (HRRPUA) for the
specified duration.
:param start_time: start time of the igniter fire
:type start_time: integer
:param end_time: finish time of the igniter fire
:type end_time: integer
:param x_strt: starting x position of the ignition strip
:type x_strt: float
:param x_end: ending x position of the ignition strip
:type x_end: float
:param y_strt: starting y position of the ignition strip
:type y_strt: float
:param y_end: ending y position of the ignition strip
:type y_end: float
.. note:: Ignition ramping is dealt with explicitly to avoid "explosions"
"""
self.params["ign"]["coords"][0] = x_strt
self.params["ign"]["coords"][1] = x_end
self.params["ign"]["coords"][2] = y_strt
self.params["ign"]["coords"][3] = y_end
delta = end_time - start_time
interval = delta/4.0
self.params["ign"]["ramp"][0] = start_time
time_step = start_time + interval
for i in range(1, 4):
self.params["ign"]["ramp"][i] = time_step
time_step += interval
self.params["ign"]["ramp"][4] = end_time
[docs] def set_wind_speed(self, inflow_spd):
"""
Set the inflow wind speed
:param inflow_spd: inflow wind speed (m/s)
:type inflow_spd: float
"""
self.params["wind_speed"] = inflow_spd
[docs] def set_init_temp(self, temp):
"""
Set the initial temperature of the simulation
:param temp: temperature (celcius)
:type temp: float
"""
self.params["init_temp"] = temp
[docs] def set_simulation_time(self, sim_time):
"""
Set the duration of the simulation
:param sim_time: duration of the simulation (s)
:type sim_time: float
"""
self.params["time"] = sim_time
[docs] def set_hrrpua(self, hrr):
"""
Set the heat release rate per unit area for the ignition strip
:param hrr: heat release rate per unit area (kW/m^2)
:type hrr: integer
"""
self.params["ign"]["hrrpua"] = hrr
[docs] def set_run_name(self, name):
"""
Adds the run name to `params` dictionary
:params name: run name
:type name: string
"""
self.params["run_name"] = name
[docs]class Execute(object):
"""
The Execute class runs the WFDS input file (platform agnostic)
:param input_file: path to and name of input file (e.g. grid.txt)
:type input_file: string
:param n_proc: number of processors
:type n_proc: integer
.. note:: multiple processors not currently supported. Planned for a future
version.
"""
def __init__(self, input_file, n_proc):
"""
Constructor
"""
# get path to STANDFIRE directory
self.fds_bin = os.path.dirname(os.path.abspath(__file__)) + "/bin/"
if platform.system().lower() == "linux":
self._exec_linux(input_file, n_proc)
elif platform.system().lower() == "windows":
self._exec_win(input_file, n_proc)
## elif platform.system().lower() == "darwin":
## self._exec_mac()
else:
print "OS of type {0} is not recognized by STANDFIRE".format(platform.system())
def _exec_linux(self, input_file, n_proc):
"""
Pseudo-private method
"""
if n_proc > 1:
print "Multiple processors not currently supported for standfire \
WFDS execution"
log = subprocess.check_output([self.fds_bin + "fds_linux/wfds", input_file],
cwd="/".join(input_file.split("/")[:-1]))
print log
def _exec_win(self, input_file, n_proc):
"""
Pseudo-private method
"""
if n_proc > 1:
print "Multiple processors not currently supported for standfire \
WFDS execution"
log = subprocess.check_output([self.fds_bin + "fds_win/wfds.exe", input_file],
cwd="/".join(input_file.split("/")[:-1]))
print log
[docs]class GenerateBinaryGrid(Mesh):
"""
This subclass of ``Mesh`` is used to generate a binary FDS grid for Capsis.
Inherits the mesh class.
.. note:: See the ``wfds.Mesh()`` class for details
"""
def __init__(self, x, y, z, res, n, f_name, stretch=False):
"""
Constructor
"""
# call super constructor (GBG inherits Mesh)
super(GenerateBinaryGrid, self).__init__(x, y, z, res, n)
if stretch:
self.stretch_mesh(stretch["CC"], stretch["PC"])
self.f_name = f_name
self.mesh = self.format_mesh()
self._write()
self._generate()
def _write(self):
"""
Pseudo-private method
Create WFDS input file (e.g. grid.txt)
"""
grid_name = self.f_name.split("/")[-1].split(".")[0]
grid_input = "&HEAD CHID='{0}', TITLE='{0}' /\n\n\n".format(grid_name)
grid_input += self.mesh
grid_input += "\n&TIME T_END=0.1 /\n"
grid_input += "\n&DUMP WRITE_XYZ=.TRUE. /\n"
grid_input += "\n&TAIL /"
with open(self.f_name, "w") as cap_grid_file:
cap_grid_file.write(grid_input)
def _generate(self):
"""
Pseudo-private method
f_name is the name of the WFDS input file (e.g. grid.txt)
"""
Execute(self.f_name, 1)