Tutorial

>>> import os
>>> import numpy
>>> from compapp import Computer
>>> class Sine(Computer):
...     steps = 100
...     freq = 50.0
...     phase = 0.0
...
...     def run(self):
...         ts = numpy.arange(self.steps) / self.freq + self.phase
...         self.results.xs = numpy.sin(2 * numpy.pi * ts)

Call execute (not run) to run the computation:

>>> app = Sine()
>>> app.execute()
>>> app.results.xs
array([...])

Any attributes assigned to results are going to be saved in datastore.dir if it is specified:

>>> app = Sine()
>>> app.datastore.dir = 'out'
>>> app.execute()
>>> npz = numpy.load('out/results.npz')
>>> numpy.testing.assert_equal(app.results.xs, npz['xs'])

You can also pass (nested) dictionary to the class:

>>> app = Sine({'datastore': {'dir': 'another-dir'}})
>>> app.datastore.dir
'another-dir'

Plotting

The figure attribute of Computer is a simple wrapper of matplotlib.pyplot.

>>> class MyApp(Computer):
...     sine = Sine
...
...     def run(self):
...         self.sine.execute()
...         _, ax = self.figure.subplots()  # calls pyplot.subplots()
...         ax.plot(self.sine.results.xs)
>>> app = MyApp()
>>> app.datastore.dir = 'out'
>>> app.execute()

The plot is automatically saved to a file in the datastore directory:

>>> os.path.isfile('out/figure-0.png')
True

In interactive environments, the figures are also shown via default matplotlib backend (e.g., as inline figures in Jupyter/IPython notebook), provided that setup_interactive is called first.

Composition of computations

Since MyApp is built on top of Sine, the result of Sine is also saved in the datastore of MyApp.

>>> os.path.isfile('out/sine/results.npz')
True

The parameter passed to the root class is passed to nested class:

>>> app = MyApp({'sine': {'phase': 0.5}})
>>> app.sine.phase
0.5

Decomposing parameters and computations in reusable building blocks makes code simple.

For example, suppose you want to try many combinations of frequencies and phases. You can use numpy.linspace for this purpose. Naive implementation would be like this:

class NaiveMultiSine(Computer):
    steps = 100

    freq_start = 10.0
    freq_stop = 100.0
    freq_num = 50

    phase_start = 0.0
    phase_stop = 1.0
    phase_num = 50

    def run(self):
        freqs = numpy.linspace(
            self.freq_start, self.freq_stop, self.freq_num)
        phases = numpy.linspace(
            self.phase_start, self.phase_stop, self.phase_num)
        ...

A better way is to use Parametric and make a composable part:

>>> from compapp import Parametric
>>> class LinearSpace(Parametric):
...     start = 0.0
...     stop = 1.0
...     num = 50
...
...     @property
...     def array(self):
...         return numpy.linspace(self.start, self.stop, self.num)

Then LinearSpace can be used as attributes:

>>> class MultiSine(Computer):
...     steps = 100
...     phases = LinearSpace
...
...     class freqs(LinearSpace):  # subclass to change default start/stop
...         start = 10.0
...         stop = 100.0
...
...     def run(self):
...         freqs = self.freqs.array
...         phases = self.phases.array
...
...         ts = numpy.arange(self.steps)
...         xs = numpy.zeros((len(freqs), len(phases), self.steps))
...         for i, f in enumerate(freqs):
...             for j, p in enumerate(phases):
...                 xs[i, j] = numpy.sin(2 * numpy.pi * (ts / f + p))
...         self.results.xs = xs
...
>>> app = MultiSine()
>>> app.freqs.num = 10
>>> app.phases.num = 20
>>> app.execute()
>>> app.results.xs.shape
(10, 20, 100)

Dynamic loading

You can switch a part of computation at execution time:

>>> class Cosine(Sine):
...     def run(self):
...         ts = numpy.arange(self.steps) / self.freq + self.phase
...         self.results.xs = numpy.cos(2 * numpy.pi * ts)
>>> from compapp import dynamic_class
>>> class MyApp2(Computer):
...     signal, signal_class = dynamic_class('.Sine', __name__)
...
...     def run(self):
...         self.signal.execute()
...         _, ax = self.figure.subplots()
...         ax.plot(self.signal.results.xs)
...
>>> assert isinstance(MyApp2().signal, Sine)
>>> assert isinstance(MyApp2({'signal_class': '.Cosine'}).signal, Cosine)

Trying out multiple parameters (in parallel)

To vary parameters of a computation, you can use the CLI bundled with compapp:

capp mrun DOTTED.PATH.TO.A.CLASS -- \
    '--builder.ranges["PATH.TO.A.PARAM"]:leval=(START,[ STOP[, STEP]])' \
    '--builder.linspaces["PATH.TO.A.PARAM"]:leval=(START,[ STOP[, STEP]])' \
    '--builder.logspaces["PATH.TO.A.PARAM"]:leval=(START,[ STOP[, STEP]])' \
    ...

You can also use the same functionality in Python code:

>>> from compapp import Variator
>>> class MyVariator(Variator):
...     base, classpath = dynamic_class('.MyApp', __name__)
...
...     class builder:
...         linspaces = {
...             'sine.freq': (10.0, 100.0, 50),
...             'sine.phase': (0.0, 1.0, 50),
...         }
...
>>> app = MyVariator()
>>> app.builder.linspaces['sine.freq'] = (10.0, 100.0, 3)  # num = 3
>>> app.builder.linspaces['sine.phase'] = (0.0, 1.0, 2)  # num = 2
>>> app.execute()
>>> len(app.variants)  # = 3 * 2
6
>>> assert isinstance(app.variants[0], MyApp)