Author: Donnal Walter
Revised: 2003-03-22

Subsections


Evolving an application

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.

Importing Mindwrapper

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).

Creating an empty 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()

Viewer for empty panel

The viewer is invoked by: (1) creating an instance of mw.Viewer with the class of the panel as an argument, and (2) calling the Run() method.

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.

Making sub-panel text boxes

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)

Making sub-panel text boxes

Because the notation at left is so simple, it is easy to miss its power. When self.Add() sees text='xx', it creates a StaticBox and StaticBoxSizer, and sets up the appropriate parent-child relationships automatically. Then it adds the whole apparatus to the parent horizontal box sixer with a margin of 5 pixels on all sides (equivalent to 'flag=wxAll, border=5').

What is demonstrated here is an effortless way of nesting panels and their sizers.

It should also be mentioned that if ActPanel and EstPanel were derived from mw.SubPanel instead of mw.Panel, the self.Add() method would create nested sizers instead of nested panels. This is more light-weight, but the improvement in performance may not be noticable.

Adding controls to a panel

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)

Adding controls to a panel

In the code above, the definition for ActPanel was just a stub. In this section, we will flesh out the definition.

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 style=mw.RIGHT obviously aligns the text to the right.

The presenter mw.NumberField wraps a custom wxTextCtrl that allows only numeric input. The parameter
margin=(0,4) is roughly equivalent to:
flag = wxLEFT | wxRIGHT, border = 4

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.

Including the frame and application

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()

Incuding the Frame and Application
Note that the parameter:
resize = (mw.FIXED, mw.GROWABLE)
is applied to the KineticsPanel as it is added to the built-in vertical sizer of the frame. It will have a fixed sized in the primary (vertical) orientation and will be allowed to grow in the secondary (horizontal) orientation.

Defining the abstraction layer

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:

  • root
    • timing
      • hrDosing
      • hrInf
      • hrPostInf
      • hrPreInf
      • hrPkTr
    • dose
    • peak
    • trough
    • weight
    • kElim
    • volDist
    • volDistPerKg
    • clearance
    • whatif
      • hrDosing
      • hrInf
      • hrPostInf
      • hrPreInf
      • dose
      • peak
      • trough

Key:

  • branch
  • independent cell
  • dependent cell

(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.)

Connecting the abstraction layer

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)
        .
        .
        .

Connecting to the abstraction layer

The presentation layer is connected to the abstraction layer through the ref parameter as shown at left. The Assemble() signature may provide a default compound Model (here the PKinetics branch) or it may be passed a model from the parent of ActPanel. Each component presenter can be sent a specific cell Model by passing it a ref parameter as well.

(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)

Adding yet another panel

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')

Adding yet another panel

One of the design goals of Mindwrapper is to make it easy to evolve an application incrementally.

Here we add another panel (PatientPanel) to the application. This panel will provide an additional independent variable (weight) and display four more dependent variables. The Labels and NumberFields (and the single SpinButton) placed in a FlexGridSizer created by:

    cols = 6
    growableRows = [1]
    growableCols = [0, 2, 3, 4, 5]

We could have added this PatientPanel directly to the Frame we created before, but since there is a margin around it, we need to put it (along with the previous KineticsPanel) in a container panel, here defined as WorkArea. This also allows us to pass the same instance of the PKinetics branch to both PatientPanel and KineticsPanel.

The presenter mw.TextReader wraps a read-only TextCtrl (but this term is subject to change).


Application tree:

  • KineticsApp
    • KineticsFrame
      • WorkArea
        • KineticsPanel
          • ActPanel
            • 16 components
          • EstPanel
            • 14 components
            • 2 spacers
        • PatientPanel
          • 6 components

Source Code:


SourceForge Logo
XHTML 1.0 © 2003 Donnal C. Walter