Department of Physics and Astronomy

The Forbes Group

Python Attributes, Parameters, and Traits

$\newcommand{\vect}[1]{\mathbf{#1}} \newcommand{\uvect}[1]{\hat{#1}} \newcommand{\abs}[1]{\lvert#1\rvert} \newcommand{\norm}[1]{\lVert#1\rVert} \newcommand{\I}{\mathrm{i}} \newcommand{\ket}[1]{\left|#1\right\rangle} \newcommand{\bra}[1]{\left\langle#1\right|} \newcommand{\braket}[1]{\langle#1\rangle} \newcommand{\op}[1]{\mathbf{#1}} \newcommand{\mat}[1]{\mathbf{#1}} \newcommand{\d}{\mathrm{d}} \newcommand{\pdiff}[3][]{\frac{\partial^{#1} #2}{\partial {#3}^{#1}}} \newcommand{\diff}[3][]{\frac{\d^{#1} #2}{\d {#3}^{#1}}} \newcommand{\ddiff}[3][]{\frac{\delta^{#1} #2}{\delta {#3}^{#1}}} \DeclareMathOperator{\erf}{erf} \DeclareMathOperator{\Tr}{Tr} \DeclareMathOperator{\order}{O} \DeclareMathOperator{\diag}{diag} \DeclareMathOperator{\sgn}{sgn} \DeclareMathOperator{\sech}{sech} $

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

In [1]:
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
In [2]:
%pylab inline --no-import-all
s = Simulation(Experiment(), q0=(3.1, 0.0), T=100)
s.run()
s.plot()
Populating the interactive namespace from numpy and matplotlib
In [3]:
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
In [4]:
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
In [5]:
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
In [6]:
import ipywidgets
import paramnb
paramnb.Widgets(s.experiment)
Widget Javascript not detected.  It may not be installed or enabled properly.

Traitlets

In [169]:
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
In [170]:
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
s.experiment
Out[170]:
<__main__.ExperimentKapitza1 at 0x117ff56d0>

attrs

In [164]:
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
In [165]:
s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
s.run()
s.plot()
s.experiment
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-165-02d653608c86> in <module>()
----> 1 s = Simulation(ExperimentKapitza1(frequency=5.0), q0=(3.1, 0.0), T=10, Nt=500)
      2 s.run()
      3 s.plot()
      4 s.experiment

<attrs generated init 64d90c11aa40074c9d90c70e28d22c5f440321da> in __init__(self, amplitude, frequency)
      1 def __init__(self, amplitude=attr_dict['amplitude'].default, frequency=attr_dict['frequency'].default):
----> 2     self.amplitude = amplitude
      3     self.frequency = frequency

AttributeError: can't set attribute

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__ or repr() 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}
In [ ]: