streamlining.py


Perform slm analysis of topographic structure, including channels, channel heads, hillslope lengths (HSL), and relationships between HSL, aspect, etc.


Requires Python packages/modules:
Imports the following Streamlines modules:
Imports the folllowing classes:

Imports functions from useful.


class streamlines.streamlining.Streamlining(**kwargs)
Parameters:**kwargs (list) – Keyword arguments.

Class providing set of methods to compute streamline trajectories and densities from raw DTM data.

Inherits the Core class.

Provides top-level methods to:
  • TBD
  • prepare DTM grid for streamline computation by fixing blockages (single-diagonal-outflow pixels) and loops (divergence, curl and net vector magnitude exceeding trio of thresholds)
  • set ‘seed’ points aka start locations (sub-pixel positions) of all streamlines
  • generate streamlines from all seed points either upstream or downstream, returning seed point locations if generated in-situ, and returning arrays of streamline points and their mean spacing
  • generate all streamlines (up and downstream) and compute the overall mean streamline point spacing
Instantiates and assigns to self attributes each of the principal workflow classes:
__init__(**kwargs)
Parameters:**kwargs (list) – Keyword arguments.

Initialize the principal ‘streamlines’ class instance, whose object will contain references to the each of the key class instances of the streamlines workflow, e.g., geodata(), trace(), analysis()

parameters_file

Name of JSON parameters file (parsed from kwargs ‘parameters_file’)

Type:str
parameters_dir

Path to folder containing JSON parameters file (parsed from kwargs ‘parameters_file’)

Type:str

Code

"""
---------------------------------------------------------------------

Perform ``slm`` analysis of topographic structure, including channels, 
channel heads, hillslope lengths (HSL), and relationships between HSL, aspect,
etc.

---------------------------------------------------------------------

Requires Python packages/modules:
  - :mod:`json`
  - :mod:`datetime`
  - :mod:`dateutil.tz`

Imports the following ``Streamlines`` modules:
 - :mod:`.connect`
 - :mod:`.channelheads`
 - :mod:`.countlink`
 - :mod:`.label`
 - :mod:`.segment`
 - :mod:`.hillslopes`
 - :mod:`.lengths`

Imports the folllowing classes:
 - :class:`.Core`
 - :class:`.State`
 - :class:`.Geodata`
 - :class:`.Preprocess`
 - :class:`.Trace`
 - :class:`.Analysis`
 - :class:`.Mapping`
 - :class:`.Plot`
 - :class:`.Save`

Imports functions from :mod:`useful <streamlines.useful>`.

---------------------------------------------------------------------

.. _json: https://docs.python.org/3/library/json.html
.. _datetime: https://docs.python.org/3/library/datetime.html
.. _dateutil: https://pypi.org/project/python-dateutil/

"""

from json     import loads
from datetime import datetime
from dateutil import tz
import os
os.environ['PYTHONUNBUFFERED']='True'

from streamlines.core       import Core
from streamlines.parameters import read_json_file, import_parameters
from streamlines.state      import State
from streamlines.geodata    import Geodata
from streamlines.preprocess import Preprocess
from streamlines.trace      import Trace
from streamlines.analysis   import Analysis
from streamlines.mapping    import Mapping
from streamlines.plot       import Plot
from streamlines.save       import Save

__all__ = ['Streamlining']

pdebug = print

