"""
Statistics-Based Neural Network Simulator

Robert A McDougal, 2011
"""

import integrator
import simulation
import os
import sim_data
import synapse
import threading
import time
import Queue
import errors
import noise
import neuronGroup
import tokenizer
import platform
import sys
import cPickle
import random
from fileManagement import delete_file
from parallelManagement import parallel_run_over_queue
from warnings import warn
import copy
import analysis
import inspect
from analysis import *

try:
    import matplotlib
    if not hasattr(matplotlib, 'lines'):
        matplotlib.use('Agg')
    from snnet.visualize import *
except ImportError:
    pass



# integrators
xpp_integrator = integrator.XppIntegrator()
fast_xpp_integrator = integrator.GccXppIntegrator()
matlab_integrator = integrator.MatlabIntegrator()
matlab_mex_integrator = integrator.MatlabMexIntegrator()

# file types
spikes = sim_data.Spikes
full = sim_data.Full2
csv = sim_data.Csv
enhanced = sim_data.Enhanced

# errors
SyntaxError = errors.SyntaxError
SimulationFailedError = errors.SimulationFailedError

# synapses
ion_synapse = synapse.IonSynapse
applied_current = synapse.AppliedCurrent

# misc
load_simulation = sim_data.load

# default integration method, ability to fork
if platform.system() in ['Darwin', 'Linux']:
    default_integrator = fast_xpp_integrator
    _can_fork = True
else:
    default_integrator = xpp_integrator
    _can_fork = False

# temporary directory handler
default_temp_dir_handler = integrator.DefaultTempDirHandler()

# thread local storage
_snnet_thread_data = threading.local()
_snnet_thread_data.simulation = simulation.Simulation(default_integrator)
_snnet_thread_data.run_id = 0
_snnet_thread_data.forks = []
_snnet_thread_data.override = {}

def name_time(name, toffset = 0):
    """Assign a name to a particular time.
    
    Parameters:
        name -- the name to assign
        toffset -- the offset from the current simulation time
    """
    _current_simulation().name_time(name, toffset)

def create_subdirectories(base, *subdirs):
    """Create subdirectories (one or many) in the directory base.
    
    If the subdirectory already exists, it will not be modified.
    
    If the base directory path does not exist, it will be created as well.
    
    This function is useful when analyzing results.
    
    Examples:
        create_subdirectories('base_folder', 'results')
        create_subdirectories('base_folder', 'potentials', 'rasters')
    """
    for name in subdirs:
        try:
            os.makedirs(os.path.join(base, name))
        except:
            pass

def simplify(expression):
    """Attempt to simplify the expression.
    
    ex:
        simplify('1+1') -- returns '2'
    """
    return tokenizer.tokens_to_string(tokenizer.parse_string(expression))
        


def default_filename(extension=None, include_run_id=True):
    """Return a filename based on the parameter sweep.
    
    Parameters:
        extension -- a filename extension to append
        include_run_id -- should we include the run_id in the name?
    
    Names are generated by writing the parameter names and values
    separated by underscores, then the run_id (after an underscore,
    if include_run_id is True)
    
    Example:
    
    If the run command sweeps or overrides gna = 2 and gk = 1 and run_id = 0,
    default_filename() returns 'gk_1_gna_2_0' or 'gna_2_gk_1_0'
    (no guarantee is made about the order)
    likewise,
    default_filename('csv') returns 'gk_1_gna_2_0.csv' or 'gna_2_gk_1_0.csv'
    """
    _current_simulation()
    params = overrides()    
    filename = '_'.join(k + '_' + str(v)
                        for k, v in zip(params.keys(), params.values()))
    if include_run_id:
        filename += '_' + str(get_run_id())
    if extension is not None:
        filename += '.' + extension
    return filename


def parameters_from_dir(directory):
    """Returns the parameters for files saved in the given directory.
    
    Assumes the filenames were chosen according to default_filename.
    
    Ignores extensions, assuming at least one character of the extension
    is not a number.
    """
    files = os.listdir(directory)
    params = []
    digits = '0123456789'
    for filename in files:
        if '.' in filename:
            extension_index = filename[:: -1].index('.')
            extension = filename[-extension_index :]
            base_filename = filename[: -extension_index - 1]
            for c in extension:
                if c not in digits:
                    f = base_filename
                    break
            else:
                f = filename            
        else:
            f = filename
        parts = f.split('_')
        i = 0
        d = {}
        while i < len(parts) - 1:
            name = []
            val = None
            while val is None:
                if name:
                    try:
                        val = float(parts[i])
                    except:
                        name.append(parts[i])
                else:
                    name = [parts[i]]
                i += 1
            d['_'.join(name)] = [val]
        params.append(d)
    return params

##################################################################
#
# graphics
#
##################################################################

def gnuplot(filename, *data, **figure_properties):
    """Use gnuplot to plot a line graph.
    
    data consists of the x and y values to plot followed by any line style information.
    Supported line style parameters:
        line type -- solid, dash, dot, dot-dash, or a number
        line width -- thin, normal, thick, or a number
        color -- line color
        axis -- which axis to plot against: 1 (default) or 2
        legend -- identifying string for the legend

    Supported figure_properties:
        x_lim
        y_lim
        x2_lim
        y2_lim
        x_label
        y_label
        grid -- True or False
        title
        legend -- anything that can go after 'set key' in gnuplot
        use_color -- can be True (default) or False
    
    Example:
    
        gnuplot('output.pdf', x1, y1, 'legend', 'graph1', x2, y2, x3, y3, title = '3 graphs')

    Note:
    
    Use the legend figure_property to set the location, e.g. 'left bottom'
    
    If no legends are set, the key is not displayed.
    
    Requires gnuplot and convert to be installed.
    """
    x_lim = figure_properties.pop('x_lim', None)
    y_lim = figure_properties.pop('y_lim', None)
    x2_lim = figure_properties.pop('x2_lim', None)
    y2_lim = figure_properties.pop('y2_lim', None)
    x_label = figure_properties.pop('x_label', None)
    y_label = figure_properties.pop('y_label', None)
    x2_label = figure_properties.pop('x2_label', None)
    y2_label = figure_properties.pop('y2_label', None)
    title = figure_properties.pop('title', None)
    use_color = figure_properties.pop('use_color', True)
    legend_settings = figure_properties.pop('legend', None)
    grid = figure_properties.pop('grid', False)
    if figure_properties:
        raise Exception('Unknown figure_properties: ' + ', '.join(figure_properties.keys()))

    # split data into x, y pairs
    x_y_data = []
    i = 0
    has_legend = False
    while i < len(data):
        if i == len(data) - 1:
            raise Exception('cannot split into x, y pairs')
        legend = None
        if use_color:
            line_type = 1
        else:
            line_type = None
        line_width = 3
        line_color = None
        axis = 'x1y1'
        x = data[i]
        y = data[i + 1]
        i += 2
        while i < len(data) and isinstance(data[i], str):
            if i == len(data) - 1:
                raise Exception('No value for option: ' + data[i])
            val = str(data[i + 1])
            val_lower = val.lower()
            par = data[i].lower()
            i += 2
            if par in ('legend', 'key'):
                legend = val
            elif par == 'axis':
                try:
                    val = int(val)
                except:
                    raise Exception('Unknown axis: ' + val)
                if val == 1:
                    axis = 'x1y1'
                elif val == 2:
                    axis = 'x2y2'
                else:
                    raise Exception('Unknown axis: ' + val)
            elif par in ('line_width', 'linewidth', 'line width'):
                if val_lower in ('thin', 'light'):
                    line_width = 1
                if val_lower in ('normal', 'default', 'medium'):
                    line_width = 4
                elif val_lower in ('thick', 'heavy'):
                    line_width = 7
                else:
                    try:
                        line_width = int(val_lower)
                    except:
                        raise Exception('unknown line width: ' + val)
            elif par in ('line_type', 'linetype', 'line type'):
                if val_lower in ('-', 'solid'):
                    line_type = 1
                elif val_lower in ('--', 'dashed', 'dash'):
                    line_type = 2
                elif val_lower in (':', 'dotted', 'dot'):
                    line_type = 3
                elif val_lower in ('-.', 'dot-dash', 'dot dash', 'dot_dash', 'dash-dot', 'dash_dot', 'dash dot'):
                    line_type = 6
                else:
                    line_type = val
            elif par in ('color', 'colour'):
                line_color = val
            else:
                raise Exception('unknown option: ' + par)
            
        x_y_data.append({'x': x, 'y': y, 'legend': legend, 'line_type': line_type,
                        'line_width': line_width, 'line_color': line_color,
                        'axis': axis})

    
    
    tmp_file = 'tmp_snnet_gnuplot_%d_%d' % (os.getpid(), random.randint(0, 10000))
    if use_color:
        terminal = 'postscript enhanced color'
    else:
        terminal = 'postscript enhanced'
    gnuplot_script = ('set terminal ' + terminal + '\n'
                      'set out "%s.ps"\n' % tmp_file)
    if grid:
        gnuplot_script += 'set grid\n'
    if legend_settings is not None:
        gnuplot_script += 'set key ' + legend_settings + '\n'
    if x_lim is not None:
        gnuplot_script += 'set xrange [%g : %g]\n' % x_lim
    if y_lim is not None:
        gnuplot_script += 'set yrange [%g : %g]\n' % y_lim
    if x2_lim is not None:
        gnuplot_script += 'set x2range [%g : %g]\n' % x2_lim
        if x2_lim != x_lim:
            gnuplot_script += 'set xtics nomirror\n'
            gnuplot_script += 'set x2tics\n'
    if y2_lim is not None:
        gnuplot_script += 'set y2range [%g : %g]\n' % y2_lim
        if y2_lim != y_lim:
            gnuplot_script += 'set ytics nomirror\n'
            gnuplot_script += 'set y2tics\n'
    if x_label is not None:

        gnuplot_script += 'set xlabel "%s"\n' % x_label
    if y_label is not None:
        gnuplot_script += 'set ylabel "%s"\n' % y_label
    if x2_label is not None:
        gnuplot_script += 'set x2label "%s"\n' % x2_label
    if y2_label is not None:
        gnuplot_script += 'set y2label "%s"\n' % y2_label
    if title is not None:
        gnuplot_script += 'set title "%s"\n' % title
    for i, d in enumerate(x_y_data):
        line_type = d['line_type']
        line_width = d['line_width']
        line_color = d['line_color']
        line_settings = ''
        if line_type is not None:
            line_settings += ' lt ' + str(line_type)
        if line_width is not None:
            line_settings += ' lw %d' % line_width
        if line_color is not None:
            line_settings += ' lc rgb "%s"' % line_color
        if line_settings:
            gnuplot_script += 'set style line %d %s\n' % (i + 1, line_settings)
    
    if not x_y_data:
        return

    plot_lines = []
    for i, d in enumerate(x_y_data):
        current = '"-" using 1:2 axes %s ls %d with lines' % (d['axis'], i + 1)
        legend = d['legend']
        if legend is not None:
            current += ' title "%s"' % legend
        else:
            current += ' notitle'
        plot_lines.append(current)

    gnuplot_script += ('plot ' + ', '.join(plot_lines) + '\n')

    if platform.system() in ['Darwin', 'Linux']:
        g = os.popen('gnuplot', 'wb')
    else:
        g = open(tmp_file + '.g', 'wb')

    g.write(gnuplot_script)
    for d in x_y_data:
        export_to_csv(g, d['x'], d['y'], separator = ' ')
        g.write('e\n')
    g.write('exit')
    if platform.system() in ['Darwin', 'Linux']:
        if g.close():
            raise Exception('error accessing gnuplot')
    else:
        g.close()
        os.system('gnuplot %s.g' % tmp_file)
        try:
            os.unlink(tmp_file + '.g')
        except:
            pass
                
    converted = False
    if filename[-4 : ] == '.pdf':
        # attempt to use ps2pdf
        exit_code = os.system('ps2pdf ' + tmp_file + '.ps ' + filename)
        if not exit_code:
            converted = True
    if not converted:
        os.system('convert ' + tmp_file + '.ps ' + filename)
    try:
        os.unlink(tmp_file + '.ps')
    except:
        pass


