Source code for sleuth_automation

# coding: utf-8
# Authors: Fidel Serrano, Rodrigo GarcĂ­a
"""
An object oriented wrapper for SLEUTH_, an urban growth model.

.. _SLEUTH: http://www.ncgia.ucsb.edu/projects/gig/

This module defines the `Location` class, which is a work unit for SLEUTH.

How to use this module
======================

1. Import it::

    import sleuth_automation as sa

2. Configure it at least with a path to SLEUTH binaries::

    sa.configure(sleuth_path='/path/to/sleuth',
                 use_mpi=True, mpi_cores=32)

2. Create an instance::

    l = sa.Location('my_location',
                    '/path/to/my_location')

3. Calibrate model::

    l.calibrate_coarse()
    l.calibrate_fine()
    l.calibrate_final()

or calibrate with a single call::

    l.sleuth_calibrate()

4. Predict growth::

    l.sleuth_predict(2017, 2060)

5. Format prediction as TIFF::

    l.gif2tif(2017, 2060)


"""

__docformat__ = 'restructuredtext'

import os
from os.path import join, abspath
from jinja2 import Environment, PackageLoader
from controlstats import ControlStats
import json
import pickle
import datetime

try:
    from sh import bash, gdal_translate, otbcli_BandMath, which
except:
    pass

try:
    from sh import mpirun
except:
    pass

config = {}


def create_dir(path):
    """
    If directory doesn't exist: create it.
    """
    if not os.path.exists(path):
        os.mkdir(path, 0755)


def configure(sleuth_path, use_mpi=False, mpi_cores=40):
    """
    Merge extra parameters into global config.

    Parameters:

    - `sleuth_path`: path to SLEUTH directory.
    - `use_mpi`: wether to call SLEUTH binaries thru the MPI.
    - `mpi_cores`: if so, how many cores to use.
    """

    config['sleuth_path'] = sleuth_path
    config['use_mpi'] = use_mpi
    config['mpi_cores'] = mpi_cores
    config['whirlgif_binary'] = join(join(sleuth_path,
                                          "Whirlgif"),
                                     'whirlgif')
    config['grow_binary'] = join(sleuth_path,
                                 "grow")


