Before the observer pattern can be implemented, a basic understanding of inheritance is needed.
Inheritance is an integral part of object orientated design. It is primarily about modularity and reusability of code, thus it helps in keeping software projects cost and time effective.
Inheritance involves a parent class and a number of child classes. The parent class is an abstract implementation of an object, it may have some implemented methods, though some of its methods may not be implemented. The child classes ``inherit'' all the methods and attributes of the parent class. So, for example, we might have the following parent:
class Parent: def myMethod(self): . . .
And the following child:
class Child(Parent): . . .
The class Child(Parent):
statement says that Child
inherits
all the features of Parent
. We could call Child.myMethod()
without defining myMethod
in the
child class. Of course, this is a very simple example, we can define
more attributes and methods in the child class if we wanted.
Another aspect of inheritance is the concept of deferred classes and deferred methods. Deferred or abstract classes are classes that have not been fully implemented, and so cannot exist as an object themselves. Deferred methods are methods that have not been implemented, and so cannot be called. Both of them must be implemented by or in a child class.
Python has very loose support for deferred classes. In Java, a deferred class is defined as follows:
abstract class Parent { void doSomething() { . . . } abstract void doSomethingElse(); }
In this case, if you tried to create an object of class Parent
, an error
would be thrown. You could only create an object of a class that
inherits from Parent
. That class must implement the
doSomethingElse()
method, otherwise the program will not compile.
The doSomething()
can be redefined, but it does not have to be
implemented, and so can be called without any implementation in the
child class.
In Python, the above would be implemented like so:
class Parent: def doSomething(self): . . . def doSomethingElse(self): raise NotYetImplemented, "Attempt to call deferred method"
Unlike Java, Python will not enforce that Parent
be implemented by a
class. It will also not enforce the implementation of a
method. In the above example, we have raised an error should that
method be called. This is helpful to ensure that that method is always
implemented by a child class. However it is still possible to create an
object of class Parent
. It therefore important when using deferred
classes to understand this, so that we don't accidentally create objects
of classes that we want to be deferred.
In our implementation of the observer pattern, we have two subjects. The
parent subject class has five methods, __init__()
, detach()
,
attach()
, notify()
and run()
. run()
is deferred,
it must be there so that the controller can call it, but is implemented
differently in the simulator and minimiser. There are also six
attributes common to all subjects. observers
is inherit from the
observer pattern, and the rest are the interface to the
output displays. Because Python has no way of declaring variables,
in this implementation, they are initialised to empty or 0, so that we
can see that they are there.
<Subject.py>= class Subject: def __init__(self): self.observers = [] self.state = "" self.atoms = [] self.totalPotential = 0 self.totalKinetic = 0 self.totalEnergy = 0 self.nside = 0 self.rside = 0 self.nstep = 0 def detach(self, observer): try: self.observers.remove(observer) except ValueError: print "Observer not in observers list" raise def attach(self, observer): self.observers.append(observer) def notify(self): for observer in self.observers: observer.update() def run(): raise NotImplementedError, "Attempt to call deferred method"
This code is written to a file (or else not used).
First we'll implement the minimiser. This is a simple task of translating our previous code into the Subject
interface. We need to remember that things that need to be accessed by other classes, particularly the atoms list, need to be attributes of the class that we're working in, and hence need to be referred to as self.xxx
.
<Minimise.py>= from Subject import Subject import math class Minimise(Subject): def run(self): tol = 0.01 self.atoms = self.createSimpleCube(self.rside, self.nside) <Initialise Velocity> self.force = self.evalForce(self.atoms) self.totalPotential = self.evalPotential(self.atoms) self.totalEnergy = self.totalPotential self.state = "initialise" self.notify() self.state = "run" forcetol = 0.01 * len(self.atoms) totalForce = forcetol + 1 while totalForce > forcetol: alpha = self.goldenSectionSearch(self.atoms, self.force, tol) self.atoms = self.nextAtoms(self.atoms, alpha, self.force) oldPotential = self.totalPotential self.force = self.evalForce(self.atoms) tol = abs((self.totalPotential - oldPotential) / self.totalPotential) * 10 totalForce = 0 for f in self.force: totalForce = totalForce + f[0] ** 2 + f[1] ** 2 + f[2] ** 2 self.totalEnergy = self.totalPotential self.notify() print "The minimum potential is %e" % self.totalPotential <Evaluate Force> <Evaluate Potential> <Create Simple Cube> <Golden Section Search> <Next Atoms>
This code is written to a file (or else not used).
Now we implement the simulator. It may have been an idea put the
evalForce
and createSimpleCube
routines in the Subject
class. However, for now we are leaving it out because further down the
track we will have a better way of doing it.
<Simulate.py>= from Subject import Subject import math class Simulate(Subject): def __init__(self): Subject.__init__(self) self.tstep = 0.01 def run(self): self.atoms = self.createSimpleCube(self.rside, self.nside) <Initialise Velocity> newForce = oldForce = self.evalForce(self.atoms) self.totalEnergy = self.totalPotential + self.totalKinetic self.state = "initialise" self.notify() self.state = "run" for istep in range(self.nstep): <Update Coordinates> newForce = self.evalForce(self.atoms) <Update Velocity> oldForce = newForce self.totalEnergy = self.totalPotential + self.totalKinetic self.notify() <Evaluate Force> <Create Simple Cube>
This code is written to a file (or else not used).
Our implementation of the observer class has two methods and one
attribute. setSubject()
is used to set the subject, and
update()
is deferred, it's where most of the hard work will be done.
The attribute is the subject
.
<Observer.py>= class Observer: def setSubject(self, subject): if self.subject: self.subject.detach(self) self.subject = subject self.subject.attach(self) def update(self): raise NotImplmentedError, "Attempt to call deferred method"
This code is written to a file (or else not used).
When we call update on the visual python renderer, there are two states that we are interested in. The first one is initialise. When update is first called, no balls would have been created. Or, we may be running the simulation again but with different parameters, in which case we need to discard the old balls and create new ones. So in the initialise state, the renderer will discard all the old balls and create new ones according to the number of atoms in the subject.
The second state is run. When the simulation is running, we don't need to create new balls each time, we can just update the position of the current ones. We'll also colour them according to their kinetic energy.
<VPRenderer.py>= from Observer import Observer from visual import * class VPRenderer(Observer): def __init__(self): self.balls = [] scene.autoscale = 0 scene.title = "Molecular dynamics simulator" self.subject = None def update(self): assert self.subject if self.subject.state == "initialise": <Initialise VPRenderer> elif self.subject.state == "run": for i in range(len(self.balls)): self.balls[i].pos = self.subject.atoms[i] self.balls[i].color = color.hsv_to_rgb((0.8 * (self.subject.kinetic[i] + 1) ** -1 - 0.15, 1, 1))
This code is written to a file (or else not used).
<Initialise VPRenderer>= for i in range(len(self.balls)): self.balls[i].visible = 0 del self.balls self.balls = [] for atom in self.subject.atoms: self.balls.append(sphere(pos=atom, color=color.blue, radius=0.3))
Used above.
The controller has to get its input, then create the observer(s) and appropriate subject, then tell the observer(s) to observe that subject, and finally it tells the subject to run.
<Controller.py>= import VPRenderer import Simulate import Minimise import sys try: if sys.argv[1] == "simulate": Subject = Simulate.Simulate() Subject.tstep = float(sys.argv[4]) Subject.nstep = int(sys.argv[5]) elif sys.argv[1] == "minimise": Subject = Minimise.Minimise() else: raise ValueError r = float(sys.argv[2]) Subject.rside = (r, r, r) n = int(sys.argv[3]) Subject.nside = (n, n, n) except (ValueError, IndexError): print "Usage: python %s minimise|simulate rside nside [tstep nstep]" % sys.argv[0] sys.exit(1) VPRenderer = VPRenderer.VPRenderer() VPRenderer.setSubject(Subject) Subject.run()
This code is written to a file (or else not used).