##################################################################
#
# simulation control
#
##################################################################

def define_neuron(neuron, potential_var=None, ignore_case=False):
    """Define a neuron from a snnet.
    
    Parameters:
        neuron -- the snnet source
        potential_var -- the variable corresponding to membrane potential, if any
        ignore_case -- should we drop capitalization in the snnet?
    
    Note:
    
    neuron is a string containing either a filename or snnet equations.
    """
    return _current_simulation().define_neuron(neuron, potential_var, ignore_case=ignore_case)


def neurons(neuron, num=1, potential_var=None, ignore_case=False, verify_defined=True, **params):
    """Define a group of neurons from a snnet.
    
    Parameters:
        neuron -- the snnet source
        num -- how many neurons to define
        potential_var -- the variable corresponding to membrane potential, if any
        ignore_case -- should we drop capitalization in the snnet?
        verify_defined -- should we check dynamics for completeness
        params -- any parameter changes to make to the snnet
    
    Note:
    
    neuron is a string containing either a filename or snnet equations.
    
    Any changes to parameters made here can be overriden by a parameter sweep, or
    with the param argument to run.
    """
    param_overrides = _current_simulation()._param_override
    n = define_neuron(neuron, potential_var, ignore_case)
    
    # create the neurons
    group = n(num, verify_defined=verify_defined)
    
    # adjust parameters as needed
    for v in params:
        if v not in param_overrides:
            group.set(v, params[v])
    
    return group


