Author: Donnal Walter
Revised: 2003-03-02
Warning:As of this writing, Mindwrapper is still in the pre-alpha stage of development. The API as described here is therefore subject to change.
Subsections
Mindwrapper is an experimental framework for developing custom clinical applications.
Framework architecture. Mindwrapper applications may be characterized as single-tiered and bi-layered, by which we mean that they are partitioned into two distinct, cohesive layers. This section of the documentation describes the abstraction layer (AL). The presentation layer (PL) will be discussed separately. (See also Architecture.)
Framework information model. Mindwrapper applications use structure data documents best described as directed, acyclic, labeled, elaborately-typed graphs. For brevity, we will call such graphs object-trees. The terms domain model, data document, and object-tree are thus synonymous here. The user-developer composes the object-tree by means of a special notation for component naming, and component binding.
An object-tree consists of branching nodes and leaf nodes. Branching nodes are called a branches, appropriately, but leaf nodes are here called cells by analogy both to biological cells and to cells in a spreadsheet. (Biological cells superbly demonstrate encapsulation, instantiation, inheritance, polymorphism and many other object-oriented features.) Complex top-level branches consist of simpler sub-branches, which may consist of still simpler branches nested to any degree of complexity. Ultimately the simplest branches consist of cells. Cells are the atomic or scalar nodes in the object-tree. Branches and cells that make no reference to other branches or cells in the tree are said to be independent. Branches or cells that require access to external branches or cells are designated as dependent.
Framework notation. The Python-based notation is designed to facilitate the definition of the object-tree using a generic text editor. The notation is machine readible, that is to say, executable, as well as readily understood by humans. It is also designed to be a kind of resource-file notation for future experiments with special purpose editors. The notation consists of Python classes (some abstract, some concrete), methods (some to be called, some to be overridden), and keyword parameters for the Add() method. Inheritance and polymorphism are used extensively.
The object-tree for an application is made up independent and dependent nodes. An independent node is one that is completely encapsulated; i.e., it has no external references. The structure of an independent branch node is determined (defined) at design-time using the following syntax:
class PatientName(Branch): # subclass derived from abstract Branch
def Assemble(self): # abstract method overridden
self.Add( # concrete method called
node = Text, # node is instance of Text class
name = 'last') # the last name of the patient
self.Add( # concrete method called
node = Text, # another instance of Text class
name = 'first') # the first name of the patient
Every branch is derived from the abstract Branch class, with the Assemble() method overridden to specify the child nodes to be added to this parent node. Each child node is added by calling the self.Add() method with at least a node parameter to specify the class of node to be added. There will usually also be a name parameter to label the node. It is not necessary to put each parameter on a separate line, but (a) with many paramters, this form is possibly more readable, (b) machine generated code will probably use this form, and (c) this form would allow comments following each parameter. Nevertheless, the above could just as well have been written as the example on the left below. For those already familiar with Python, this is roughly equivalent to the traditional Python code on the right.
# Mindwrapper syntax
class PatientName(Branch):
def Assemble(self):
self.Add(node=Text, name='last')
self.Add(node=Text, name='first')
|
# Roughly equivalent Python
class PatientName(Branch):
def __init__(self):
self.last = Text()
self.first = Text()
|
The main difference is that the Mindwrapper Assemble/Add syntax also takes care of passing hidden information from the parent node to each child node.
What sets the independent branches apart from dependent branches (described below) is that the Assemble() method has only the self parameter and no others. This means that the independent branch cannot reference any nodes (branches or cells) outside of itself.
The leaf nodes of an object-tree are called cells. As mentioned in the overview, cells demonstrate encapsulation, inheritance and polymorphism much as biological cells do. In addition, they function like cells in a spreadsheet, except that they are labeled with names in a namespace hierarchy rather than by coordinates on a grid.
Three basic cell types are built into Mindwrapper: Text, Number, and Timestamp. Extending Mindwrapper to create new basic cells types is not difficult, but the need for such extensions is expected to be infrequent. On the other hand, defining specialized versions of the basic cell types (a process called elaboration by instance or by class) will be quite common in composing a Mindwrapper application.
Regardless of type, all cells have a number of features in common. Independent cells stand alone; they do not reference any other data objects. This means that when they are instantiated with the Add() method, no ref parameter is provided, and when subclasses are defined (class elaboration), the Assemble() method is not overridden.
All cells are observable. When the value is changed the cell notifies all objects that have registered themselves as observers of that cell. Observers may be other (dependent) cells in the AL or they may be presenters in the PL. Registration as an observer takes place automatically (at a high level) when a dependent cell or presenter is created.
The value of a Text cell is a Python string. Typically the value is set or changed by a presenter in the PL or by an observable in the AL. The user-developer will rarely have reason to access the value of the Text cell except through these built-in mechanisms, but when necessary, the Python property text provides such access.
tempDx = self.problem.diagnosis.text
self.currentDx.text = tempDx
The constraints for Text cells include choices and more, as well as the generic constraints (see below). The choices contraint creates a list of options suggested for a Text cell. The more constraint simply adds to that list. The choices are used by associated presenters in the PL but not enforced by the cell itself.
choices (set list of choices)more (add to choices)text
The constraints for a Number cell include scale, digits, and limits. The value of a Number cell is a floating-point type if the scale is set to 0 and an integer if the scale is 1. When the scale is between 0.00001 and 1000 (except for 1), the value is a scaled integer (fixed-point decimal) number. The digits constraint determines the number of decimal digits to the right of the decimal place when the number is converted to a text string. The limits constraint provides a range of acceptable values. This range can be nested one level with the first pair of limits being the outer range and the second set the inner range. Typically the inner range would be used for 'normal' values, while the outer range would be used for 'feasible' values. The limits are used by associated presenters in the PL but not enforced by the cell itself.
As with Text cells, it is usually unnecessary to access value of a Number cell except through the built-in observerable mechanisms, but should direct access be necessary, the val property can be used (or the text property for string representation).
scaledigitslimitstext
val
...
text ("yyyy-mm-dd hh:mm")
date ("yyyy-mm-dd")
time ("hh:mm")
val (seconds since epoch)
...
def Assemble(self):
self.Add(
node = Number,
name = myWeight
scale = 0.001,
digits = 3,
limits = [0.3, 5.0, 0.45, 10.0])
Not only is is possible to set constraints on invidual cells, it is also possible to define constraints for classes of cells.
class Weight(Number):
scale = 0.001
digits = 3
limits = [0.3, 5.0, 0.45, 10.0]
...
class VolDist(Number):
"""Volume of distribution for multiple dosing."""
scale = 0 # value will be floating-point
digits = 2 # show two decimal digits
def Assemble(self, kE, dose, cPk, tDose, tInf, tPost):
"""(elimination constant, dose, peak concentration, dosing interval,
infusion time, post-infusion time to peak)"""
d = kE * tInf
a = exp(-d)
b = exp(-kE * tPost)
c = exp(-kE * tDose)
x = ((1 - a) * b) / ((1 - c) * d) # adjustment for timing
vol = dose / cPk # volume of distribution
return vol * x # adjusted volume of distribution
...
self.Add(
node = VolDist,
name = 'volDist',
ref = [
self.kElim,
self.dose,
self.peak,
self.time.hrDosing,
self.time.hrInf,
self.time.hrPostInf])
A branch with one or more cells that make reference to cells external to that branch is said to be dependent.
def Assemble(self, aBranch):
self.Add(
node = Number,
name = 'aResult',
ref = [aBranch.inpA, aBranch.inpB])
def Assemble(self, cellA, cellB):
self.Add(
node = Number,
name = 'aResult',
ref = [cellA, cellB])
...
...
def Assemble(self):
self.AddList(
node = MedicationOrder,
name = 'myMedList')
...
def Assemble(self):
self.AddList(
node = MedicationOrder,
name = 'myMedList',
ref = [self.weight])
...
...
def Assemble(self, pt):
self.AddCopy(
node = Number,
name = 'currentWt',
ref = [pt.weight])
Mindwrapper applications are typically data-centric without being data-intensive. In other words they require a mechanism for storing and retrieving data, but not access to huge data sets or large numbers of records at a given time. The unit of data persistence in Mindwrapper is the structured data document. In this sense, Mindwrapper applications are much like a spreadsheet templates except that the defined data structures are hierarchical rather than tabular (as are the user interfaces).
...
...
Directory. ...
Database file. ...
Assemble()Assemble()Assemble()