class Streamlining(Core):
    """
    Args:
        **kwargs (list): Keyword arguments.

    Class providing set of methods to compute streamline trajectories and 
    densities from raw DTM data.
    
    Inherits the :class:`Core <.Core>` class.

    
    Provides top-level methods to: 
        - TBD
        - prepare DTM grid for streamline computation
          by fixing blockages (single-diagonal-outflow pixels) and loops 
          (divergence, curl and net vector magnitude exceeding trio of thresholds)
        - set 'seed' points aka start locations (sub-pixel positions) of all streamlines 
        - generate streamlines
          from all seed points either upstream or downstream, returning seed point 
          locations if generated in-situ, and returning arrays of streamline points 
          and their mean spacing
        - generate all streamlines (up and downstream) and compute the overall
          mean streamline point spacing
          
    Instantiates and assigns to self attributes each of the principal workflow classes:
         - :class:`State <.State>` 
         - :class:`Geodata <.Geodata>` 
         - :class:`Preprocess <.Preprocess>` 
         - :class:`Trace <.Trace>` 
         - :class:`Analysis <.Analysis>` 
         - :class:`Mapping <.Mapping>` 
         - :class:`Plot <.Plot>` 
         - :class:`Save <.Save>` 

    

    
        """  
    def __init__(self, **kwargs):
        """
        Args:
            **kwargs (list): Keyword arguments.
    
        Initialize the principal 'streamlines' class instance, whose object
        will contain references to the each of the key class instances of 
        the streamlines workflow, e.g., geodata(), trace(), analysis()
        

        Attributes:
            parameters_file (str): Name of JSON parameters file 
                                   (parsed from kwargs 'parameters_file')
            parameters_dir (str): Path to folder containing JSON parameters file 
                                  (parsed from kwargs 'parameters_file')
        

        """
        
        # Are we running in a notebook?
        environment = os.path.basename(os.environ['_'])
        notebook_path = os.path.realpath(
            os.path.join((*os.path.split(os.getcwd())),'..','..'))
        if 'notebook' not in environment and 'jupyter' not in environment \
            and  'ipython' not in environment:
            environment = 'shell (probably)'
        print('Environment:',environment)
        
        #
        # Parse workflow parameters in JSON files and command line
        #
        if 'parameters_file' not in kwargs.keys() or kwargs['parameters_file'] is None:
            parameters_path = ''
            parameters_file = 'defaults'
#             raise ValueError('Must specify a parameters JSON file')
        else:
            parameters_path, parameters_file  = os.path.split(kwargs['parameters_file'])
        # Remove trailing .json for now if there is one
        parameters_file = ''.join(parameters_file.split('.json',-1))
        
        # Look for the JSON file in several likely places
        if parameters_path=='':   
            # Try the current directory - in UNIX, the dir from which slm was invoked         
            possible_paths = ['.']
            # Try the slm "home" dir given in the SLM environment variable
            try:
                possible_paths += [os.path.join(os.environ['SLM'],'json')]
            except:
                pass
            # Try the likely relative path to JSON parameters file
            try:
                possible_paths += [os.path.join(os.path.realpath('.'),'..','Parameters')]
            except:
                pass
#             guess = os.path.join(os.path.realpath('.'),'..','Parameters')
#             if os.path.isdir(guess):
#                 possible_paths += [guess]
            for path in possible_paths:
                real_path = os.path.realpath(os.path.join(path, parameters_file+'.json'))