def default_value(var, val):
    """Set a default value in the simulation.
    
    This will override the values specified in the snnet file of any dynamics
    defined after this command has been run (but not those already defined.)
    
    Values overriden in the simulation (e.g. via sweep or param in run)
    take precedence to values set here."""
    sim = _current_simulation()
    val = sim.value(val)
    params = sim._param_override
    if var not in params:
        params[var] = val


def overrides():
    """Return the parameter overrides specified in the run command, if any."""
    _current_simulation()
    return _snnet_thread_data.override

def run_to(t_stop):
    """Run the simulation until a specified time."""
    _current_simulation().run_to(t_stop)

def advance(t):
    """Run the simulation for an addition time t."""
    _current_simulation().advance(t)

def set_initial_time(t):
    """Set the initial time for the simulation to t."""
    if _current_simulation()._data_t:
        raise Exception('Cannot set initial time after simulation has been run.')
    _current_simulation().t = t

def load_dynamics(dynamics, ignore_case=False):
    """Load shared (global) dynamics from dynamics.
    
    Parameters:
        dynamics -- the snnet source
        ignore_case -- should we drop capitalization
    
    Note:
        dynamics can be either a filename or a string containing equations.
    """
    warn(DeprecationWarning('load_dynamics is deprecated; use global_dynamics instead.'))
    return global_dynamics(dynamics, ignore_case=ignore_case)

