Author: Donnal Walter
Revised: 2003-03-22
Subsections
Until such time as a proper tutorial can be written, this description of a small proof-of-concept project will serve as an introduction to the use of Mindwrapper. The objective of this mini-tutorial is to create a limited but workable pharmacokinetics calculator for newborn intensive care. The simple program we build here will become part of a more comprehensive drug management program, which will eventually become one component of a large clinical system.
Mindwrapper is an experimental framework for the evolution of custom clinical applications. The primary goal is to make it possible to work on such applications from the top down, or from the bottom up, or from any point that captures the developer's interest, allowing one to see results quickly and involve end-users early.
Mindwrapper applications are built in two layers. One may start with the abstraction layer (the domain data model), or one may choose to begin with the presentation layer (the user-interface). The latter is the approach we will take in this tutorial.
Assuming that Python 2.2 and wxPython 2.4 are already present, Mindwrapper 0.1 should be downloaded, unzipped and installed with the "python setup.py install" command. The Mindwrapper API is accessed by importing the Mindwrapper.api module using one of the following statements:
from Mindwrapper import api from Mindwrapper import api as mw # the statement used below from Mindwrapper.api import *
Although the Mindwrapper API is small, use of the "import *" statement is generally discouraged. In this document we use the second of these alternatives, so all Mindwrapper key words are prefixed with "mw." (e.g., mw.Panel).
Although it is quite easy with Mindwrapper to set up an application and top-level frame (see below), it will often be more convenient to use the built-in panel-viewer, which is how we start out here. We will define a class KineticsPanel, and use the viewer to "run" it. An explanation of the Mindwrapper notation follows the example.
from Mindwrapper import api as mw
class KineticsPanel(mw.Panel):
def Assemble(self):
self.Add(node = mw.Blank,
size = (240, 80))
viewer = mw.Viewer(KineticsPanel)
viewer.Run()
|
|
The first line above imports the Mindwrapper API as 'mw'. The class KineticsPanel is derived, then, from mw.Panel, which is a composite Presenter that wraps a wxPython Panel as its View. This presenter also takes care of component layout and setting up the interface to a Model within the abstraction layer. This is the so-called Model-View-Presenter (MVP) architecture.
A custom presenter class (such as KineticsPanel) is defined by subclassing the the appropriate Mindwrapper class (in this case, mw.Panel), overriding its Assemble() method. This kind of inheritance with polymorphism is ubiquitous in the Mindwrapper notation. Each component is added to the panel by calling self.Add() method with appropriate parameters. The most important parameter is node, which is the class of the component to be added. Notice that it is the class, not an instance of the class, that is passed to the method. This allows preprocessing of other parameters to occur before the component is instantiated. The component automatically becomes a child of the presenter/view being defined (i.e. parent=self), and the component is automatically added to the parent's sizer using parameters sent to the self.Add() method. In the code above, default values for these parameters are used, making it easy to get started.
The mw.Blank class is a presenter that merely wraps a wxWindow with a simple border. It is only a placeholder, but like any window, its size can be set to some arbitrary minimum. It should be emphasized that this component is automatically added to a BoxSizer with a vertical orientation by the self.Add() method.
The next step is to divide the panel into two sections, visibly and logically, by removing the mw.Blank component and replacing it with two custom mw.Panel components. Let's say that we want the subpanels to be side-by-side rather than vertically oriented. Assigning rows=1 prior to the def Assemble() definition changes the orientation of the built-in sizer to horizontal.
class KineticsPanel(mw.Panel):
rows = 1
def Assemble(self):
self.Add(node = ActPanel,
name = 'panel1',
text = 'actual',
margin = 5)
self.Add(node = EstPanel,
name = 'panel2',
text = 'estimated',
margin = 5)
class ActPanel(mw.Panel):
def Assemble(self):
self.Add(node = mw.Blank,
size = (100,50))
class EstPanel(mw.Panel):
def Assemble(self):
self.Add(node = mw.Blank)
|
What is demonstrated here is an effortless way of nesting panels and their sizers. It should also be mentioned that if |
class ActPanel(mw.Panel):
cols = 4
growableCols = [1]
def Assemble(self):
self.Add(node = mw.Label,
text = 'Dose',
style = mw.RIGHT)
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4))
self.Add(node = mw.Label,
text = 'mg')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL)
self.Add(node = mw.Label,
text = 'Interval')
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4))
self.Add(node = mw.Label,
text = 'hr')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL)
self.Add(node = mw.Label,
text = 'Peak',
style = mw.RIGHT)
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4))
self.Add(node = mw.Label,
text = 'mcg/ml')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL)
self.Add(node = mw.Label,
text = 'Trough')
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4))
self.Add(node = mw.Label,
text = 'mcg/ml')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL)
|
At the outset we know that a simple box sizer will not suffice. So we change it to a GridSizer with the following assignment:
cols = 4
But what we really want is a FlexGridSizer, so we specify which columns we want to be growable. That's all.
growableCols = [1]
Now all we have to do is add the components one at a time. Each component becomes a child of the ActPanel presenter and each one is also added to its FlexGridSizer. (Technical note: If we had derived from mw.SubPanel rather than mw.Panel, each component would become a child of KineticsPanel instead of ActPanel, but the sizers would still be nested appropriately.) The presenter mw.Label wraps a wxStaticText view, and The presenter mw.NumberField wraps a custom wxTextCtrl that allows only numeric input. The parameter We have not yet connected the presenters to models in the abstraction layer, but when we do, the range of the SpinButton is automatically set to the limits constraint of the associated Number cell. |
As we mentioned at the outset, it is convenient to be able to work on a Panel presenter and view it with the built-in viewer without worrying about setting up an application and frame with which to test it. Eventually, however, the Panel will become part of an application. The application itself may read a configuration file, display a splash screen, require user authentication, and so on before loading its top frame. The top frame may have a menubar, toolbar and statusbar and so on. We will not discuss these options in this mini-tutorial.
The simplest definition of an application is something like the code below. The custom application is derived from mw.Application and the custom frame presenter is derived from mw.Frame. The frame title is specified with the text parameter when adding it to the the application. (There are also other ways of setting the frame title not discussed here.)
The definition for EstPanel (not shown, but see top.py) is similar to that of ActPanel except that the Peak and Trough NumberFields are read-only (because the variables will be dependent) and they therefore do not have associated SpinButtons.
class KineticsFrame(mw.Frame):
def Assemble(self):
self.Add(node = KineticsPanel,
resize = (mw.FIXED,
mw.GROWABLE))
class KineticsApp(mw.Application):
def Assemble(self):
self.Add(node = KineticsFrame,
text = 'Pharmacokinetics')
app = KineticsApp()
app.Run()
|
|
At this point we have an attractive user-interface; how do we make it do something useful? The next step is to define the abstraction layer. Please note, however, that we could have started with the abstraction layer first. It is possible to define (and test) the abstraction layer with no knowlege of (or thought given to) what the user-interface will look like. One can then modify the user interface (presentation layer) or create multiple user interfaces without changing the abstraction layer.
The abstraction layer is a tree graph that is defined using a notation quite similar to that for the presentation layer. The custom tree is derived from the Mindwrapper mw.Branch class, overriding the , calling the self.Add() method. In this case, however, the node parameter is either another user-defined branch class (such as Timing) or a cell class (such as mw.Number).
The other parameters of the self.Add() method either bind the node to a name (in the labeled graph) or provide instance constraints such as scale, digits, and limits. For custom-defined cells, the same constraints may be declared by class. If so, instance declarations override the class declarations.
A cell node may be one of the built-in classes, or it may be user-defined (such as KElim below). The primary reason to custom define a cell class is to make it a dependent cell which calculates its own value from the values of cells that it observes. In this case, the cell's Assemble() method is overridden to return the value of some appropriate computation. When such a cell is instantiated with the self.Add() method, the input cells are supplied as a list via the ref parameter. The self.Add() method takes care of registering the dependent cell as an observer of all the necessary independent (observable) cells.
Abstraction layer tree graph |
Class definitions for dependent cells | |
# branches.py class PKinetics(mw.Branch): def Assemble(self): self.Add( node = Timing, name = 'time') self.Add( node = mw.Number, name = 'dose', scale = 0.01, digits = 2) self.Add( node = mw.Number, name = 'peak', scale = 0.01, digits = 2) self.Add( node = mw.Number, name = 'trough', scale = 0.01, digits = 2) self.Add( node = mw.Number, name = 'weight', scale = 0.001, digits = 3) self.Add( node = KElim, name = 'kElim', ref = [ self.peak, self.trough, self.time.hrPkTr]) self.Add( node = VolDist, name = 'volDist', ref = [ self.kElim, self.dose, self.peak, self.time.hrDosing, self.time.hrInf, self.time.hrPostInf]) self.Add( node = mw.Quotient, name = 'volDistPerKg', digits = 3, ref = [ self.volDist, self.weight]) self.Add( node = mw.Product, name = 'clearance', digits = 3, ref = [ self.volDistPerKg, self.kElim]) self.Add( node = Whatif, name = 'whatif', ref = self) # 'Timing' & 'Whatif' definitions not shown |
# formulae.py from math import log, exp from Mindwrapper import api as mw class KElim(mw.Number): scale = 0 digits = 3 def Assemble(self, pk, tr, hr): return (log(pk) - log(tr)) / hr class VolDist(mw.Number): scale = 0.0001 digits = 4 def Assemble(self, kE, dose, cPk, tDose, tInf, tPost): d = kE * tInf a = exp(-d) b = exp(-kE * tPost) c = exp(-kE * tDose) x = ((1 - a) * b) / ((1 - c) * d) vol = dose / cPk return vol * x Data-object tree:
|
(Admittedly, this treatment of the abstraction layer is cursory, at best. I have started work on a more definitive document, but as of 2003-03-22 it is incomplete as well. For the curious, however, part of a rough draft may be found here.)
from branches import PKinetics
class ActPanel(mw.Panel):
cols = 4
growableCols = [1]
def Assemble(self, ref=PKinetics()):
self.Add(node = mw.Label,
text = 'Dose',
style = mw.RIGHT)
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4),
ref = ref.dose)
self.Add(node = mw.Label,
text = 'mg')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL,
ref = ref.dose)
self.Add(node = mw.Label,
text = 'Interval')
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4),
ref = ref.time.hrDosing)
self.Add(node = mw.Label,
text = 'hr')
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL,
ref = ref.time.hrDosing)
.
.
.
|
The presentation layer is connected to the abstraction layer through the ref parameter as shown at left. The (Notice, for example, that the first NumberField and the first SpinButton are both connected to the same Number cell named PKinetics.dose.) Here again, everything is transparent. The presenters are automatically made observers of the specified cells. Event binding is automatic as well, so that clicking on a SpinButton changes both the underlying Number cell and the associated NumberField. This is what makes the Model-View-Presenter architecure so powerful. Maintaining both the abstraction layer and the presentation layer is uncomplicated by hard-coded connections. (For full source, see top.py) |
class PatientPanel(mw.Panel):
cols = 6
growableRows = [1]
growableCols = [0, 2, 3, 4, 5]
def Assemble(self, ref):
self.Add(node = mw.Label,
style = mw.CENTER,
text = 'Weight')
self.Add(node = mw.Spacer)
self.Add(node = mw.Label,
style = mw.CENTER,
text = 'kElim')
self.Add(node = mw.Label,
style = mw.CENTER,
text = 'vDist')
self.Add(node = mw.Label,
style = mw.CENTER,
text = 'vD/kg')
self.Add(node = mw.Label,
style = mw.CENTER,
text = 'clearance')
self.Add(node = mw.NumberField,
size = (50, -1),
margin = (0, 4),
ref = ref.weight)
self.Add(node = mw.SpinButton,
margin = (0, 4),
style = mw.HORIZONTAL,
ref = ref.weight)
self.Add(node = mw.TextReader,
size = (50, -1),
margin = (0, 4),
ref = ref.kElim)
self.Add(node = mw.TextReader,
size = (50, -1),
margin = (0, 4),
ref = ref.volDist)
self.Add(node = mw.TextReader,
size = (50, -1),
margin = (0, 4),
ref = ref.volDistPerKg)
self.Add(node = mw.TextReader,
size = (50, -1),
margin = (0, 4),
ref = ref.clearance)
class WorkArea(mw.Panel):
def Assemble(self, ref=PKinetics()):
self.Add(node = KineticsPanel,
resize = (mw.FIXED,
mw.GROWABLE),
ref = ref)
self.Add(node = PatientPanel,
text = 'Patient',
margin = (2,4),
resize = (mw.FIXED,
mw.GROWABLE),
ref = ref)
class KineticsFrame(mw.Frame):
def Assemble(self):
self.Add(node = WorkArea)
class KineticsApp(mw.Application):
def Assemble(self):
self.Add(node = KineticsFrame,
text = 'Pharmacokinetics')
|
One of the design goals of Mindwrapper is to make it easy to evolve an application incrementally. Here we add another panel (
cols = 6
growableRows = [1]
growableCols = [0, 2, 3, 4, 5]
We could have added this The presenter Application tree:
Source Code: |