PScan.Scan
==========

A module whose sole purpose is to facilitate parameter scans via a
generic framework for specifying parameter values to scan.

Combinatorial parameter sweeps, joint parameter sweeps, "scientific"
parameter sweeps, and advanced options for how many times to repeat
simulations are available.

This module was written after the author got tired of writing the same
boiler plate code every time he wanted to sweep parameters in his
simulations.

The impetus for the design of the module was the realization that there
are basically only four types of "parameter sweeps" that ever really
need to be done.

1. run the same parameters many times (e.g. stoch simulation)
2. vary certain parameters jointly (e.g. (i,j) = (1,2), (2,3), (3,4),
   ... )
3. vary parameters combinatorially (e.g. (i,j) = (1,1), (1,2), (2,1),
   (2,2))
4. vary "scientifically" (e.g. (i0,j),(i,j0) for fixed i0,j0, varying
   i,j)

All three of these can be specified declaratively (e.g. in English
sentences) relatively easily, but usually each requires coding an
easy-to-goof up nested for loop. This module turns a simple listing of
which parameters into an iterable you can query to get the next set of
parameters in your scan. If a Scan is stopped midway, it will remember
its state, allowing you to load it back up, change the scan parameters,
and continue the run. A Scan is *not* useful if your code needs to
regularly generate the next parameters based on previous simulations.

If you have several "jointly" varying parameters, each "group" of them
will interact combinatorially. More abstractly, you should think of each
set of jointly varying parameters as being the same as a "single"
parameter.

Here's an example that uses all of the features of the package. Say we
have two parameters, ``a`` and ``b``, that we want to vary jointly, and
another parameter, ``c``, that we want to to vary for each value of
``(a,b)``. To further complicate things, we want to run the simulation 2
times for each set of ``(a,b,c)``, except that if 'c' is too big, we run
less repeats to save time. This would normally involve a lot of
carefully-coded boilerplate, but instead we can just tell ``PScan.Scan``
what we want:

::

    >>> import numpy as np
    >>> def f(a,b,c):
    >>>     print('a = ', a, ', b = ', b, ', c = c')
    >>> p['a'] = np.linspace(0, 10, 5)
    >>> p['b'] = np.linspace(0.1, -0.5, 5)
    >>> p['c'] = np.linspace(10, 20, 3)
    >>> default_count = lambda p: 2
    >>> # None specifies to default to previous value
    >>> big_c_count = lambda p: 1 if p['c'] >= 14 else None
    >>>
    >>> s = Scan.from_dict(p, joint_lists=[['a','b']])
    >>> s.add_count(default_count) # these will be called
    >>> s.add_count(big_c_count) # in the order they were added
    >>> s.run_scan(f) # verify scan validity and run

        0.0 0.1 10.0
        0.0 0.1 10.0
        2.5 -0.05 10.0
        2.5 -0.05 10.0
        5.0 -0.2 10.0
        5.0 -0.2 10.0
        7.5 -0.35 10.0
        7.5 -0.35 10.0
        10.0 -0.5 10.0
        10.0 -0.5 10.0
        0.0 0.1 15.0
        2.5 -0.05 15.0
        5.0 -0.2 15.0
        7.5 -0.35 15.0
        10.0 -0.5 15.0
        0.0 0.1 20.0
        2.5 -0.05 20.0
        5.0 -0.2 20.0
        7.5 -0.35 20.0
        10.0 -0.5 20.0
