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)