def global_dynamics(dynamics, ignore_case=False, verify_defined=True, **params):
    """Load shared (global) dynamics from dynamics.
    
    Parameters:
        dynamics -- the snnet source
        ignore_case -- should we drop capitalization
        verify_defined -- should we check dynamics for completeness
        params -- parameters with values
    
    Note:
        dynamics can be either a filename or a string containing equations.
        
        parameters set here override the values in the snnet but are themselves
        overriden by sweeps or the param argument to run.
    """
    sim = _current_simulation()
    param_overrides = sim._param_override
    dyn = sim.global_dynamics(dynamics, ignore_case=ignore_case)
    dyn._verify_defined = verify_defined
    # adjust parameters as needed
    for v in params:
        if v not in param_overrides:
            dyn.set(v, params[v])

    return dyn

def set_run_id(run_id):
    """Sets the run_id, the default filename."""
    global _snnet_thread_data
    _current_simulation()
    _snnet_thread_data.run_id = run_id
    
def get_run_id():
    """Return the run_id, the default filename.
    
    For simulations started via run, this is a number corresponding to the
    random seed used."""
    _current_simulation()
    return _snnet_thread_data.run_id

def save_data(data=None):
    """Append or get to the current list of saved data"""
    return _current_simulation().save_data(data)

def current_simulation():
    """Return the current Simulation.
    
    Used for advanced simulation control.
    """
    return _current_simulation()
    
def _current_simulation():
    """Return the current Simulation.
    
    Used for advanced simulation control.
    """
    try:
        sim = _snnet_thread_data.simulation
        # update list of NeuronGroup objects
        variables = inspect.stack()[2][0].f_locals
        for var, val in zip(variables.keys(), variables.values()):
            if isinstance(val, neuronGroup.NeuronGroup):
                sim._cell_group_objects[var] = val
                sim._cell_groups[var] = val._list
        return sim
    except AttributeError:
        return None

def simulation_data():
    """Return a SimData instance corresponding to the current simulation.
    
    Do not use this instance after changing the simulation; that behavior is
    undefined and subject to change.
    """
    result = sim_data.Full()
    result._initialize(_current_simulation(), None, get_run_id())
    return result

def set_max_advance_time(max_advance_time):
    """Sets the maximum time a simulation will advance in one step.
    
    Smaller values potentially reduce memory needs but increase run time.
    
    Applies only to the current simulation.
    """
    _current_simulation().max_advance_time = max_advance_time

def set_simulation(new_sim):
    """Used for running multiple simulations with the same initial portion.
    
    Pass in either a Simulation or a Full instance.
    
    Warning:
    
    In most cases, you will want to pass in a copy of a Simulation, not the original.
    """
    global _snnet_thread_data
    
    old_sim = _current_simulation()
    
    if isinstance(new_sim, sim_data.Full):
        new_sim = new_sim._sim
    _snnet_thread_data.simulation = new_sim
    
    # update NeuronGroups from old_sim, if any
    if old_sim is not None:
        for group in old_sim._cell_group_objects.values():
            group._simulation = new_sim
        new_sim._cell_groups = old_sim._cell_groups
        new_sim._cell_group_objects = old_sim._cell_group_objects

def integrator_method(integration_method=None):
    """Set or get the integrator"""
    return _current_simulation().integrator_method(integration_method)


def total_volume(volume=None):
    """Set or get the total volume (extracellular + cellular)."""
    sim = _current_simulation()
    if volume is not None:
        sim._total_volume = sim.value(volume)
    return float(sim._total_volume)

def rand(number):
    """Get a random number using the snnet syntax.
    
    e.g.
    
        rand('[3 : 5]') -- returns a uniform r.v. float between 3 and 5
        rand('3 [2]') -- returns a normal r.v. float with mean 3, std dev 2
        rand('5 [1%]') -- normal r.v. float, mean 5, std dev 1% of mean (.05)

    Random numbers are generated via Python's random module, which uses the
    Mersenne Twister algorithm (in Python 2.3+).
    """
    return _current_simulation().collapse_probability(number)

def random_instance():
    """Return the random instance used by snnet.
    
     e.g.
     
        r = random_instance()
        r.uniform(5, 12) -- uniform r.v. between 5 and 12
        r.betavariate(alpha, beta) -- beta r.v.
 
    The return value is an instance of random.Random. See the Python documentation
    for more information.
    
    Simulations initiated via run have the random seed set to the run_id.
    """
    return _current_simulation().random
    
    
