DC Zope/Python User Group
  January 16, 2002
A.M. Kuchling
  www.amk.ca
  amk @ amk.ca
MEMS = MicroElectroMechanical Systems
  They combine perception, computation, and actuation.
|  |  |  |  | 
| Inertial Measurement | Accelerometers, gyroscopes, vibration sensors | 
| Microfluidics | Gene chips, lab on a chip, chemical sensors, flow controllers, microvalves, micronozzles | 
| Optical MEMS | Optical switches, displays, adative optics | 
| Pressure Measurement | Auto sensors, medical sensors, industrial sensors | 
| RF Technology | RF switches, filters, variable capacitors, inductors, antennas, phase shifters, scanned aperatures | 
| Other | Data storage, picosatellites, actuators, etc. | 
 
  | IC
      Processes Oxidation Diffusion LPCVD Photolithography Epitaxy Sputtering Ion Implantation Etc. | Micromachining
      Processes Bulk Micromachining Surface Micromachining Wafer Bonding LIGA Deep Silicon Reactive Ion Etching Micromolding Etc. | 
The MEMS Exchange will perform a process sequence in order to create a device. The process sequence can be spread across several individual fabrication facilities for the sake of process and design freedom.
The MEMS Exchange virtual fabrication network allows flexibility in:
Academic
Commercial:
 
   
   
   
   
   
   
  First step toward building Web-based tools.
Checks a sequence for violations of several different rules.
Also graphically displays the layers being built up.
ProcessLibrary  <<database root
  BaseProcess
    Parameter
      PhysicalValue
  ProcessModule
    ...
UserDatabase    <<database root
  User
  Group
RunDatabase     <<database root
  ProcessRun
    ProcessSequence
      [ProcessStep1, ...]         
         Parameter
TemplateLibrary, BusinessDatabase, MaterialDatabase, ...
  Instead, we use the ZODB. Our database has:
Features:
Note: ACID = Atomicity, Consistency, Isolation, Durability
from ZODB import DB
from ZODB.FileStorage import FileStorage
storage = FileStorage('/tmp/test-filestorage.fs')
db = DB(storage)
conn = db.open()
root = db.root()
user_db = root['user_db']
newuser = User('amk') 
userdb.users['amk'] = newuser
get_transaction().commit()
  Replace FileStorage with
  BerkeleyStorage to use a different low-level storage
  mechanism.
ZEO consists of a ClientStorage class which
  retrieves objects over a socket, and a server that the
  ClientStorage can talk to.
That's it! There are a few rules to keep in mind, though...
import ZODB
from Persistence import Persistent
class User(Persistent):
    def __init__ (self):
        self.email = None
        self.names = []
    
    def add_name (self, name):
        self.names.append(name)
        self._p_changed = 1
  _p_changed)isinstance(), issubclass() don't
    work.__cmp__, __r*__, ...)__setattr__, __delattr__ must set
    dirty bit manuallyAn updated version for Python 2.2 should make #2 and #3 go away (eventually).
<User at 0000000000000ef3: akuchlin>
  prefix/first/last/suffix: Mr/Andrew/Kuchling/
  email: 'amk@amk.ca'
  address: <Address at 0000000000000f2d>
    ...
  _p_changed: false
  
<Address at 0000000000000f2d>
  street1/2/3: 1320 N. Veitch St., #608//
  city/state/zip: Arlington/VA/22201
    ...
  _p_changed: false
  
ludwig akuchlin>opendb
root databases available:
  template_lib
  process_lib
  run_db
  user_db
  business_db
  results_db
  session_manager
  shared_cache
 
other variables and functions:
  database
  connection
  root
  commit() = get_transaction().commit()
  abort()  = get_transaction().abort()
  sync()   = connection.sync()
>>> r = run_db.get_run(113)
>>> r.owner
<User at 839e2f8: akuchlin>
>>> r.owner = user_db.get_user('gward')
>>> r.owner
<User at 83a0348: gward>
>>> commit()        # commit the transaction
  
