Source code for capsis

#!python2
################################################################################
#-----------#
# capsis.py #
#-----------#

"""
This module is a Python wrapper for the Capsis Standfire suite. Currently
Capsis' role in STANDFIRE is to distribute the fuels present in
the files generated by Fvsfuels. Capsis uses a pre-generated FDS grid file
(.xyz) to place canopy and surface fuels in a user defined domain. Capsis
provides many options for placing these fuels. The pertenant arguments can be
controlled through the Capsis ``RunConig`` class. The ``Execute`` class is used
to run Capsis. An up-to-date Java installation (1.8.0 for STANDFIRE version
1.1.4) is required since Capsis runs on a Java Virtual Machine.
"""

# 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 os
from shutil import copyfile, rmtree
import subprocess
import platform
import random

from wfds import GenerateBinaryGrid

[docs]class RunConfig(object): """ The ``RunConfig`` class is used to configure a capsis run. :param run_directory: desired path for Capsis run :type run_directory: string *Example:* >>> import capsis >>> config = capsis.RunConfig("/path/to/capsis_run/") >>> config.set_xy_size(160,90,64,64) >>> config.set_svs_base("stand_0001") >>> config.set_crown_space(1.5) >>> config.set_show_3d("true") >>> config.save_config() """ def __init__(self, run_directory): """ Constructor """ # default parameters self.params = {"path" : run_directory, "speciesFile" : "/speciesFile.txt", "svsBaseFile" : "", "additionalProperties" : "/additionalProperties.txt", "sceneOriginX" : 0.0, "sceneOriginY" : 0.0, "sceneSizeX" : 0, "sceneSizeY" : 0, "sceneSizeZ" : 100, "show3d" : "false", "extend" : "false", "xOffset" : 0.0, "yOffset" : 0.0, "spatialOpt" : 0, "respace" : "false", "respaceDistance" : 0.0, "prune" : "false", "pruneHeight" : 0.0, "format" : 86, "litter" : "true", "leaveLive" : "true", "leaveDead" : "true", "twig1Live" : "false", "twig1Dead" : "false", "twig2Live" : "false", "twig2Dead" : "false", "twig3Live" : "false", "twig3Dead" : "false", "canopyGeom" : "CYLINDER", "layerGeom" : "HET_RECTANGLE_TEXT", "bdBin" : 0.01, "firstGridFile" : "grid.xyz", "gridNumber" : 1, "gridResolution" : 1.0, "vegetation_cdrag" : 0.5, "vegetation_char_fraction" : 0.2, "emissivity" : 0.99, "degrad" : "false", "mlr" : 0.35, "init_temp" : 30.0, "veg_char_fraction" : 0.25, "veg_drag_coefficient" : 0.125, "burnRateMax" : 0.4, "dehydration" : 0.4, "rmChar" : "true", "outDir" : "/output/", "fileName" : "capsis_fuels.txt", # capsis out file "srf_blocks": {1: [[0, 0], [0, 0], [0, 0], [0, 0]], 2: [[0, 0], [0, 0], [0, 0], [0, 0]], 3: [[0, 0], [0, 0], [0, 0], [0, 0]], 4: [[0, 0], [0, 0], [0, 0], [0, 0]], 5: [[0, 0], [0, 0], [0, 0], [0, 0]], 6: [[0, 0], [0, 0], [0, 0], [0, 0]]}, "srf_fuels" : {"shrub" : {"ht" : 0.35, "cbh" : 0.0, "cover" : 0.5, "width" : 5.0, "spat_group": 1, "live" : {"load" : 0.72, "mvr" : 500, "svr" : 5000, "moisture": 100}, "dead" : {"load" : 0.08, "mvr" : 500, "svr" : 5000, "moisture": 40}}, "herb" : {"ht" : 0.35, "cbh" : 0.0, "cover" : 0.8, "width" : 1.0, "spat_group": 1, "live" : {"load" : 0.0, "mvr" : 500, "svr" : 5000, "moisture": 100}, "dead" : {"load" : 0.8, "mvr" : 500, "svr" : 5000, "moisture": 5}}, "litter" : {"ht" : 0.1, "cbh" : 0.0, "cover" :1.0, "width" : -1, "spat_group": 0, "load" : 0.5, "mvr" : 500, "svr" : 2000, "moisture" : 10}}} # verify path self.set_path(run_directory) ## #B these two methods are not currently used (offsets calculated and set in ## _set_x_offset and _set_y_offset. Could be used in the future to allow ## cmd line users to alter offsets... ## ## def set_x_offset(self, offset): ## """ ## Set the x offset of the area of analysis ## ## :param offset: x offset of the AOA ## :offset type: integer ## """ ## self.params["xOffset"] = offset ## ## def set_y_offset(self, offset): ## """ ## Set the y offset of the area of analysis ## ## :param offset: y offset of the AOA ## :offset type: integer ## """ ## self.params["yOffset"] = offset
[docs] def set_show_3d(self, value): """ Set the boolean value of the show3D parameter in the Capsis run file. If true, Capsis will open a 3D display showing the simulation domain. :param value: Truth value of the show3D parameter :type value: boolean """ self.params["show3d"] = value
[docs] def set_crown_space(self, space): """ Set the distance between crown for Capsis intervention :param space: crown spacing in meters :type space: float """ if space != 0: self.params["respace"] = "true" self.params["respaceDistance"] = space
[docs] def set_prune_height(self, prune): """ Set the prunning height (vertical spacing between ground and crown) for a Capsis intervention :param prune: prunning height :type prune: float """ if prune != 0: self.params["prune"] = "true" self.params["pruneHeight"] = prune
[docs] def set_srf_height(self, shrub_ht, herb_ht, litter_ht): """ Set surface fuel heights for shrubs, herbs and litter :param shrub_ht: shrub height :type shrub_ht: float :param herb_ht: herb height :type herb_ht: float :param litter_ht: litter height :type litter_ht: float """ self.params["srf_fuels"]["shrub"]["ht"] = shrub_ht self.params["srf_fuels"]["herb"]["ht"] = herb_ht self.params["srf_fuels"]["litter"]["ht"] = litter_ht
## def set_srf_cbh(self, shrub_cbh, herb_cbh): ## """ ## Currently unused method. Need to determine whether and how these ## parameters are used by capsis. ## ## :param shrub_cbh: ## :shrub_cbh type: float ## :param herb_cbh: ## :herb_cbh type: float ## """ ## self.params["srf_fuels"]["shrub"]["cbh"] = shrub_cbh ## self.params["srf_fuels"]["herb"]["cbh"] = herb_cbh
[docs] def set_srf_cover(self, shrub_cover, herb_cover, litter_cover): """ Set percent cover for shrubs, herbs and litter :param shrub_cover: shrub percent cover :type shrub_cover: integer :param herb_cover: herb percent cover :type herb_cover: integer :param litter_cover: litter percent cover :type litter_cover: integer """ self.params["srf_fuels"]["shrub"]["cover"] = shrub_cover self.params["srf_fuels"]["herb"]["cover"] = herb_cover self.params["srf_fuels"]["litter"]["cover"] = litter_cover
[docs] def set_srf_patch(self, shrub_patch, herb_patch, litter_patch): #B """ Set patch sizes (widths) for shrubs, herbs and litter :param shrub_patch: shrub patch size (meters) :type shrub_patch: float :param herb_patch: herb patch size (meters) :type herb_patch: float :param litter_patch: litter patch size (meters) :type litter_patch: float """ self.params["srf_fuels"]["shrub"]["width"] = shrub_patch self.params["srf_fuels"]["herb"]["width"] = herb_patch self.params["srf_fuels"]["litter"]["width"] = litter_patch
[docs] def set_srf_live_svr(self, shrub_svr, herb_svr): """ Set surface area to volume ratio for live shrubs and herbs :param shrub_svr: shrub surface area to volume ratio :type shrub_svr: integer :param herb_svr: herbaceaous surface area to volume ratio :type herb_svr: integer """ self.params["srf_fuels"]["shrub"]["live"]["svr"] = shrub_svr self.params["srf_fuels"]["herb"]["live"]["svr"] = herb_svr
[docs] def set_srf_dead_svr(self, shrub_svr, herb_svr, litter_svr): """ Set surface area to volume ratio for dead shrubs, herbs and litter :param shrub_svr: shrub surface area to volume ratio :type shrub_svr: integer :param herb_svr: herbaceaous surface area to volume ratio :type herb_svr: integer :param litter_svr: litter surface area to volume ratio :type litter_svr: integer .. note:: Live and dead surface area to volume ratios and not yet differentiated in the user interface. Both live and dead surface area to volume ratios are currently set to the same value. """ self.params["srf_fuels"]["shrub"]["dead"]["svr"] = shrub_svr self.params["srf_fuels"]["herb"]["dead"]["svr"] = herb_svr self.params["srf_fuels"]["litter"]["svr"] = litter_svr
[docs] def set_srf_live_load(self, shrub_load, herb_load): """ Set surface loads (kg/m2) for live shrubs and herbs :param shrub_load: live shrub surface load (kg/m2) :type shrub_load: float :param herb_load: live herbaceaous surface load (kg/m2) :type herb_load: float """ self.params["srf_fuels"]["shrub"]["live"]["load"] = shrub_load self.params["srf_fuels"]["herb"]["live"]["load"] = herb_load
[docs] def set_srf_dead_load(self, shrub_load, herb_load, litter_load): """ Set surface loads (kg/m2) for dead shrubs, herbs and litter :param shrub_load: dead shrub surface load (kg/m2) :type shrub_load: float :param herb_load: dead herbaceaous surface load (kg/m2) :type herb_load: float :param litter_load: litter surface load (kg/m2) :type litter_load: float """ self.params["srf_fuels"]["shrub"]["dead"]["load"] = shrub_load self.params["srf_fuels"]["herb"]["dead"]["load"] = herb_load self.params["srf_fuels"]["litter"]["load"] = litter_load
[docs] def set_srf_live_mc(self, shrub_mc, herb_mc): """ Set percent moisture content for live shrubs and herbs :param shrub_mc: live shrub moisture content (%) :type shrub_mc: integer :param herb_mc: live herbaceaous moisture content (%) :type herb_mc: integer """ self.params["srf_fuels"]["shrub"]["live"]["moisture"] = shrub_mc self.params["srf_fuels"]["herb"]["live"]["moisture"] = herb_mc
[docs] def set_srf_dead_mc(self, shrub_mc, herb_mc, litter_mc): """ Set percent moisture content for dead shrubs, herbs and litter :param shrub_mc: dead shrub moisture content (%) :type shrub_mc: integer :param herb_mc: dead herbaceaous moisture content (%) :type herb_mc: integer :param litter_mc: dead litter moisture content (%) :type litter_mc: integer """ self.params["srf_fuels"]["shrub"]["dead"]["moisture"] = shrub_mc self.params["srf_fuels"]["herb"]["dead"]["moisture"] = herb_mc self.params["srf_fuels"]["litter"]["moisture"] = litter_mc
[docs] def set_path(self, path): """ Sets path to Capsis run directory. Initially used to verify that "path" is a legitimate directory. Allows the user to modify the path after initialization. :param path: path to Capsis run directory :type path: string """ if os.path.isdir(path): self.params["path"] = path else: print "ERROR: " + path + " is not a directory"
[docs] def set_svs_base(self, base_name): """ Sets the base file name for FVS/SVS fuel output files. Unless reset using this function the base name generally is the stand ID found in the FVS keyword file. :param base_name: base file name for fuel output files :type base_name: string .. note:: Only the tree .csv file is required. If snags, coarse woody debris (cwd) and scalar files exist in the same directory they will be used by Capsis when writing WFDS fuel inputs. """ if os.path.isfile(os.path.join(self.params["path"], base_name + "_trees.csv")): self.params["svsBaseFile"] = base_name else: print "ERROR: " + base_name + "does not exist in " + self.params["path"]
[docs] def set_extend_fvs_sample(self, extend): """ Resets CAPSIS extend to true. This will cause CAPSIS to add additional trees to its landscape based on patterns found in the FVS input tree list. This should only be implemented when the sample doesn't cover the whole scene (e.g. not for lidar data). :param extend: Truth value of the extend parameter :type extend: boolean """ if extend: self.params["extend"] = "true" else: self.params["extend"] = "false"
[docs] def set_xy_size(self, x_size, y_size, x_aoi_size, y_aoi_size): """ Sets scene and Area Of Interest (AOI) dimensions. Calls methods to update offset and surface fuel block values :param x_size: size of scene in the x domain (meters) :type x_size: integer :param y_size: size of scene in the y domain (meters) :type y_size: integer :param x_aoi_size: area of interest size in the x domain :type x_aoi_size: integer :param y_aoi_size: area of interest size in the y domain :type y_aoi_size: integer .. note:: "x_size" and "y_size" must be greater than or equal to 64 meters .. note:: "x_aoi_size" and "y_aoi_size" must be at least 2 meters less than x and y sizes """ # set x and y sizes if x_size < 64: print "Scene X dimension must be greater than or equal to 64 meters" return -1 else: self.params["sceneSizeX"] = x_size if y_size < 64: print "Scene Y dimension must be greater than or equal to 64 meters" return -1 else: self.params["sceneSizeY"] = y_size # insure aoi sizes are at least 2 meters smaller than x and y sizes if x_size - x_aoi_size < 2: x_aoi_size = x_size - 2 if y_size - y_aoi_size < 2: y_aoi_size = y_size - 2 # update offset and surface fuel block verticies self._set_x_offset(x_aoi_size, y_aoi_size) self._set_y_offset(y_aoi_size) self._set_block_verts(x_aoi_size, y_aoi_size)
[docs] def set_z_size(self, z_size): """ Sets scene z dimension :param z_size: size of scene in the z domain (meters) :type z_size: integer .. note:: "z_size" must be greater than or equal to tallest tree in the domain """ self.params["sceneSizeZ"] = z_size
def _set_x_offset(self, x_aoi_size, y_aoi_size): """ Pseudo-private method "x_offset" defines the distance between the left edge of the x domain (x=0) and the left edge of the Area Of Interest (x=x_offset). It helps define the buffer around the AOI in the x dimension. See ``_set_block_verts()`` comments for more information. :param x_aoi_size: "area of interest" size in the x domain :type x_aoi_size: integer :param y_aoi_size: "area of interest" size in the y domain :type y_aoi_size: integer """ x_size = self.params["sceneSizeX"] y_size = self.params["sceneSizeY"] self.params["xOffset"] = x_size - (x_aoi_size + int((y_size - y_aoi_size)/2.0)) def _set_y_offset(self, y_aoi_size): """ Pseudo-private method "y_offset" defines the distance between the bottom edge of the y domain (y=0) and the bottom edge of the Area Of Interest (y=y_offset). It helps define the buffer around the AOI in the y dimension. See ``_set_block_verts()`` comments for more information. :param y_aoi_size: "area of interest" size in the y domain :type y_aoi_size: integer """ self.params["yOffset"] = int((self.params["sceneSizeY"] - y_aoi_size)/2.0) def _set_block_verts(self, x_aoi_size, y_aoi_size): """ Auto-calculates verticies for 5 surface fuel blocks (b1-5) for the Wildland-urban interface Fire Dynamics Simulator (WFDS) domain. Uses domain dimensions, Area of Interest dimensions and the x and y offsets between domain and AOI dimensions to calculate the verticies. The set of vertices for each block consists of four sets of coordinates: [[x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max]]. As currently implemented, the y dimension of b3 and b5 and the x dimension of b4 will be equal and the x dimension of b2 will be equal to: domain (total) x size - (b1x + b4x) :param x_aoi_size: "area of interest" size in the x domain :type x_aoi_size: integer :param y_aoi_size: "area of interest" size in the y domain :type y_aoi_size: integer Example 1: |-----------------------| | | b3 | | | |-----| | | b2 | b1 | b4 | | |(AOI)| | | |-----| | |<-x offset->| b5 | | |-----------------------| Example 2: |----------------------| | | b3 | | | |----------------| | | | | | |b2| b1 |b4| | | (AOI) | | | |----------------| | | | b5 | | |----------------------| """ x_size = self.params["sceneSizeX"] y_size = self.params["sceneSizeY"] xoff = self.params["xOffset"] yoff = self.params["yOffset"] x_aoi = x_aoi_size y_aoi = y_aoi_size block_1 = ([[xoff, yoff], [xoff+x_aoi, yoff], [xoff+x_aoi, yoff+y_aoi], [xoff, yoff+y_aoi]]) block_2 = [[0, 0], [xoff, 0], [xoff, y_size], [0, y_size]] block_3 = ([[xoff, yoff+y_aoi], [xoff+x_aoi, yoff+y_aoi], [xoff+x_aoi, y_size], [xoff, y_size]]) block_4 = [[xoff+x_aoi, 0], [x_size, 0], [x_size, y_size], [xoff+x_aoi, y_size]] block_5 = [[xoff, 0], [xoff+x_aoi, 0], [xoff+x_aoi, yoff], [xoff, yoff]] block_6 = [[0, 0], [x_size, 0], [x_size, y_size], [0, y_size]] self.params["srf_blocks"][1] = block_1 self.params["srf_blocks"][2] = block_2 self.params["srf_blocks"][3] = block_3 self.params["srf_blocks"][4] = block_4 self.params["srf_blocks"][5] = block_5 self.params["srf_blocks"][6] = block_6
[docs] def save_config(self): """ This method uses template and reference files to create capsis input files. Input files include input_template.txt, additionalProperties_template.txt and speciesFile.txt, all of which are located in the /data/capsis/ directory. It creates the following output files for the capsis run: capsis_run_file.txt, additionalProperties.txt and <run name>_scalars.csv. Run name is generally the standid found in the FVS keyword file. This method also calls the WFDS.py ``GenerateBinaryGrid`` method to generate a binary FDS grid for use by capsis and creates the sim_area.txt file for use by the standfire_analyze.py script. """ # get the current location of this module this_dir = os.path.dirname(os.path.abspath(__file__)) # read the capsis input template with open(this_dir + "/data/capsis/input_template.txt", "r") as it_file: input_params = it_file.read() # read the capsis additional properties template with open(this_dir + "/data/capsis/additionalProperties_template.txt", "r") as apt_file: properties = apt_file.read() # copy species file from standfire directory to capsis run directory copyfile(this_dir + "/data/capsis/speciesFile.txt", self.params["path"] + "/speciesFile.txt") # configure the input templates with user-defined parameters input_params = input_params.format(d=self.params) properties = properties.format(d=self.params) # and save the input files in the run directory specified on class instantiation with open(self.params["path"] + "/capsis_run_file.txt", "w") as run_file: run_file.write(input_params) with open(self.params["path"] + "/additionalProperties.txt", "w") as ap_file: ap_file.write(properties) with open(self.params["path"] + "/" + self.params["svsBaseFile"] + "_scalars.csv", "w") as sclr_file: sclr_file.write('"shrubwt", "herbwt", "litter", "duff"') # create an output director to store capsis fuels if os.path.isdir(self.params["path"] + "/output/"): rmtree(self.params["path"] + "/output") os.mkdir(self.params["path"] + "/output/") else: os.mkdir(self.params["path"] + "/output/") # write sim area to file for standfire analyze with open(self.params["path"] + "/output/sim_area.txt", "a") as area_file: area_file.write(str(self.params["sceneSizeX"]*self.params["sceneSizeY"])) # generate binary grid for fuel placement by capsis GenerateBinaryGrid(self.params["sceneSizeX"], self.params["sceneSizeY"], self.params["sceneSizeZ"], self.params["gridResolution"], self.params["gridNumber"], self.params["path"] + "/grid.txt")
[docs]class Execute(object): """ This class executes capsis according to the configuration from the ``RunConfig`` class. Capsis execution is platform agnostic. It also conditionally reduces the amount of fuels that will be tracked by WFDS and populates the fuels variable used to configure the WFDS run. :param path_to_run_file: Path and file name of capsis run file :type path_to_run_file: string :param subset_percent: Percentage of fuels to leave OUTPUT_TREE=.TRUE. :type subset_percent: float """ def __init__(self, path_to_run_file, subset_percent): """ Constructor """ self.capsis_dir = os.path.dirname(os.path.abspath(__file__)) + "/bin/capsis/" self.run_file = path_to_run_file self.subset_percent = subset_percent # expose this to user at some point # Run capsis if platform.system().lower() == "linux": self._exec_capsis_linux() if platform.system().lower() == "windows": self._exec_capsis_win() if subset_percent != 1.0: self.subset_fuels() self.read_fuels() def _exec_capsis_linux(self): """ Pseudo-private method """ subprocess.call(["sh", self.capsis_dir + "capsis.sh", "-p", "script", "standfire.myscripts.SFScript", self.run_file]) def _exec_capsis_win(self): """ Pseudo-private method """ os.chdir(self.capsis_dir) subprocess.call([self.capsis_dir + "capsis.bat", "-p", "script", "standfire.myscripts.SFScript", self.run_file])
[docs] def subset_fuels(self): """ This is a post-capsis run method that sets a user defined percentage of fuels (shrubs, herbs and trees) OUTPUT_TREE parameter to FALSE in the capsis_fuels.txt file. This effects what fuels WFDS tracks (not what it models - it still models all fuels) and should reduce the computing resources needed to run WFDS and SMOKEVIEW. It reads the output file from capsis (capsis_fuels.txt) and updates the OUTPUT_TREE parameter for a random selection of fuels. It also creates a copy of the original files renamed capsis_fuels_raw.txt for posterity. """ cap_fuels = "/".join(self.run_file.split("/")[:-1]) + "/output/capsis_fuels.txt" # read capsis_fuels.txt into the list variable "lines" capsis_fuels = open(cap_fuels, "r") lines = capsis_fuels.readlines() capsis_fuels.close() # create a list of indicies for those lines that contain "OUTPUT_TREE=" tree_line_index = [index for index, x in enumerate(lines) if "OUTPUT_TREE=" in x] # generate a random sample of indicies sample_size = int(round(len(tree_line_index) * (1 - self.subset_percent))) rand_sample = sorted(random.sample(tree_line_index, sample_size)) # set OUTPUT_TREE to FALSE for those lines indexed in the random sample line_num = 0 lines_new = [] for line in lines: if line_num not in rand_sample: lines_new.append(line) else: lines_new.append(line.replace("OUTPUT_TREE=.TRUE.", "OUTPUT_TREE=.FALSE.")) line_num += 1 # copy original version of capsis_fuels.txt to preserve it (for now) copyfile(cap_fuels, cap_fuels[:-4] + "_raw.txt") # overwrite capsis_fuels.txt with the new values out = open(cap_fuels, "w") out.writelines(lines_new) out.close()
[docs] def read_fuels(self): """ Reads capsis fuels file and populates the fuels variable which will be used to configure the WFDS run. """ capsis_out_file = "/".join(self.run_file.split("/")[:-1]) + "/output/capsis_fuels.txt" with open(capsis_out_file, "r") as out_file: lines = out_file.read() # creates and populates fuels variable. Becomes part of the object stored in # "capsis_execute" in the standfire_mini_interface script self.fuels = lines