def save_simulation(output_dir, filename=None, save_type=full):
    """Saves the current simulation.
    
    If filename is None, uses current run_id."""
    if filename is None:
        filename = get_run_id()

    # create the directory if it does not already exist
    try:
        os.makedirs(output_dir)
    except:
        pass
            
    _current_simulation().save(output_dir, filename, save_type)
    

def fork():
    """Use to initiate a new fork in the simulation."""
    _snnet_thread_data.forks.append(copy_simulation())

def end_fork():
    """Use to end a simulation fork and return to the previous state."""
    if _snnet_thread_data.forks:
        set_simulation(_snnet_thread_data.forks.pop())
        _current_simulation()
    else:
        raise errors.SimulationFailedError(message = 'end_fork() without matching fork()')
    
def copy_simulation():
    """Returns a copy of the current simulation."""
    return copy.deepcopy(_current_simulation())

def value(var, val=None):
    """Return the simulation parameter var if it exists, else val."""
    if var in _current_simulation()._param_override:
        return float(_current_simulation()._param_override[var])
    else:
        return val

def group(name):
    """Return the cell group with the given name."""
    return _current_simulation().group(name)

def export_to_csv(filename, *args, **kwargs):
    """Save lists to csv file.
    
    Parameters:
        filename -- output csv file
        list1, list2, ... -- lists to output to the file
        separator -- separator to use between data points.
        
    Note:
    
    filename can be a string name or a file object.
    If filename is a file object, will leave open when done.
    
    If no lists are passed, no file will be created.
    
    separator defaults to ', '
    
    Example usage:
    
    export_to_csv('results', x, y)
    export_to_csv('results', x, y, separator = ' ')
    """
    
    separator = kwargs.pop('separator', ', ')
    newline = kwargs.pop('newline', '\n')
    if kwargs:
        raise Exception('Unknown option: ' + kwargs.values())
    
    if len(args) == 0:
        return
    
    list_length = len(args[0])
    
    for a in args:
        if len(a) != list_length:
            raise Exception('all lists must be the same length')
    
    if isinstance(filename, file):
        f = filename
    else:
        f = open(filename, 'wb')
    for i in xrange(list_length):
        row = []
        for a in args:
            row.append(str(a[i]))
        f.write(separator.join(row) + newline)
    if not isinstance(filename, file):
        f.close()


def periodic(freq):
    """Returns a callable object corresponding to periodic firing at a given frequency."""
    return noise.Periodic(freq)


def exponentially_decaying_noise(num=1, firing_rate=5, decay_rate=2, delay=0, max_value=1):
    """Return num sources of exponentially decaying noise.
    
    Parameters:
        num -- how many noise sources to create
        firing_rate -- firing rate in Hz (or function)
        decay_rate -- in ms
		delay -- period synapse stays elevated before decaying
		max_value -- the peak value of the artifical synapse
    """
    sim = _current_simulation()
    for i in range(num):
        sim._noise.append(noise.ExponentiallyDecayingNoise(sim, firing_rate, decay_rate, delay, max_value))
    return neuronGroup.NeuronGroup(range(num - len(sim._noise), -len(sim._noise), -1), sim)


def square_wave_noise(num=1, firing_rate=5, duration=1, max_value=1):
    """Return num sources of exponentially decaying noise.
    
    Parameters:
        num -- how many noise sources to create
        firing_rate -- firing rate in Hz (or function)
        duration -- in ms
		max_value -- the peak value of the artifical synapse
    """
    sim = _current_simulation()
    for i in range(num):
        sim._noise.append(noise.SquareWaveNoise(sim, firing_rate, duration, max_value))
    return neuronGroup.NeuronGroup(range(num - len(sim._noise), -len(sim._noise), -1), sim)