[docs]class Location: """ This class represents the main work unit of the SLEUTH model. Within a location urban growth is simulated. A location should have a name which is just a string and is used for naming output files, and a directory containing GIF files as specified by the SLEUTH documentation_. .. _documentation: http://www.ncgia.ucsb.edu/projects/gig/About/dtInput.htm """ def save_status(self): with open(join(self.input_path, "%s.pickle" % self.location), 'w') as p: pickle.dump(self.status, p) def __init__(self, location, input_path): """ Initialize a `Location` object. Parameters: - `location`: a name for the location according to sleuth docs - `input_path`: a path to the location directory """ assert len(config.keys()) > 0 self.location = location self.input_path = input_path self.output_path = join(input_path, 'out') create_dir(self.output_path) self.urban_layers = [] self.roads_layers = [] for thisFile in os.listdir(input_path): if thisFile.endswith('.gif') and not thisFile.startswith("."): if ".urban." in thisFile: self.urban_layers.append(thisFile) if ".roads." in thisFile: self.roads_layers.append(thisFile) if ".slope." in thisFile: self.slope_layer = thisFile if ".hillshade." in thisFile: self.hillshade_layer = thisFile if ".excluded." in thisFile: self.exclusion_layer = thisFile self.urban_layers.sort() self.roads_layers.sort() # predict start and end must be in scenario files, tho ignored # in calibration stages self.predict_start = self.urban_layers[-1].split('.')[-2] self.predict_end = int(self.predict_start)+1 self.env = Environment(loader=PackageLoader('sleuth_automation', 'templates')) s = self.__dict__.copy() del(s['env']) s.update(config) self.status = {'init': {'stamp': datetime.datetime.now(), 'data': s}, 'calibration': {'coarse':{}, 'fine': {}, 'final': {}}, 'prediction': {}} self.save_status()
[docs] def create_scenario_file(self, params, monte_carlo_iterations): """ Merges extra parameters to global config and renders them into a SLEUTH scenario file. Parameters: - `params`: extra parameters to merge. - `monte_carlo_iterations`: more iterations take a longer time to run but add precision. """ template = self.env.get_template('scenario.jinja') arguments = {'whirlgif_binary': config['whirlgif_binary'], 'input_dir': self.input_path, 'monte_carlo_iterations': monte_carlo_iterations, 'urban': self.urban_layers, 'roads': self.roads_layers, 'exclude': self.exclusion_layer, 'slope': self.slope_layer, 'hillshade': self.hillshade_layer, 'predict_start': self.predict_start, 'predict_end': self.predict_end} arguments.update(params) return template.render(arguments)
[docs] def calibrate_coarse(self, monte_carlo_iterations=50): """ Run SLEUTH coarse calibration. Parameters: - `monte_carlo_iterations`: iterations for the coarse calibration step. """ coarse_dir = join(self.output_path, 'coarse') create_dir(coarse_dir) coarse_params = {'diff': 50, 'diff_start': 0, 'diff_step': 25, 'diff_end': 100, 'brd': 50, 'brd_start': 0, 'brd_step': 25, 'brd_end': 100, 'sprd': 50, 'sprd_start': 0, 'sprd_step': 25, 'sprd_end': 100, 'slp': 50, 'slp_start': 0, 'slp_step': 25, 'slp_end': 100, 'rg': 50, 'rg_start': 0, 'rg_step': 25, 'rg_end': 100, 'output_dir': coarse_dir + '/'} with open(join(self.output_path, 'scenario.%s.coarse' % self.location), 'w') as f: scenario_file_path = f.name f.write(self.create_scenario_file(coarse_params, monte_carlo_iterations)) self.status['calibration']['coarse']['start'] = datetime.datetime.now() self.status['calibration']['coarse']['params'] = coarse_params self.save_status() if config['use_mpi']: mpirun('-np', config['mpi_cores'], config['grow_binary'], 'calibrate', scenario_file_path, _out=join(coarse_dir, 'mpi_out.log'), _err=join(coarse_dir, 'mpi_err.log')) else: bash('-c', "%s calibrate %s" % (config['grow_binary'], scenario_file_path)) self.status['calibration']['coarse']['end'] = datetime.datetime.now() self.save_status()
[docs] def calibrate_fine(self, monte_carlo_iterations=50): """ Run SLEUTH fine calibration. Parameters: - `monte_carlo_iterations`: iterations for the fine calibration step. """ fine_dir = join(self.output_path, 'fine') create_dir(fine_dir) default_step = 5 cs = ControlStats(join(join(self.output_path, 'coarse'), 'control_stats.log'), default_step) cs.params['output_dir'] = fine_dir + '/' cs.params['monte_carlo_iterations'] = monte_carlo_iterations with open(join(self.output_path, 'scenario.%s.fine' % self.location), 'w') as f: scenario_file_path = f.name f.write(self.create_scenario_file(cs.params, monte_carlo_iterations)) self.status['calibration']['fine']['start'] = datetime.datetime.now() self.status['calibration']['fine']['params'] = cs.params self.save_status() if config['use_mpi']: mpirun('-np', config['mpi_cores'], config['grow_binary'], 'calibrate', scenario_file_path, _out=join(fine_dir, 'mpi_out.log'), _err=join(fine_dir, 'mpi_err.log')) else: bash('-c', "%s calibrate %s" % (config['grow_binary'], scenario_file_path)) self.status['calibration']['fine']['end'] = datetime.datetime.now() self.save_status()
[docs] def calibrate_final(self, monte_carlo_iterations=50): """ Run SLEUTH final calibration. Parameters: - `monte_carlo_iterations`: iterations for the final calibration step. """ final_dir = join(self.output_path, 'final') create_dir(final_dir) default_step = 1 cs = ControlStats(join(join(self.output_path, 'fine'), 'control_stats.log'), default_step) cs.params['output_dir'] = final_dir + '/' cs.params['monte_carlo_iterations'] = monte_carlo_iterations with open(join(self.output_path, 'scenario.%s.final' % self.location), 'w') as f: scenario_file_path = f.name f.write(self.create_scenario_file(cs.params, monte_carlo_iterations)) self.status['calibration']['final']['start'] = datetime.datetime.now() self.status['calibration']['final']['params'] = cs.params self.save_status() if config['use_mpi']: mpirun('-np', config['mpi_cores'], config['grow_binary'], 'calibrate', scenario_file_path, _out=join(final_dir, 'mpi_out.log'), _err=join(final_dir, 'mpi_err.log')) else: bash('-c', "%s calibrate %s" % (config['grow_binary'], scenario_file_path)) self.status['calibration']['final']['end'] = datetime.datetime.now() self.save_status()
[docs] def sleuth_calibrate(self): """ Run coarse, fine and final calibration steps in one call. """ self.calibrate_coarse() self.calibrate_fine() self.calibrate_final()
[docs] def sleuth_predict(self, end, diff=None, brd=None, sprd=None, slp=None, rg=None, monte_carlo_iterations=50): """ Run SLEUTH prediction step for range enclosed in start, end. Parameters: - `end`: end of temporal range for prediction, beggining is infered from last input - `diff`: TODO: see model documentation - `brd`: TODO: see model documentation - `sprd`: spread - `slp`: slope - `rg`: TODO: see model documentation - `monte_carlo_iterations`: iterations for the prediction step """ self.predict_end = end predict_dir = join(self.output_path, 'predict') create_dir(predict_dir) default_step = 0 # ignored for predict cs = ControlStats(join(join(self.output_path, 'final'), 'control_stats.log'), default_step) cs.params['output_dir'] = predict_dir + '/' if diff: cs.params['diff'] = diff if brd: cs.params['brd'] = brd if sprd: cs.params['sprd'] = sprd if slp: cs.params['slp'] = slp if rg: cs.params['rg'] = rg cs.params['monte_carlo_iterations'] = monte_carlo_iterations with open(join(self.output_path, 'scenario.%s.predict' % self.location), 'w') as f: scenario_file_path = f.name f.write(self.create_scenario_file(cs.params, monte_carlo_iterations)) self.status['prediction']['start'] = datetime.datetime.now() self.status['prediction']['params'] = cs.params self.save_status() if config['use_mpi']: mpirun('-np', 1, config['grow_binary'], 'predict', scenario_file_path, _out=join(predict_dir, 'mpi_out.log'), _err=join(predict_dir, 'mpi_err.log')) else: bash('-c', "%s predict %s" % (config['grow_binary'], scenario_file_path)) self.status['prediction']['end'] = datetime.datetime.now() self.save_status()
[docs] def gif2tif(self, start, end): """ Convert `prediction output`_ from GIF to TIFF. Will use extent.json and gdal_translate to recover geospatial information lost to GIF format. Parameters: - `start`: beginning of prediction range. - `end`: ending of prediction range, inclusive. Start and end parameters are necessary to figure out paths. .. _`prediction output`: http://www.ncgia.ucsb.edu/projects/gig/About/dtImOut.htm """ predict_dir = join(self.output_path, 'predict') with open(join(self.input_path, 'extent.json')) as extent_file: extent = json.load(extent_file) columnas = str(int(extent["columns"]) - 1) renglones = str(int(extent["rows"]) - 1) epsg = 'EPSG:' + extent["epsg"] xmin = str(extent["xmin"]) xmax = str(extent["xmax"]) ymin = str(extent["ymin"]) ymax = str(extent["ymax"]) self.save_status('gif2tif') for year in range(start + 1, end + 1): gif = join(predict_dir, "%s_urban_%s.gif" % (self.location, year)) tmp_tif = join(predict_dir, "%s_urban_%s_tmp.tif" % (self.location, year)) tmp_xml = join(predict_dir, "%s_urban_%s_tmp.tif.aux.xml" % (self.location, year)) tif = join(predict_dir, "%s_urban_%s.tif" % (self.location, year)) gdal_translate('-a_srs', epsg, '-ot', 'Float64', '-of', 'GTiff', '-gcp', '0', renglones, xmin, ymin, '-gcp', columnas, renglones, xmax, ymin, '-gcp', columnas, '0', xmax, ymax, '-gcp', '0', '0', xmin, ymax, gif, tmp_tif) otbcli_BandMath('-il', tmp_tif, '-out', tif, '-exp', 'im1b1 < 9 ? im1b1 : 0') try: os.remove(tmp_tif) os.remove(tmp_xml) except: pass
class Region: """ A directory containing several Locations """ def __init__(self, region_dir, predict_end, monte_carlo_iterations): self.region_dir = region_dir self.predict_end = predict_end self.monte_carlo_iterations = monte_carlo_iterations self.env = Environment(loader=PackageLoader('sleuth_automation', 'templates')) self.locations = [] for f in os.listdir(region_dir): if os.path.isdir(join(region_dir, f)): self.locations.append({"name": f, "path": abspath(join(region_dir, f)) + '/'}) def build(self): template = self.env.get_template("condor_submit.jinja") with open(join(self.region_dir, 'submit.condor'), 'w') as f: f.write(template.render({'executable': which('sleuth_run.py'), 'list_of_regions': self.locations, 'predict_end': self.predict_end, 'sleuth_path': config['sleuth_path'], 'mpi_cores': config['mpi_cores'], 'montecarlo_iterations': self.monte_carlo_iterations})) return f.name