Making classes in python can be a bit of a pain due to the need to define various methods like __init__()
, __repr__()
, etc. to get reasonable behavior. In this post we discuss some alternatives for specifying attributes in python classes.
Use Case¶
The use case I will demonstrate is controlling a set of simulations corresponding to experiments. A series of Experiment
classes will be used to provide an interface for the simulations, documenting and translating various experimental parameters into the values needed for the simulation. As an example, we will simulate a pendulum of length $l$ and mass $m$ in a constant gravitation field with acceleration $g$. The experiments will control the location of the pivot point $[x_0(t), y_0(t)]$. In the simulation, we shall use units where $l=m=g=1$.
As a coordinate, we introduce the angle $\theta(t)$ increasing anti-clockwise with $\theta=0$ pointing down in the direction of the force of gravity, which satisfies the following ODE:
$$ \ddot{\theta} = - \frac{g+\ddot{y}_0}{l}\sin\theta - \frac{\ddot{x}_0}{l}\cos\theta. $$Pure Python¶
import math
from scipy.integrate import odeint
import numpy as np
class Simulation(object):
def __init__(self, experiment, q0=(0.0, 0.0), T=10.0, Nt=100):
self.experiment = experiment
self.q0 = q0
self.T = T
self.Nt = Nt
def run(self):
"""Run the simulation and store the results."""
self.ts = np.linspace(0, self.T, self.Nt)
self.thetas, self.dthetas = odeint(self._rhs, self.q0, self.ts).T
def _rhs(self, q, t):
"""RHS of the ODE.
Parameters
----------
q : (theta, dtheta)
Current coordinate and velocity describing the pendulum,
t : float
Current time.
"""
theta, dtheta = q
a_x, a_y = self.experiment.acceleration(t)
ddtheta = -(1.0 + a_y)*math.sin(theta) - a_x*math.cos(theta)
return (dtheta, ddtheta)
def plot(self):
"""Plot the results."""
plt.plot(self.ts, self.thetas)
class Experiment(object):
def __init__(self, **kw):
self.__dict__.update(kw)
def acceleration(self, t):
"""Return `(a_x, a_y)`, the the acceleration of the pivot."""
return (0.0, 0.0)
class ExperimentKapitza(Experiment):
amplitude = 100.0
frequency = 5.0
def acceleration(self, t):
"""Return `(a_x, a_y)`, the the acceleration of the pivot."""
return (0, self.amplitude * math.sin(2*math.pi * self.frequency*t))
class ExperimentKapitza1(ExperimentKapitza):
y_amplitude = 0.1
frequency = 5.0
@property
def amplitude(self):
w = 2*math.pi * self.frequency
return self.y_amplitude * w**2
%pylab inline --no-import-all
s = Simulation(Experiment(), q0=(3.1, 0.0), T=100)
s.run()
s.plot()
s = Simulation(ExperimentKapitza1(frequency=5.0, y_amplitude=0.1), q0=(3.1, 0.0), T=10,
Nt=500)
s.run()
s.plot()
param
¶
conda install -c ioam param paramnb widgetsnbextension
import param
class ExperimentKapitza(param.Parameterized):
amplitude = param.Number(100.0, doc="Ampltitude of force")
frequency = param.Number(5.0, doc="Frequency (Hz) of force")
def acceleration(self, t):
"""Return `(a_x, a_y)`, the the acceleration of the pivot."""
return (0, self.amplitude * math.sin(2*math.pi * self.frequency*t))
class ExperimentKapitza1(ExperimentKapitza):
y_amplitude = param.Number(0.1, doc="Ampltitude of displacement")
@property
def amplitude(self):
w = 2*math.pi * self.frequency
return self.y_amplitude * w**2
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
import ipywidgets
import paramnb
paramnb.Widgets(s.experiment)
Traitlets
¶
import traitlets
class ExperimentKapitza(traitlets.HasTraits):
amplitude = traitlets.Float(100.0, doc="Ampltitude of force")
frequency = traitlets.Float(5.0, doc="Frequency (Hz) of force")
def acceleration(self, t):
"""Return `(a_x, a_y)`, the the acceleration of the pivot."""
return (0, self.amplitude * math.sin(2*math.pi * self.frequency*t))
class ExperimentKapitza1(ExperimentKapitza):
y_amplitude = traitlets.Float(0.1, doc="Ampltitude of displacement")
@property
def amplitude(self):
w = 2*math.pi * self.frequency
return self.y_amplitude * w**2
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
s.experiment
attrs
¶
import attr
@attr.s
class ExperimentKapitza(object):
amplitude = attr.ib(100.0)
frequency = attr.ib(5.0)
def acceleration(self, t):
"""Return `(a_x, a_y)`, the the acceleration of the pivot."""
return (0, self.amplitude * math.sin(2*math.pi * self.frequency*t))
class ExperimentKapitza1(ExperimentKapitza):
y_amplitude = attr.ib(0.1)
@property
def amplitude(self):
w = 2*math.pi * self.frequency
return self.y_amplitude * w**2
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
s.experiment
properties
¶
Summary¶
param
¶
Pros:
- Nice generated class
__doc__
and__repr__()
. - Objects given unique names.
- Used by
holoviews
.
Cons:
- Sparse documentation.
- Don't know how to hide inherited parameter.
Traitlets¶
Pros:
- Used by IPython/Jupyter.
Cons:
- No
__doc__
orrepr()
generation.
Notes¶
The dynamics may be derived from the following Lagrangian:
\begin{align} [x, y] &= [x_0 + l\sin\theta, y_0 - l\cos\theta], \\ L[\theta, \dot{\theta}] &= \frac{m}{2}\left( (\dot{x}_0 + l\dot{\theta}\cos\theta )^2 + (\dot{y}_0 + l\dot{\theta}\sin\theta)^2 \right) - mg(y_0 - l\cos\theta) \\ \ddot{\theta} &= - \frac{g+\ddot{y}_0}{l}\sin\theta - \frac{\ddot{x}_0}{l}\cos\theta \end{align}