def _run_sim(data, return_data=False):
    """Helper function for run.
    
    Responsible for running and saving simulations.
    """
    global _snnet_thread_data
    active_temp_files = []
    sim = simulation.Simulation(data['integrator'], data['override'])
    sim.random.seed(data['run_id'])
    sim._temp_files = active_temp_files
    sim._suffix = data['suffix'] + '_' + repr(data['run_id'])
    sim._leave_temp_files = data['leave_temp_files']

    # store the sim in the current thread data
    set_simulation(sim)
    set_run_id(data['run_id'])
    _snnet_thread_data.forks = []
    
    # make a copy of the original override data
    override = copy.deepcopy(data['override'])
    _snnet_thread_data.override = override
    
    # run the simulation
    try:
        data['sim']()
    except:
        if data['do_cleanup'] and not data['leave_temp_files']:
            for f in active_temp_files:
                delete_file(f, num_attempts=1)
        raise
    
    output_dir = data['output_dir']
    if output_dir is not None:
        if hasattr(output_dir, '__call__'):
            output_dir = output_dir(data['override'])
        
        # create the directory if it does not already exist
        try:
            os.makedirs(output_dir)
        except:
            pass
        
        # save the simulation
        sim.save(output_dir, data['run_id'], data['save_type'])
    
    # do any post_run analysis, etc...
    post_run = data['post_run']
    if post_run is not None:
        result = sim_data.Full()
        result._initialize(sim, output_dir, data['run_id'])
        post_run(result, data['run_id'], override)
    
    if return_data:
        result = sim_data.Full()
        result._initialize(sim, output_dir, data['run_id'])
        return result


def run(sim, output_dir=None, params={}, sweep={}, integrator=default_integrator,
        num_times=1, save_type=full, num_parallel=1, num_threads=None,
        post_run=None,
        start_id=0, do_cleanup=True, temp_dir_handler=default_temp_dir_handler,
        leave_temp_files=False,
        continue_after_failure=False):
    """Run a series of simulations.
    
    Parameters:
        sim -- function defining the simulation protocol
        output_dir -- directory to store the results in (string, None, or
                      function)
        params -- a dict of parameters to override with values
        sweep -- a dict of arguments to sweep over
        integrator -- which integrator to use
        num_times -- how many times to run each parameter set
        save_type -- output file_handler
        num_parallel -- how many simulations to run simultaneously
        post_run -- a function to call after each simulation
        start_id -- the simulation id # to start at (use for going back and
                    running more)
        do_cleanup -- should we delete temp files when a simulation fails?
        temp_dir_handler -- function returning a string pointing to a directory
                            to use for storing temporary files
        leave_temp_files -- should temporary files be left in the local directory
                            after running a simulation
        continue_after_failure -- if integration fails, should we keep trying?
    
    Deprecated Parameters:
        num_threads -- use num_parallel instead
    
    Notes:
    
    If only one simulation is ran, returns a Full SimData object with the results.
    
    If output_dir is omitted or None, does not automatically save the results.
    Use _current_simulation().save(...) to save manually.
    
    If output_dir is a function (or callable class), it must accept a dict of 
    overridden parameters and return a string.
    
    The output_dir (or the result of calling that function) is created if it 
    does not already exist.
    
    post_run must be either None or a function of three variables:
    data, run_id, params. Here, params is a dict of overriden parameters (if any).
        
    sweep may contain multiple variables, in which case all possible 
    combinations will be tried. This can get large very quickly.
    e.g.
        run(simulation, out_dir, sweep = {'iapp': [1, 5, 10], 'gsyn': [.01, .02]})
    will run 3 * 2 = 6 simulations.
    """
    if num_threads is not None:
        if num_parallel != 1:
            raise Exception('Cannot specify both num_threads and num_parallel.'
                            ' Use only num_parallel.')
        warn(DeprecationWarning('num_threads is deprecated. Use num_parallel'
                                ' instead.'))
        num_parallel = num_threads

    # verify save type
    if isinstance(save_type, str):
        raise Exception('save_type must be a SimData subclass. Perhaps you meant %r without the quotes?' % save_type)

    integrator._temp_dir_hander = temp_dir_handler    
    
    sims_to_do = Queue.Queue()
       
    # define the simulations
    for run_id in range(start_id, start_id + num_times):
        if sweep != {}:
            if not isinstance(sweep, list):
                sweep = [sweep]
            for sweep_params in sweep:
                sim_pars = {'run_id': run_id, 'integrator': integrator,
                            'save_type': save_type, 'sim': sim, 'do_cleanup': do_cleanup,
                            'post_run': post_run, 'leave_temp_files': leave_temp_files}
                _define_simulations(sims_to_do, output_dir, sim_pars, sweep_params.keys(),
                                    sweep_params.values(), override=params)
        else:
            sims_to_do.put({'run_id': run_id, 'integrator': integrator,
                            'output_dir': output_dir, 'save_type': save_type,
                            'sim': sim, 'suffix': '', 'override': params,
                            'do_cleanup': do_cleanup, 'post_run': post_run,
                            'leave_temp_files': leave_temp_files})

    num_sims = sims_to_do.qsize()
    if num_sims > 1:
        parallel_run_over_queue(_run_sim, sims_to_do, num_parallel=num_parallel,
                                temp_dir=temp_dir_handler,
                                continue_after_failure=continue_after_failure)
    elif num_sims == 1:
        return _run_sim(sims_to_do.get(), True)




