예제 #1
0
    def setUpClass(cls):
        # We zip the test RHESSys mode to be nice to GitHub, unzip it
        cls.basedir = os.path.abspath('./rhessyscalibrator/tests/data/DR5')
        basedirZip = "%s.zip" % (cls.basedir, )
        if not os.access(basedirZip, os.R_OK):
            raise IOError(
                errno.EACCES,
                "Unable to read test RHESSys model zip %s" % basedirZip)
        basedirParent = os.path.split(basedirZip)[0]
        if not os.access(basedirParent, os.W_OK):
            raise IOError(
                errno.EACCES,
                "Unable to write to test RHESSys model parent dir %s" %
                basedirParent)
        zip = ZipFile(basedirZip, 'r')
        extractDir = os.path.split(cls.basedir)[0]
        zip.extractall(path=extractDir)

        # Build RHESSys
        currDir = os.getcwd()
        os.chdir('./rhessyscalibrator/tests/data/DR5/rhessys/src/rhessys')
        res = subprocess.call('make install RHESSYS_BIN=../../bin', shell=True)
        # Make sure binary is executable else some tests will fail
        os.chmod('../../bin/rhessys5.15',
                 stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
        os.chdir(currDir)

        cls.clusterCalibrator = RHESSysCalibrator()
예제 #2
0
    def main(self, args):
        # Set up command line options
        parser = argparse.ArgumentParser(description="Tool for performing behavioral model runs for RHESSys")
        parser.add_argument("-b", "--basedir", action="store", 
                          dest="basedir", required=True,
                          help="Base directory for the calibration session")
        
        parser.add_argument("-s", "--postprocess_session", action="store", type=int,
                          dest="postprocess_id", required=True,
                          help="Post-process session to use for behavioral runs.")
        
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument("-st", dest="startDate", nargs=4, type=int,
                           help='Start date and time for behavioral model runs, of the form "YYYY M D H"')
        group.add_argument("-c", "--cmdproto", dest="cmdproto",
                           help="Filename of cmd.proto to use for behavioral runs (relative to basedir)")
        
        parser.add_argument('-ed', dest='endDate', required=False, nargs=4, type=int,
                            help='Date date and time for behavioral model runs, of the form "YYYY M D H"')
        
        parser.add_argument("-f", "--behavioral_filter", action="store",
                          dest="behavioral_filter", required=False,
                          default="nse>0.5 and nse_log>0.5",
                          help="SQL where clause to use to determine which runs qualify as behavioral parameters.  E.g. 'nse>0.5 AND nse_log>0.5' (use quotes)")

        parser.add_argument("-u", "--user", action="store",
                          dest="user", required=False, default=os.getlogin(),
                          help="User to associate with the calibration session.  If not supplied, the value of os.getlogin() will be used.")
        
        parser.add_argument("-p", "--project", action="store",
                          dest="project", required=True,
                          help="Name of the project ot associate with the calibration session.")

        parser.add_argument("-j", "--jobs", action="store", type=calibrator.num_jobs_type,
                          dest="processes", required=True,
                          help="The number of simultaneous jobs (runs) to run at any given time in the calibration session (e.g. --jobs 32). Maximum is %s." % (calibrator.MAX_PROCESSORS,) ) 

        parser.add_argument("--simulator_path", action="store", 
                            dest="simulator_path", required=False,
                            help="Set path for LSF simulator.  When supplied, jobs will be submitted to the simulator, not via actual LSF commands.  Must be the absolute path (e.g. /Users/joeuser/rhessys_calibrator/lsf-sim)")

        parser.add_argument("-q", "--queue", action="store",
                          dest="queue_name", required=False,
                          help="Set queue name to submit jobs to using the underlying queue manager.  " +
                               "Applies only to non-process-based calibration runners (specified by parallel_mode option).")

        parser.add_argument("--parallel_mode", action="store", 
                          dest="parallel_mode", required=False,
                          default='lsf', choices=calibrator.PARALLEL_MODES,
                          help="Set method to use for running jobs in parallel.")

        parser.add_argument("--polling_delay", action="store", type=calibrator.polling_delay_type, 
                          dest="polling_delay", required=False,
                          default=1,
                          help="[ADVANCED] Set multiplier for how long to wait in between successive pollings of job status.  Default polling delay is 60 seconds, thus a multiplier of 5 will result in a delay of 5 minutes instead of 1 minute.  Maximum is %d." % (calibrator.MAX_POLLING_DELAY_MULT,) )

        parser.add_argument("--bsub_exclusive_mode", action="store_true",
                          dest="bsub_exclusive_mode", required=False,
                          help="[ADVANCED] For LSF parallel mode: run bsub with arguments \"-n 1 -R 'span[hosts=1]' -x\" to ensure jobs only run exclusively (i.e. the only job on a node). This can be useful for models that use a lot of memory.")

        parser.add_argument("--mem_limit", action="store", type=int, 
                          dest="mem_limit", required=False,
                          default=4,
                          help="For non-process based parallel modes: Specify memory limit for jobs.  Unit: gigabytes  Defaults to 4.")

        parser.add_argument("--wall_time", action="store",
                            type=int, dest="wall_time",
                            help="For PBS- and SLURM-based parallel mode: Specify wall time in hours that jobs should take.")

        parser.add_argument("-l", "--loglevel", action="store",
                          dest="loglevel", default="OFF", choices=['OFF', 'DEBUG', 'CRITICAL'], required=False,
                          help="Set logging level")
        
        options = parser.parse_args()
        
        # Handle command line parameters
        if "DEBUG" == options.loglevel:
            self._initLogger(logging.DEBUG)
        elif "CRITICAL" == options.loglevel:
            self._initLogger(logging.CRITICAL)
        else:
            self._initLogger(logging.NOTSET)
         
        if options.parallel_mode != calibrator.PARALLEL_MODE_PROCESS and not options.queue_name:
            sys.exit("""Please specify a queue/partition name that is valid for your system.""")
            
        wall_time = None
        if options.wall_time:
            if options.parallel_mode == calibrator.PARALLEL_MODE_PBS or options.parallel_mode == calibrator.PARALLEL_MODE_SLURM:
                if options.wall_time < 1 or options.wall_time > 168:
                    sys.exit("Wall time must be greater than 0 and less than 169 hours")
            wall_time = options.wall_time
            
        if not os.path.isdir(options.basedir) or not os.access(options.basedir, os.R_OK):
            sys.exit("Unable to read project directory %s" % (options.basedir,) )
        self.basedir = os.path.abspath(options.basedir) 
            
        self.logger.debug("parallel mode: %s" % options.parallel_mode)
        self.logger.debug("basedir: %s" % self.basedir)
        self.logger.debug("user: %s" % options.user)
        self.logger.debug("project: %s" % options.project)
        self.logger.debug("jobs: %d" % options.processes)
               
        if options.startDate:
            if not options.endDate:
                sys.exit("You must specify a simulation end date")
            startDate = datetime(options.startDate[0], 
                                 options.startDate[1],
                                 options.startDate[2],
                                 options.startDate[3])
            endDate = datetime(options.endDate[0], 
                               options.endDate[1],
                               options.endDate[2],
                               options.endDate[3])
            # Make sure start date is before end date
            if startDate >= endDate:
                sys.exit("Start date %s is not before end date %s" % (str(startDate), str(endDate)) )
            startDateStr = ' '.join([str(d) for d in options.startDate])
            endDateStr = ' '.join([str(d) for d in options.endDate])
        
            readCmdProtoFromRun = True
        
            self.logger.debug("start date: %s" % (startDate,) )
            self.logger.debug("end date: %s" % (endDate,) )
        if options.cmdproto:
            cmdProtoPath = os.path.join(self.basedir, options.cmdproto)
            if not os.access(cmdProtoPath, os.R_OK):
                sys.exit("Unable to read behavioral cmd.proto: %s" % (cmdProtoPath,) )
            
            readCmdProtoFromRun = False
            
            self.logger.debug("behavioral cmd.proto: %s" % (cmdProtoPath,) )
        
        notes = "Behavioral run, using filter: %s" % (options.behavioral_filter,)

        try:
            dbPath = RHESSysCalibrator.getDBPath(self.basedir)
            self.calibratorDB = ModelRunnerDB2(dbPath)
        
            # Get post-process session
            postproc = self.calibratorDB.getPostProcess(options.postprocess_id)
            if None == postproc:
                raise Exception("Post-process session %d was not found in the calibration database %s" % (options.postprocess_id, dbPath))
            # Get session
            calibSession = self.calibratorDB.getSession(postproc.session_id)
            if None == calibSession:
                raise Exception("Session %d was not found in the calibration database %s" % (postproc.session_id, dbPath))
            calibItr = calibSession.iterations

            # Get runs in calibration session
            runs = self.calibratorDB.getRunsInPostProcess(postproc.id, where_clause=options.behavioral_filter)
            numRuns = len(runs)
            print("\n{0}\n".format(notes))
            response = raw_input("%d runs selected of %d total runs (%.2f%%) in post process session %d, calibration session %d, continue? [yes | no] " % \
                                (numRuns, calibItr, (float(numRuns) / float(calibItr)) * 100, options.postprocess_id, postproc.session_id ) )
            response = response.lower()
            if response != 'y' and response != 'yes':
                # Exit normally
                return 0
            self.logger.debug("%d runs selected" % (numRuns,) )
            
            # Make sure we have everything we need to run behavioral runs    
            # Get list of worldfiles
            self.worldfiles = self.getWorldfiles(self.basedir)
            if len(self.worldfiles) < 1:
                raise Exception("No worldfiles found")
            self.logger.debug("worldfiles: %s" % self.worldfiles)          
            
            # Get tecfile name
            (res, tecfilePath) = self.getTecfilePath(self.basedir)
            if not res:
                raise Exception("No tecfile found")
   
            # Get RHESSys executable path
            (rhessysExecFound, rhessysExec, rhessysExecPath) = \
                self.getRHESSysExecPath(self.basedir)
            if not rhessysExecFound:
                raise Exception("RHESSys executable not found")
            
            if readCmdProtoFromRun:
                # Rewrite cmd_proto to use dates from command line
                cmd_proto = re.sub("-st (\d{4} \d{1,2} \d{1,2} \d{1,2})",
                                   "-st %s" % (startDateStr,),
                                   calibSession.cmd_proto)
                cmd_proto = re.sub("-ed (\d{4} \d{1,2} \d{1,2} \d{1,2})",
                                   "-ed %s" % (endDateStr,),
                                   cmd_proto)
                self.logger.debug("Original cmd.proto: %s" % (calibSession.cmd_proto,) )
                self.logger.debug("Behavioral cmd.proto: %s" % (cmd_proto,) )
            else:
                # Use cmd proto from file
                fd = open(cmdProtoPath)
                cmd_proto = fd.read()
                fd.close()
            
            # Strip any parameter ranges from cmd.proto
            cmd_proto_noparam = self.stripParameterRangesFromCmdProto(cmd_proto)
            
            # Pre-process cmd.proto to add rhessys exec and tecfile path
            cmd_proto_pre = self.preProcessCmdProto(cmd_proto_noparam,
                                                    os.path.join(rhessysExecPath, rhessysExec),
                                                    tecfilePath)
            
            # Check for explicit routing and surface flowtable in cmd_proto, get dicts of
            # flowtables from basedir
            (self.flowtablePath, self.surfaceFlowtablePath) = self.determineRouting(cmd_proto_noparam)
            
            # Create behavioral session
            self.session = self.createCalibrationSession(options.user, 
                                                         options.project,
                                                         numRuns,
                                                         options.processes,
                                                         self.basedir,
                                                         notes,
                                                         cmd_proto_noparam)
            # Create postprocess session
            behave_postproc_id = self.calibratorDB.insertPostProcess(self.session.id, postproc.obs_filename, postproc.fitness_period,
                                                                     exclude_date_ranges=postproc.exclude_date_ranges)
            
            # Initialize CalibrationRunner consumers for executing jobs
            (runQueue, consumers) = \
                RHESSysCalibrator.initializeCalibrationRunnerConsumers(self.basedir, self.logger,
                                                                       self.session.id, options.parallel_mode, options.processes, options.polling_delay,
                                                                       options.queue_name, 
                                                                       mem_limit=options.mem_limit,
                                                                       wall_time=wall_time, 
                                                                       bsub_exclusive_mode=options.bsub_exclusive_mode,
                                                                       simulator_path=options.simulator_path)
            
            # Dispatch runs to consumer
            # Note: we're iterating over behavioral runs to get their paramter values
            for (i, run) in enumerate(runs):
                itr = i + 1
                # Get parameters for run
                parameterValues = run.getCalibrationParameters()
                itr_cmd_proto = self.addParametersToCmdProto(cmd_proto_pre,
                                                             parameterValues)
                # For each world file
                for worldfile in self.worldfiles.keys():
                    self.logger.critical("Iteration %d, worldfile: %s" %
                                         (itr, worldfile))
                    # Create new ModelRun2 object for this run
                    behavioralRun = ModelRun2()
                    behavioralRun.session_id = self.session.id
                    behavioralRun.worldfile = worldfile
                    behavioralRun.setCalibrationParameters(parameterValues)
                    # Create new Runfitness2 object for this run, copying fitness parameters so that we can 
                    # draw undercertainty bounds later
                    behavioralRunfit = RunFitness2()
                    behavioralRunfit.postprocess_id = behave_postproc_id
                    behavioralRunfit.nse = run.run_fitness.nse
                    behavioralRunfit.nse_log = run.run_fitness.nse_log
                    behavioralRunfit.pbias = run.run_fitness.pbias
                    behavioralRunfit.rsr = run.run_fitness.rsr
                    behavioralRunfit.runoff_ratio = run.run_fitness.runoff_ratio
                    behavioralRunfit.userfitness = run.run_fitness.userfitness
                    behavioralRun.run_fitness = behavioralRunfit
                    
                    # Add worldfile and flowtable paths to command
                    if self.explicitRouting:
                        if self.surfaceFlowtable:
                            cmd_raw_proto = self.addWorldfileAndFlowtableToCmdProto(\
                                itr_cmd_proto, self.worldfiles[worldfile], 
                                self.flowtablePath[worldfile],
                                self.surfaceFlowtablePath[worldfile])
                        else:
                            cmd_raw_proto = self.addWorldfileAndFlowtableToCmdProto(\
                                itr_cmd_proto, self.worldfiles[worldfile], 
                                self.flowtablePath[worldfile])
                    else:
                        cmd_raw_proto = self.addWorldfileToCmdProto(\
                            itr_cmd_proto, self.worldfiles[worldfile])

                    # Finally, create output_path and generate cmd_raw
                    behavioralRun.output_path = self.createOutputPath(self.basedir,
                                                            self.session.id,
                                                            worldfile,
                                                            itr)
                    behavioralRun.cmd_raw = self.getCmdRawForRun(cmd_raw_proto,
                                                       behavioralRun.output_path)
        
                    if "process" == options.parallel_mode:
                        # Set job ID if we are in process parallel mode
                        #   (in lsf mode, we will use the LSF job number instead of itr)
                        behavioralRun.job_id = itr
        
                    # Dispatch to consumer
                    runQueue.put(behavioralRun)

            time.sleep(5)

            # Wait for all jobs to finish
            self.logger.critical("calling runQueue.join() ...")
            runQueue.join()
            for consumerProcess in consumers:
                consumerProcess.join()

            # Update session endtime and status
            self.calibratorDB.updateSessionEndtime(self.session.id,
                                                   datetime.utcnow(),
                                                   "complete") 
            
            print("\n\nBehavioral results saved to session {0}, post-process session {1}".format(self.session.id, behave_postproc_id))
        except:
            raise
        else:
            self.logger.debug("exiting normally")
            return 0
        finally:
            self.calibratorDB = None
예제 #3
0
 def __init__(self):
     RHESSysCalibrator.__init__(self)
                      CalibrationRunner class and descendants
            20110626: 1.0.3: refactored CalibrationRunner to ModelRunner for generalization
                             only update accounting of active jobs after verifying that
                             job returned by bjobs belongs to us (is in current session
                             record)
            20110628: fixed bug where --create option required iterations and jobs options
                      to be specified
            20110710: 1.0.4: Added subprocess parallelization method; Removed pipe-based IPC method.
            20120105: 1.0.5: Added support for running RHESSys in TOPMODEL model (i.e. no flow tables);
                             Added support for using the same calibration parameters for both horizontal
                             and vertical m parameter
                             Changed LSF queues to reflect those on KillDevil
            20120114: 1.0.6: Added support for running bsub with exclusive mode parameter
                             Removed old PIPE-based CalibrationRunner implementation
             20120215: 1.0.7: Updated --use_horizontal_m_for_vertical to apply also to the K parameter
                              (new option is --use_horizontal_m_and_K_for_vertical)
            20120327: 1.0.8: Added support for vgsen and svalt calibration parameters
            20120328: 1.0.9: Fixed bug where third -s parameter was not set by CalibrationParameters
            20120529: 1.0.10: Added filter for worldfile lookup that excludes redefine worldfiles.
                            

"""
import sys

from rhessyscalibrator.calibrator import RHESSysCalibrator

if __name__ == "__main__":
    RHESSysCalibrator = RHESSysCalibrator()
    # main's return value will be the exit code
    sys.exit(RHESSysCalibrator.main(sys.argv))
    def readBehavioralData(
        self, basedir, session_id, variable="streamflow", observed_file=None, behavioral_filter=None, end_date=None
    ):

        dbPath = RHESSysCalibrator.getDBPath(basedir)
        if not os.access(dbPath, os.R_OK):
            raise IOError(errno.EACCES, "The database at %s is not readable" % dbPath)
        self.logger.debug("DB path: %s" % dbPath)

        outputPath = RHESSysCalibrator.getOutputPath(basedir)
        if not os.access(outputPath, os.R_OK):
            raise IOError(errno.EACCES, "The output directory %s is  not readable" % outputPath)
        self.logger.debug("Output path: %s" % outputPath)

        rhessysPath = RHESSysCalibrator.getRhessysPath(basedir)

        calibratorDB = ModelRunnerDB(RHESSysCalibrator.getDBPath(basedir))

        # Make sure the session exists
        session = calibratorDB.getSession(session_id)
        if None == session:
            raise Exception("Session %d was not found in the calibration database %s" % (session_id, dbPath))
        if session.status != "complete":
            print "WARNING: session status is: %s.  Some model runs may not have completed." % (session.status,)
        else:
            self.logger.debug("Session status is: %s" % (session.status,))

        # Determine observation file path
        if observed_file:
            obs_file = observed_file
        else:
            # Get observered file from session
            assert session.obs_filename != None
            obs_file = session.obs_filename
        obsPath = RHESSysCalibrator.getObsPath(basedir)
        obsFilePath = os.path.join(obsPath, obs_file)
        if not os.access(obsFilePath, os.R_OK):
            raise IOError(errno.EACCES, "The observed data file %s is  not readable" % obsFilePath)
        self.logger.debug("Obs path: %s" % obsFilePath)

        # Get runs in session
        runs = calibratorDB.getRunsInSession(session.id, where_clause=behavioral_filter)
        numRuns = len(runs)
        if numRuns == 0:
            raise Exception("No runs found for session %d" % (session.id,))
        response = raw_input(
            "%d runs selected for plotting from session %d in basedir '%s', continue? [yes | no] "
            % (numRuns, session_id, os.path.basename(basedir))
        )
        response = response.lower()
        if response != "y" and response != "yes":
            # Exit normally
            return 0
        self.logger.debug("%d behavioral runs" % (numRuns,))

        # Read observed data from file
        obsFile = open(obsFilePath, "r")
        (obs_datetime, obs_data) = RHESSysOutput.readObservedDataFromFile(obsFile)
        obsFile.close()
        obs = pd.Series(obs_data, index=obs_datetime)
        if end_date:
            obs = obs[:end_date]

        self.logger.debug("Observed data: %s" % obs_data)

        likelihood = np.empty(numRuns)
        ysim = None
        x = None

        runsProcessed = False
        for (i, run) in enumerate(runs):
            if "DONE" == run.status:
                runOutput = os.path.join(rhessysPath, run.output_path)
                self.logger.debug(">>>\nOutput dir of run %d is %s" % (run.id, runOutput))
                tmpOutfile = RHESSysCalibrator.getRunOutputFilePath(runOutput)
                if not os.access(tmpOutfile, os.R_OK):
                    print "Output file %s for run %d not found or not readable, unable to calculate fitness statistics for this run" % (
                        tmpOutfile,
                        run.id,
                    )
                    continue

                tmpFile = open(tmpOutfile, "r")

                (tmp_datetime, tmp_data) = RHESSysOutput.readColumnFromFile(tmpFile, "streamflow")
                tmp_mod = pd.Series(tmp_data, index=tmp_datetime)
                # Align timeseries to observed
                (mod, obs) = tmp_mod.align(obs, join="inner")

                # Stash date for X values (assume they are the same for all runs
                if x == None:
                    x = [datetime.strptime(str(d), "%Y-%m-%d %H:%M:%S") for d in mod.index]

                # Put data in matrix
                dataLen = len(mod)
                if ysim == None:
                    # Allocate matrix for results
                    ysim = np.empty((numRuns, dataLen))
                assert np.shape(ysim)[1] == dataLen
                ysim[i,] = mod

                # Store fitness parameter
                likelihood[i] = run.nse

                tmpFile.close()
                runsProcessed = True

        return (runsProcessed, obs, x, ysim, likelihood)
예제 #6
0
    def main(self, args):
        # Set up command line options
        parser = argparse.ArgumentParser(description="Tool for performing behavioral model runs for RHESSys")
        parser.add_argument(
            "-b",
            "--basedir",
            action="store",
            dest="basedir",
            required=True,
            help="Base directory for the calibration session",
        )

        parser.add_argument(
            "-s",
            "--behavioral_session",
            action="store",
            type=int,
            dest="session_id",
            required=True,
            help="Session to use for behavioral runs.",
        )

        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument(
            "-st",
            dest="startDate",
            nargs=4,
            type=int,
            help='Start date and time for behavioral model runs, of the form "YYYY M D H"',
        )
        group.add_argument(
            "-c",
            "--cmdproto",
            dest="cmdproto",
            help="Filename of cmd.proto to use for behavioral runs (relative to basedir)",
        )

        parser.add_argument(
            "-ed",
            dest="endDate",
            required=False,
            nargs=4,
            type=int,
            help='Date date and time for behavioral model runs, of the form "YYYY M D H"',
        )

        parser.add_argument(
            "-f",
            "--behavioral_filter",
            action="store",
            dest="behavioral_filter",
            required=False,
            default="nse>0.5 and nse_log>0.5",
            help="SQL where clause to use to determine which runs qualify as behavioral parameters.  E.g. 'nse>0.5 AND nse_log>0.5' (use quotes)",
        )

        parser.add_argument(
            "-u",
            "--user",
            action="store",
            dest="user",
            required=False,
            default=os.getlogin(),
            help="User to associate with the calibration session.  If not supplied, the value of os.getlogin() will be used.",
        )

        parser.add_argument(
            "-p",
            "--project",
            action="store",
            dest="project",
            required=True,
            help="Name of the project ot associate with the calibration session.",
        )

        parser.add_argument(
            "-j",
            "--jobs",
            action="store",
            type=calibrator.num_jobs_type,
            dest="processes",
            required=True,
            help="The number of simultaneous jobs (runs) to run at any given time in the calibration session (e.g. --jobs 32). Maximum is %s."
            % (calibrator.MAX_PROCESSORS,),
        )

        parser.add_argument(
            "--simulator_path",
            action="store",
            dest="simulator_path",
            required=False,
            help="Set path for LSF simulator.  When supplied, jobs will be submitted to the simulator, not via actual LSF commands.  Must be the absolute path (e.g. /Users/joeuser/rhessys_calibrator/lsf-sim)",
        )

        parser.add_argument(
            "-q",
            "--queue",
            action="store",
            dest="lsf_queue",
            required=False,
            default="day",
            choices=calibrator.LSF_QUEUES,
            help="Set queue name to pass to LSF job submission command.",
        )

        parser.add_argument(
            "--parallel_mode",
            action="store",
            dest="parallel_mode",
            required=False,
            default="lsf",
            choices=calibrator.PARALLEL_MODES,
            help="Set method to use for running jobs in parallel.",
        )

        parser.add_argument(
            "--polling_delay",
            action="store",
            type=calibrator.polling_delay_type,
            dest="polling_delay",
            required=False,
            default=1,
            help="[ADVANCED] Set multiplier for how long to wait in between successive pollings of job status.  Default polling delay is 60 seconds, thus a multiplier of 5 will result in a delay of 5 minutes instead of 1 minute.  Maximum is %d."
            % (calibrator.MAX_POLLING_DELAY_MULT,),
        )

        parser.add_argument(
            "--bsub_exclusive_mode",
            action="store_true",
            dest="bsub_exclusive_mode",
            required=False,
            help="[ADVANCED] run bsub with arguments \"-n 1 -R 'span[hosts=1]' -x\" to ensure jobs only run exclusively (i.e. the only job on a node). This can be useful for models that use a lot of memory.",
        )

        parser.add_argument(
            "--bsub_mem_limit",
            action="store",
            type=int,
            dest="bsub_mem_limit",
            required=False,
            default=4,
            help="[ADVANCED] run bsub with -M mem_limit option.  Defaults to 4GB",
        )

        parser.add_argument(
            "-l",
            "--loglevel",
            action="store",
            dest="loglevel",
            default="OFF",
            choices=["OFF", "DEBUG", "CRITICAL"],
            required=False,
            help="Set logging level",
        )

        options = parser.parse_args()

        # Handle command line parameters
        if "DEBUG" == options.loglevel:
            self._initLogger(logging.DEBUG)
        elif "CRITICAL" == options.loglevel:
            self._initLogger(logging.CRITICAL)
        else:
            self._initLogger(logging.NOTSET)

        if not os.path.isdir(options.basedir) or not os.access(options.basedir, os.R_OK):
            sys.exit("Unable to read project directory %s" % (options.basedir,))
        self.basedir = os.path.abspath(options.basedir)

        self.logger.critical("parallel mode: %s" % options.parallel_mode)
        self.logger.debug("basedir: %s" % self.basedir)
        self.logger.debug("user: %s" % options.user)
        self.logger.debug("project: %s" % options.project)
        self.logger.debug("jobs: %d" % options.processes)

        if options.startDate:
            if not options.endDate:
                sys.exit("You must specify a simulation end date")
            startDate = datetime(options.startDate[0], options.startDate[1], options.startDate[2], options.startDate[3])
            endDate = datetime(options.endDate[0], options.endDate[1], options.endDate[2], options.endDate[3])
            # Make sure start date is before end date
            if startDate >= endDate:
                sys.exit("Start date %s is not before end date %s" % (str(startDate), str(endDate)))
            startDateStr = " ".join([str(d) for d in options.startDate])
            endDateStr = " ".join([str(d) for d in options.endDate])

            readCmdProtoFromRun = True

            self.logger.debug("start date: %s" % (startDate,))
            self.logger.debug("end date: %s" % (endDate,))
        if options.cmdproto:
            cmdProtoPath = os.path.join(self.basedir, options.cmdproto)
            if not os.access(cmdProtoPath, os.R_OK):
                sys.exit("Unable to read behavioral cmd.proto: %s" % (cmdProtoPath,))

            readCmdProtoFromRun = False

            self.logger.debug("behavioral cmd.proto: %s" % (cmdProtoPath,))

        notes = "Behavioral run, using filter: %s" % (options.behavioral_filter,)

        if options.simulator_path:
            run_cmd = RHESSysCalibrator.getRunCmdSim(options.simulator_path)
            run_status_cmd = RHESSysCalibrator.getRunStatusCmdSim(options.simulator_path)
        elif "lsf" == options.parallel_mode:
            run_cmd = RHESSysCalibrator.getRunCmd(options.bsub_mem_limit, options.bsub_exclusive_mode)
            run_status_cmd = RHESSysCalibrator.getRunStatusCmd()
        else:
            run_cmd = run_status_cmd = None

        try:
            dbPath = RHESSysCalibrator.getDBPath(self.basedir)
            self.calibratorDB = ModelRunnerDB(dbPath)

            # Get calibration session
            calibSession = self.calibratorDB.getSession(options.session_id)
            if None == calibSession:
                raise Exception(
                    "Session %d was not found in the calibration database %s" % (options.session_id, dbPath)
                )
            calibItr = calibSession.iterations

            # Get runs in calibration session
            runs = self.calibratorDB.getRunsInSession(calibSession.id, options.behavioral_filter)
            numRuns = len(runs)
            print(notes)
            response = raw_input(
                "%d runs selected of %d total runs (%.2f%%) in session %d, continue? [yes | no] "
                % (numRuns, calibItr, (float(numRuns) / float(calibItr)) * 100, options.session_id)
            )
            response = response.lower()
            if response != "y" and response != "yes":
                # Exit normally
                return 0
            self.logger.debug("%d runs selected" % (numRuns,))

            # Make sure we have everything we need to run behavioral runs
            # Get list of worldfiles
            self.worldfiles = self.getWorldfiles(self.basedir)
            if len(self.worldfiles) < 1:
                raise Exception("No worldfiles found")
            self.logger.debug("worldfiles: %s" % self.worldfiles)

            # Get tecfile name
            (res, tecfilePath) = self.getTecfilePath(self.basedir)
            if not res:
                raise Exception("No tecfile found")

            # Get RHESSys executable path
            (rhessysExecFound, rhessysExec, rhessysExecPath) = self.getRHESSysExecPath(self.basedir)
            if not rhessysExecFound:
                raise Exception("RHESSys executable not found")

            if readCmdProtoFromRun:
                # Rewrite cmd_proto to use dates from command line
                cmd_proto = re.sub(
                    "-st (\d{4} \d{1,2} \d{1,2} \d{1,2})", "-st %s" % (startDateStr,), calibSession.cmd_proto
                )
                cmd_proto = re.sub("-ed (\d{4} \d{1,2} \d{1,2} \d{1,2})", "-ed %s" % (endDateStr,), cmd_proto)
                self.logger.debug("Original cmd.proto: %s" % (calibSession.cmd_proto,))
                self.logger.debug("Behavioral cmd.proto: %s" % (cmd_proto,))
            else:
                # Use cmd proto from file
                fd = open(cmdProtoPath)
                cmd_proto = fd.read()
                fd.close()

            # Pre-process cmd.proto to add rhessys exec and tecfile path
            cmd_proto_pre = self.preProcessCmdProto(cmd_proto, os.path.join(rhessysExecPath, rhessysExec), tecfilePath)

            # Check for explicit routing and surface flowtable in cmd_proto, get dicts of
            # flowtables from basedir
            (self.flowtablePath, self.surfaceFlowtablePath) = self.determineRouting(cmd_proto)

            # Create behavioral session
            self.session = self.createCalibrationSession(
                options.user, options.project, numRuns, options.processes, self.basedir, notes, cmd_proto
            )
            # Get observation file from calibrationSession
            self.session.obs_filename = calibSession.obs_filename
            self.calibratorDB.updateSessionObservationFilename(self.session.id, self.session.obs_filename)

            # Initialize CalibrationRunner consumers for executing jobs
            (runQueue, consumers) = RHESSysCalibrator.initializeCalibrationRunnerConsumers(
                self.basedir,
                self.logger,
                self.session.id,
                options.parallel_mode,
                options.processes,
                options.polling_delay,
                options.lsf_queue,
                run_cmd,
                run_status_cmd,
            )

            # Dispatch runs to consumer
            # Note: we're iterating over behavioral runs to get their paramter values
            for (i, run) in enumerate(runs):
                itr = i + 1
                # Get parameters for run
                parameterValues = run.getCalibrationParameters()
                itr_cmd_proto = self.addParametersToCmdProto(cmd_proto_pre, parameterValues)
                # For each world file
                for worldfile in self.worldfiles.keys():
                    self.logger.critical("Iteration %d, worldfile: %s" % (itr, worldfile))
                    # Create new ModelRun object for this run
                    behavioralRun = ModelRun()
                    behavioralRun.session_id = self.session.id
                    behavioralRun.worldfile = worldfile
                    behavioralRun.setCalibrationParameters(parameterValues)
                    # Copy fitness parameters so that we can draw undercertainty bounds later
                    behavioralRun.nse = run.nse
                    behavioralRun.nse_log = run.nse_log
                    behavioralRun.pbias = run.pbias
                    behavioralRun.rsr = run.rsr
                    behavioralRun.user1 = run.user1
                    behavioralRun.user2 = run.user2
                    behavioralRun.user3 = run.user3
                    behavioralRun.fitness_period = run.fitness_period

                    # Add worldfile and flowtable paths to command
                    if self.explicitRouting:
                        if self.surfaceFlowtable:
                            cmd_raw_proto = self.addWorldfileAndFlowtableToCmdProto(
                                itr_cmd_proto,
                                self.worldfiles[worldfile],
                                self.flowtablePath[worldfile],
                                self.surfaceFlowtablePath[worldfile],
                            )
                        else:
                            cmd_raw_proto = self.addWorldfileAndFlowtableToCmdProto(
                                itr_cmd_proto, self.worldfiles[worldfile], self.flowtablePath[worldfile]
                            )
                    else:
                        cmd_raw_proto = self.addWorldfileToCmdProto(itr_cmd_proto, self.worldfiles[worldfile])

                    # Finally, create output_path and generate cmd_raw
                    behavioralRun.output_path = self.createOutputPath(self.basedir, self.session.id, worldfile, itr)
                    behavioralRun.cmd_raw = self.getCmdRawForRun(cmd_raw_proto, behavioralRun.output_path)

                    if "process" == options.parallel_mode:
                        # Set job ID if we are in process parallel mode
                        #   (in lsf mode, we will use the LSF job number instead of itr)
                        behavioralRun.job_id = itr

                    # Dispatch to consumer
                    runQueue.put(behavioralRun)

            time.sleep(5)

            # Wait for all jobs to finish
            self.logger.critical("calling runQueue.join() ...")
            runQueue.join()
            for consumerProcess in consumers:
                consumerProcess.join()

            # Update session endtime and status
            self.calibratorDB.updateSessionEndtime(self.session.id, datetime.utcnow(), "complete")
        except:
            raise
        else:
            self.logger.debug("exiting normally")
            return 0
        finally:
            self.calibratorDB = None