#                 pdebug('path?',path,real_path)
                if os.path.isfile(real_path):
                    parameters_path = path
                    break
            # If we still can't find the specified JSON file, bail
            if parameters_path=='':
                raise ValueError('Cannot find JSON parameters file "{0}.json" in {1}'
                                 .format(parameters_file,possible_paths))
        parameters_path = os.path.realpath(parameters_path)
        
        # Read in parameters and assign to the State class instance
        imported_parameters, slm_path, slmnb_path \
            = import_parameters(parameters_path, parameters_file)
        if ( ('verbose' not in kwargs.keys() or kwargs['verbose'] is None and 
                  'verbose' in imported_parameters['state'].keys() 
                   and imported_parameters['state']['verbose']) 
             or ('verbose' in kwargs.keys() and kwargs['verbose'] is not None 
                   and kwargs['verbose'])):
            # dateutil seems like best way to insert local TZ info into naive datetime
            now = datetime.now().replace(tzinfo=tz.tzlocal())            
            print(now.strftime('\n%a %Y-%m-%d %H:%M:%S %Z%z'))
            print('\n**Initialization begin**') 
            print('Loaded JSON parameters file "{}"'
                  .format(os.path.realpath(os.path.join(parameters_path, 
                                                        parameters_file+'.json'))))
        # If the command line requires override of JSON-file parameters, make it happen
        try:
            override_parameters = kwargs['override_parameters']
        except:
            override_parameters = None
        if override_parameters is not None and override_parameters!='':
            # The override parameter string is itself JSON data 
            override_dict = loads(override_parameters)
            for item in override_dict.items():
                imported_parameters[item[0]].update(item[1])

        # Instantiate the workflow "state"
        self.state = State(None,imported_parameters)
        # Record the JSON parameters file path & name 
        self.state.parameters_path = parameters_path
        self.state.parameters_file = parameters_file

        # Parse command line args and assign to State attributes in most cases
        for item in kwargs.items():
            if item[0]=='do_plot':
                if item[1]=='0' or item[1]=='off' or item[1]=='false':
                    self.state.do_plot=False
                elif item[1]=='maps':
                    self.state.do_plot=True
                    imported_parameters['plot']['do_plot_maps']=True
                    imported_parameters['plot']['do_plot_distributions']=False
                elif item[1]=='pdfs' or item[1]=='distributions':
                    self.state.do_plot=True
                    imported_parameters['plot']['do_plot_maps']=False
                    imported_parameters['plot']['do_plot_distributions']=True
                elif item[1]=='all' or item[1]=='1' \
                        or item[1]=='True' or item[1]=='true':
                    self.state.do_plot=True
                    imported_parameters['plot']['do_plot_maps']=True
                    imported_parameters['plot']['do_plot_distributions']=True
                elif item[1] is not None:
                    self.state.do_plot = item[1]
            elif item[1] is not None:
                setattr(self.state, item[0],item[1])
        # Used by State.inventorize_run_state() and other State methods
        self.state.obj_list=[self.state]
        
        # Build list of paths to likely git repos
        repo_search_list = [['slm',  slm_path]]
        if 'notebook' in environment or 'jupyter' in environment \
            or  'ipython' in environment:
            repo_search_list += [['notebook', notebook_path]]
        else:
            environment = 'shell (probably)'
            
        # Likely git repo for JSON file
        json_path = os.path.realpath(
            os.path.join(os.path.realpath(parameters_path),'..','..'))
        repo_search_list += [['parameters', json_path]]
        json_path = os.path.realpath(
            os.path.join(os.path.realpath(parameters_path),'..','..','..'))
        repo_search_list += [['parameters', json_path]]
        
        # Instantiate slm data class instance
        self.geodata = Geodata(self.state,imported_parameters)
        repo_search_list += [['data', self.geodata.data_path[0]]]
                

#         print('repo_search_list:',repo_search_list)
        # Try to fetch latest slm-related git repo information
        #   - notably the commit hash, author, date & time
        if self.state.do_git_info:
            # Avoid the need to have the Python git module installed
            #   by only importing if do_git_info is true
            import git
            for repo_name, repo_path in repo_search_list:
#                 print(repo_name,repo_path)
                print('{} repo path: {}'.format(repo_name,repo_path))
                try:
                    # Create a short-lived git repo class instance
                    repo = git.Repo(repo_path)
                    # Grab its summary - seems to be the fastest way to get git info
                    summary = repo.git.show('--summary').split('\n')
                    git_info = [  [summary[0]]+[summary[1].split(' <')[0]]+[summary[2]] 
                                + ([summary[4]] if summary[4]!='' else []) 
                                + ([summary[5]] if summary[5]!='' else []) ]
                    setattr(self.state,repo_name+'_gitinfo',git_info)
                    # Print git info if verbose mode is on
                    self.print('{} repo info:'.format(repo_name))
                    self.pprint(git_info)
                except:
                    pass
        
        # Instantiate slm workflow classes
        self.preprocess = Preprocess(self.state,imported_parameters,self.geodata)
        self.trace      = Trace(self.state,imported_parameters,self.geodata,
                                self.preprocess)
        self.analysis   = Analysis(self.state,imported_parameters,self.geodata,
                                    self.preprocess, self.trace)
        self.mapping    = Mapping(self.state,imported_parameters,
                                  self.geodata,self.preprocess,self.trace,self.analysis)
        self.plot       = Plot(self.state,imported_parameters, self.geodata,
                               self.preprocess, self.trace, self.analysis, self.mapping)
        # Hackish way to allow plotting from mapping
        self.mapping._augment(self.plot)
        self.analysis._augment(self.mapping)
        self.save       = Save(self.state,imported_parameters,
                               self.geodata, self.preprocess, self.analysis, 
                               self.trace, self.mapping, self.plot)
        # Used by State.save_state()
        self.state.trace = self.trace
                             
        self.print('**Initialization end**\n')