def do_sweep(sim, var, val, sweep_base_dir='sweep', integrator=default_integrator,
             num_times=1, save_type=full, num_parallel=1, num_threads=None,
             start_id=0, do_cleanup=True):
    """Deprecated. Pass a sweep argument to run instead.
    
    Perform a parameter sweep.
    
    Parameters:
        sim -- function defining the simulation protocol
        var -- the variable to sweep over
        val -- a list of values to test
        sweep_base_dir -- directory to use as the base of the sweeps
        integrator -- which integrator to use
        num_times -- how many times to run each parameter set
        save_type -- output file_handler
        num_parallel -- how many simulations to run simultaneously
        start_id -- the simulation id # to start at (use for going back and running more)
        do_cleanup -- should we delete temp files when a simulation fails?
    
    Deprecated Parameters:
    
        num_threads -- use num_parallel instead
    """
    if num_threads is not None:
        if num_parallel != 1:
            raise Exception('Cannot specify both num_threads and num_parallel. Use only num_parallel.')
        warn(DeprecationWarning('num_threads is deprecated. Use num_parallel instead.'))
        num_parallel = num_threads

    warn(DeprecationWarning('do_sweep is deprecated. Use the sweep argument of run instead.'))
    run(sim, sweep_base_dir, sweep={var: val}, integrator=integrator, num_times=num_times,
        save_type=save_type, num_parallel=num_parallel, start_id=start_id, do_cleanup=do_cleanup)


def _define_simulations(sims_to_do, output_dir, sim_pars={}, var=[],
                        val=[], override={}, suffix=''):
    """Helper function for sweeps in run.
    
    Recursively add simulations to the sims_to_do Queue.
    """
    if len(var) == 0:
        local_pars = dict(sim_pars)
        local_pars['override'] = override
        local_pars['suffix'] = suffix
        local_pars['output_dir'] = output_dir
        sims_to_do.put(local_pars)
        return
    for i, v in enumerate(val[0]):
        local_override = dict(override)
        local_override[var[0]] = v
        if isinstance(output_dir, str):
            local_output_dir = os.path.join(output_dir, var[0], '%g' % v)
        else:
            local_output_dir = output_dir
        _define_simulations(sims_to_do, local_output_dir, sim_pars, var[1 : ],
                            val[1 : ], local_override, suffix + '_%d' % i)
    