class ProcessRun(MXPersistent):
    """
    Instance attributes:
      run_id : int
        the unique identifier for this process run
      name : string
        the user-supplied name of this run
      owner : User    # notnone
        the individual user who owns this run
      billed : DateTime | boolean
        whether or not we've sent a bill to the customer for this run
      review_comments : PersistentList [Comment]
        An always-growing list of comments...
    """
  
Type-checking discovered database errors; here they are:
 
run_db.runs[639].object_versions[6].sequence._key_map['S0002'].wafer_ids['W001']
     .description.resistivity.unit.dims:
  expected attribute 'dimension_names' not found
run_db.runs[639].object_versions[6].sequence._key_map['S0002'].wafer_ids['W001']
     .description.resistivity.unit.dims:
  expected attribute 'num_dimensions' not found
run_db.runs[639].object_versions[6].sequence._key_map['S0002'].wafer_ids['W001']
     .description.resistivity.unit.dims:
  expected attribute 'dimension_powers' not found
make: *** [check] Error 1
  ute tools>MX_DB=file:/www/var/mxdb.fs python zodb_census.py opening database... expecting to see 306153 objects maximum expected OID: 00000000000abe43 OID: 00000000000abe44 (objects seen: 306153) census completed total OIDs attempted: 704068 empty slots seen: 397915 actual objects seen: 306153 objects seen by type: ActiveVersionCollection 963 Address 1861 AlignmentMark 51 BaseProcess 2324 BusinessDatabase 1 ... OOBTree 15 OOBucket 296 Parameter 29585 ParameterList 17966 ProcessHierarchy 1 ProcessLibrary 1 ProcessModule 840 ProcessRun 1885 ProcessSequence 1885 ProcessStep 16990 RunDatabase 1 ...
Builds an index of the references in the ZODB's object graph, and lets you explore it.
(Finding a bug, as demonstrated by Greg Ward on our internal mailing list)
First, a mystery:
$ opendb [...] >>> len(user_db.users) 775 $ make census [...] User 873
There are almost 100 more User objects in the ZODB than users
  in our UserDatabase object.
Let's list those 874 objects, sorted by user ID:
$ ./tools/zodb_index.py -C mems.access.user.User | sort -f -b +4 [...] 00000000000380d0: <User at 83805c8: akuchlin> 00000000000010ca: <User at 83278d8: gward> 0000000000000011: <User at 816cd18: Hexon> 000000000001a5d1: <User at 8332608: Hexon> 000000000001e4b1: <User at 833a138: Hexon> 0000000000021943: <User at 83437c0: Hexon> 0000000000023c4c: <User at 8346be8: Hexon> 00000000000317af: <User at 82c0578: Hexon> 00000000000010c9: <User at 83258d0: wbenard>
6 objects for the user "Hexon"?!?
Running zodb_index.py -r 0000000000000011 shows
  251 refs to OID 11, so we'll guess that OID 11 is The One True
  User Object for Hexon, and the others are impostors.
Let's dig deeper...
 
  $ ./tools/zodb_index.py -r 1a5d1 1e4b1 21943 23c4c 317af
  000000000001a5d1: <User at 81e4768: Hexon>
    000000000001a5d0: <Review at 8220698>
    000000000001a5d2: <FabProvider at 82248d0: UMich>
    000000000001a5d2: <FabProvider at 82248d0: UMich>
    000000000001a608: <Comment at 8231b88: Hexon at 2001-04-27 14:34:31.51>
    0000000000060994: <Review at 8230a28>
    0000000000060b00: <Comment at 822ff60: Hexon at 2001-04-27 14:34:31.51>
  000000000001e4b1: <User at 8224550: Hexon>
    000000000001e4b0: <Review at 8232308>
    000000000001e4b2: <FabProvider at 822ff00: UMich>
    000000000001e4b2: <FabProvider at 822ff00: UMich>
    000000000001e5e7: <Comment at 8233138: Hexon at 2001-04-25 15:58:12.17>
  0000000000021943: <User at 81e3a38: Hexon>
    0000000000021942: <Review at 8232ca0>
  ...
  zodb_index-style reference
    indexing?We have over 4000 tests for our basic objects. The existence of this test suite gives us confidence when we refactor the code.
