Python notes 6/28/07


More looping

A simple 2nd order loop and oscillation can be created out of 2 integrators and one additive inverse:

>>> from pwb import *
>>> from components import *
>>> c1 = Integrate('integrate 1')
>>> c2 = Integrate('integrate 2')
>>> c3 = Minus('minus')
>>> c4 = Display2('display')
>>> connect(c1, c2)
>>> connect(c2, c3)
>>> connect(c3, c1)
>>> connect(c2, c4)
>>> c1.sum = 1.0
>>> run(100)
---
---
integrate 2 got 1.0 from integrate 1
minus got 0.0 from integrate 2
display got 0.0 from integrate 2
etc...

The Integrate component uses Euler's method, which is not sufficient to make the oscillation stable. However, by adding a little damping, the system can be made pseudo-stable:

>>> reset()
>>> c1.dt = 0.25
>>> c2.dt = 0.25
>>> c1.damping = 0.9445
>>> c2.damping = 0.9445
>>> c1.sum = 1.0
>>> run(100)
---
---
integrate 2 got 0.94450000000000001 from integrate 1
minus got 0.0 from integrate 2
display got 0.0 from integrate 2
etc...

Stand-alone programs

Although you can continue to use PWB components and displays from within an interactive Python session, most component programs will be run as stand-alone Python programs. This also improves the performance of graphics, especially of input controls (see below). Here is the previous program rendered as a stand-alone (also in file test1.py):

import pwb
import components
import time

c1 = components.Integrate('integrate 1')
c2 = components.Integrate('integrate 2')
c3 = components.Minus('minus')
c4 = components.Display2('display')
pwb.connect(c1, c2)
pwb.connect(c2, c3)
pwb.connect(c3, c1)
pwb.connect(c2, c4)
c1.dt = 0.25
c2.dt = 0.25
c1.damping = 0.9445
c2.damping = 0.9445
c1.sum = 1.0

while not pwb.done():
   pwb.run(1)
   time.sleep(0.1)

This program can be run with:

python test1.py

Unfortunately, since none of the components in the program is ever done, the program runs forever until you kill it with control-C. One way to prevent this is by using a command line argument to set the number of loops (also in file test2.py):

import pwb
import components
import time
import sys
...
for i in xrange(int(sys.argv[1])):
   pwb.run(1)
   time.sleep(0.1)

This program can be run with:

python test2.py 1000

Slider input

PWB components can receive input from sliders and other GUI controls (defined in controls.py). For example, in the following program (test3.py) a slider is used to set the damping of the integration components from the previous program:

import pwb
import components
import time
import sys

c1 = components.Integrate('integrate 1')
c2 = components.Integrate('integrate 2')
c3 = components.Minus('minus')
c4 = components.Display2('display')
c5 = components.Slider('Damping')
pwb.connect(c1, c2)
pwb.connect(c2, c3)
pwb.connect(c3, c1)
pwb.connect(c2, c4)
c1.dt = 0.25
c2.dt = 0.25
c1.sum = 1.0

for i in xrange(int(sys.argv[1])):
   pwb.run(1)
   damping = 0.9 + 0.1 * c5.value
   print 'Damping set to ' + repr(damping)
   c1.damping = damping
   c2.damping = damping
   time.sleep(0.1)

Run control

A program containing an infinite loop can be controlled by a Button component and a little bit of logic. Here is an example (test4.py) of using such a control to stop, start, and exit the loop of the previous program:

Image programs

Here is an example of using 3 sliders to circumscribe a planet (test5.py, images in images):

Event-driven programs

It's not necessary to have all the components running all the time in order to create a program like the last one. The sliders can be configured to send data to the image display only when their values change. This improves the GUI performance considerably. In addition, 3 sliders for X, Y, and Radius can be combined in a single window. In order to do this, the Slider code must be edited so that it sends data in the callback handler, rather than in 'run':

class Slider2(Slider):
   def __init__(self, name, n=1):
      pwb.Component.__init__(self, name)
      self.value = 1
      # Create window, but not app
      # Set number of sliders desired
      self.window = controls.SliderWindow(self.name, self.handler, n)
      self.window.show()
      
   def handler(self, tag, newValue):
      self.value = newValue
      print self.name + ": tag = " + repr(tag) + " value = " + repr(newValue)
      # Send both slider tag and value to all outputs
      for o in range(len(self.outputs)):
         self.send(o, (tag, self.value))

The image component must also be edited so that it can receive data directly into its input buffers, and then parse that data based on tag and value from just one input:

class ImageCircle2(pwb.Component):
   def __init__(self, name, data, min=0.0, max=0.0, x=100.0, y=100.0, r=100.0):
      pwb.Component.__init__(self, name)
      self.x = x
      self.y = y
      self.r = r
      # Create window, but not app
      self.window = plots.ImageCircleWindow(self.name, data, min, max, self.x, self.y, self.r)
      self.window.show()
      
   def run(self):
      if self.done(): return
      if len(self.inputs) > 0 and len(self.inputs[0]['queue']) > 0:
         # Get slider tag and value from one input
         e = self.inputs[0]['queue'].pop(0)
         tag = e[0]
         value = e[1]
         # Set x
         if tag == 0:
            self.x = value * self.window.pixmap.width()
         # Set y
         elif tag == 1:
            self.y = (1.0 - value) * self.window.pixmap.height()
         # Set radius
         elif tag == 2:
            self.r = value * sqrt(self.window.pixmap.width() * \
               self.window.pixmap.width() + self.window.pixmap.height() * \
               self.window.pixmap.height()) / 2.0
      if self.x != self.window.x or self.y != self.window.y or self.r != self.window.r:
         # Update display
         self.window.x = self.x
         self.window.y = self.y
         self.window.r = self.r
         self.window.update()

   # Overload 'receive' function from Component class to put data directly into
   # queue rather than intermediate buffer
   def receive(self, input, data):
      for i in self.inputs:
         if i['component'] == input:
            i['queue'].append(data)
      self.run()
      
   def reset(self):
      pwb.Component.reset(self)
      self.x = 100
      self.y = 100
      self.r = 100
      self.window.x = self.x
      self.window.y = self.y
      self.window.r = self.r
      self.window.update()

The new program (test6.py) is much simpler, and makes use of the Qt application event loop:

# Python Workbench prototype
# Draw a circle around a planet using 3 sliders
# (C)Sky Coyote, June 2007

# Import required code
import pwb
import components
from PyQt4 import QtCore, QtGui
import pyfits

app = QtGui.QApplication([])

c1 = components.Slider2('X - Y - R', 3)
c2 = components.ImageCircle2('Image', pyfits.getdata('images/c1230.en.fits'))

pwb.connect(c1, c2)

app.exec_()

What's next?

I hope you can tell me! Obviously this stuff seems to work well, and can be extended to create programs such as FITSRegister, FITSFlow, and possibly the legendary sought after 'SuperFITS'. But should it do so? I hope that we can discuss this issue, and decide on what to do next month, at Friday's virtual meeting. I think we should proceed with Python, but the time for evaluating different packages, etc..., seems to have passed. I would recommend that we consider committing to something more formidable for the next few months, whatever it might be.


İSky Coyote 2007