Consider this simple function:
def f(s, val):
    if val < 0:
        raise ValueError, 'val cannot be negative'
    elif val == 42:
        print 'The answer!'
 
    return s * val
  
from quixote.test.unittest import TestScenario, parse_args, run_scenarios
import module
 
tested_modules = ['module']
class MyFunctionTest (TestScenario):
    def setup(self): pass
    def shutdown(self): pass
    def check_func(self):
        "Test the function's output: 6"
 
        # Test the null case (val == 0)
        self.test_val( "module.f('', 0)", '')
        self.test_val( "module.f('abc', 0)", '')
 
        # Test the identity (val == 1)
        self.test_val( "module.f('', 1)", '')
        self.test_val( "module.f('abc', 1)", 'abc')
 
        # Test a real case (val == 3)
        self.test_val( "module.f('', 3)", '')
        self.test_val( "module.f('abc', 3)", 'abcabcabc')
 
if __name__ == "__main__":
    (scenarios, options) = parse_args()
    run_scenarios (scenarios, options)
  
ute /tmp>python test.py
FunctionTest:
  ok: Test the function's output ('func') (6 tests passed)
passed) ok: 9 tests passed
ute /tmp>
  Or, in case of failure:
ute /tmp>python test.py
MyFunctionTest:
  not ok: Test the function's output ('func') (6 tests expected, 6 run, 2 failed)
    not ok: module.f('', 0) != '' (raised ValueError: "val cannot be negative")
      failed at test.py, line 12 (in 'check_func()'):
        self.test_val( "module.f('', 0)", '')
        File "/home/amk/src/mems/quixote/test/unittest.py", line 337, in test_val
          val = eval (code, globals, locals)
        File "<string>", line 0, in ?
        File "module.py", line 3, in f
          raise ValueError, 'val cannot be negative'
    not ok: module.f('abc', 0) != '' (raised ValueError: "val cannot be negative")
      ...
not ok: 6 tests expected, 6 run, 2 failed
ute /tmp>
  
ute /tmp>python test.py -c
MyFunctionTest:
  ok: Test the function's output ('func') (6 tests passed)
passed) ok: 6 tests passed
code coverage:
  module:  83.3% (5/6)
ute /tmp>python test.py -c -v
  ... additional output while running the tests deleted ...
ok: 6 tests passed
code coverage:
  module:
      .
    10: def f(s, val):
      9:    if val < 0:
      1:        raise ValueError, 'val cannot be negative'
      8:    elif val == 42:
  >>>>>>        print 'The answer!'
      .
      8:    return s * val
  83.3% (5/6)
kronos /tmp>
  ute proto3>~/src/mems/tools/run_tests.py -r . looking for test scripts...found 45 ok: lib/test/test_pvalue.py: 118 tests passed ok: lib/test/test_range.py: 129 tests passed ok: lib/test/test_unit.py: 109 tests passed ... ok: template/test/test_eqtemplate.py: 14 tests passed ok: prc/test/test_inter.py: 0 tests passed ok: 4000 tests passed
Code coverage: grouch.valuetype: 36.6% (168/459) mems.run.process_run: 43.1% (202/469) mems.tools.unittest: 45.4% (237/522) mems.lib.base: 46.9% (75/160) grouch.util: 48.1% (26/54) mems.process.process_module: 52.4% (100/191) ...
Quixote is our environment for building Web applications.
Design goals:
Features:
An app is a Python package whose name is specified in a
  config. file. (mems.ui,
  quixote.demo)
webapp/
    __init__.py                     
    module1.py
    module2.py
    pages1.ptl
    pages2.ptl
  A URL is mapped to a callable Python object by traversing objects starting from the base package.
| http://www/ | calls | webapp._q_index | 
| http://www/simple | calls | webapp.simple | 
| http://www/run/ | calls | webapp.run._q_index | 
__init__.py:
_q_exports = ["simple", "error", "widgets"]
import sys
from quixote.demo.pages import _q_index
from quixote.demo.widgets import widgets
from quixote.demo.integer_ui import IntegerUI
def simple (request):
    # This function returns a plain text document, not HTML.
    request.response.set_content_type("text/plain")
    return "This is the Python function 'quixote.demo.simple'.\n"
  request wraps up:
Most templating syntaxes look like HTML with a bit of additional syntax.
| PHP | <? for ($i=1; $i<10; $i++) print $i;> | 
| ASP | 
<% addr = Request.form("email"); %>
 | 
| DTML | <dtml-var "row"> or <!--#var "row"--> | 
Our syntax looks completely different, though still familiar...
pages.ptl:
template _q_index(request):
    print "debug message from the index page"
    """
    <html>
    <head><title>Quixote Demo</title></head>
    <body>
    <h1>Hello, world!</h1>
    """
    "<table>"
    for i in range(10):
        make_row(i)
    "</table>"
template make_row(num):
    "<tr><td colspan=%i>%i</td></tr>" % (num, num)
  When Python evaluates a lone expression, it discards the result:
def f():
    string.lower('abc')
    return 1
  In PTL, the result is converted to a string and appended to the output.
template numbers(n):
    for i in range(n):
        i
        " " # add whitespace
  An import hook lets us import PTL files as if they're regular Python modules:
from quixote import enable_ptl enable_ptl() from webapp.pages import simple
vi and Emacs modes for
    it._q_index (request) : string
  When traversal stops at a module or package, this is the default
  function name that's tried.
_q_access (request)
  When found along the way, this function is called and must not
  raise an exception to let traversal continue.
_q_getname (request, component) : object
  When a name isn't found, _q_getname will be called.
  If it returns an object, traversal will continue with this
  object.
Access control function; traversal can go no further if it raises an exception.
  def _q_access (request):
    from mems.ui.lib.errors import NotLoggedInError
    if request.session.user is None:
        raise NotLoggedInError, ("You must be signed in to view runcards.")
  This saves us from having to write checking code for every public function. Instead, we can just put an access restriction on the whole module.
"/run/200/" is a more readable URL than "/run/?run_id=200".
_q_getname gets called with the current request
  and the component of the URL path. If it returns an object,
  traversal continues with that object.
mems/ui/run/__init__.py
def _q_getname (request, component):
    return RunUI(request, component)
class RunUI:
    _q_exports = ['details', 'check', ...]
    def __init__ (self, request, component):
        run_db = get_run_database()
        self.run = run_db.get_run(int(component)) 
    template _q_index (self, request):
        # /run/200/
        ... return index page ...
    template details (self, request):
        # /run/200/details/
        ... return a more detailed page ...
  An optional part of Quixote that lives in the
  quixote.form subpackage, the framework lets us
  quickly implement basic (and not so basic) HTML forms.
Features:
Is a container for child widgets, and has three methods:
Widgets then produce HTML, and handle parsing of the resulting form element.
class LoginForm (Form):
    def __init__ (self, request):
        Form.__init__(self)
            
        self.add_widget(
            "string", "user_id",
            value=self.default_user_id,
            title="User ID",
            size=15, maxlength=32)
        self.add_widget(
            "password", "password",
            title="Password",
            size=15, maxlength=32)
        self.add_submit_button(name='login', value="Sign In")
    template render (self, request, action_url):
        title = "Sign In to the MEMS Exchange"
        standard.header(request, title, tree_info=[])
        for widget in self.widget_order:
            widget.render(request)
            "<br>"
            
        for button in self.submit_buttons:
            button.render(request)
        standard.footer(title)
  
    def process (self, request):
        form_data = Form.process(self, request)
        user_id = form_data["user_id"]
        password = form_data["password"]
        if user ID check fails:
            self.error['user_id'] = "Invalid user ID"
        elif not valid_password(user, password):
            self.error['password'] = "Invalid password"
        self.user = user
        return form_data
    def action (self, request, submit, form_data):
        request.session.set_user(self.user)
        self.user.record_login(request.get_environ("REMOTE_ADDR"))
  These slides:
  http://www.amk.ca/talks/
More on the MX toolset:
  http://www.amk.ca/python/writing/mx-architecture/
ZODB/ZEO package:
  http://www.amk.ca/zodb/
Grouch:
  http://www.mems-exchange.org/software/grouch/
Quixote:
  http://www.mems-exchange.org/software/quixote/
Join the quixote-users mailing list.