Creating a model by using Mekanimo’s GUI and then writing a script to perform some tasks is a very powerful technique to create sophisticated designs. The following tutorial demonstrates this mixed-mode (GUI + scripting) approach with a short example.
First, we will create a simple mechanism with a tracer to create pretty graphics by using the GUI. Then conduct a simple parametric study to generate many different graphic designs by writing a short script. And finally, we will create a collage (similar to Figure-1 but with 16 pictures instead of 4) of these designs by using PIL (Python Imaging Library).
Create the model shown in Figure-2 (if you set the grid and snap to grid options on, this task should take less than a minute) or copy and paste the code shown in mekanograf.mek. Your model should look like Figure-2. After you created the model, deactivate the box. Then to prevent the links from colliding with each oter, select all rectangles and then
Object -> Never collide
Mekanimo automatically converts the GUI actions into Python code and displays it in the Code panel. Your code may look different.
You can copy and paste this code snippet into Mekanimo’s code panel if you would like to have an identical model to the one in Figure-2.
rectangle(x=25.0, y=26.875, width=15.0, height=1.25,
name='rectangle1', alias='r1', fillColor=(234, 240, 109, 150))
rectangle(x=31.875, y=21.25, width=1.25, height=12.5,
name='rectangle2', alias='r2', fillColor=(113, 188, 167, 150))
rectangle(x=35.625, y=15.625, width=8.75, height=1.25,
name='rectangle3', alias='r3', fillColor=(147, 146, 122, 150))
motor(body1=rectangle2, body2=rectangle3, b1x=0.0, b1y=-5.625,
rpm=120, torqueLimit=5000, name='motor1', alias='m1')
motor(body1=rectangle1, body2=rectangle2, b1x=6.875, b1y=0.0,
rpm=120, torqueLimit=5000, name='motor2', alias='m2')
motor(body1=rectangle1, body2=ground, b1x=-6.875, b1y=0.0,
rpm=120, torqueLimit=5000, name='motor3', alias='m3')
trace(body=rectangle3, bx=3.75, by=0.0,
name='trace1', alias='t1')
system.box.active = False
noCollision([rectangle1, rectangle2, rectangle3])
motor1.rpm = 50
motor2.rpm = 50
motor3.rpm = 30
Since our final product is a graphic design we need to hide the mechanical elements. To do this:
First hide everything: View -> Hide -> All
Then show only the trace elements: View -> Show -> Trace
We can also change the characteristics of the trace as shown below. Now, your results should look like the animation shown in Figure-5.
>>> trace1.pen.Width = 2
>>> trace1.pen.Colour = color.navy
>>> trace1.pen.Style = wx.DOT_DASH
The default values for trace.pen are:
Width: 1
Colour: (155, 155, 155, 255)
Style: wx.SOLID
Other available line-styles:
The steps we need to take can be summarized as :
- Run several different analyses with different rpm values and at the end of the each run capture the screen and save it into a file.
- As the final step, stitch these pictures together to form a collage like the one shown in Figure-1.
Step-1 can be further subdivided:
- Come up with a scheme to vary the RPM values.
- Run analyses according to this scheme.
- Capture screen at the end of each run.
If you are an experienced programmer you can skip the steps and study the final script. Individual steps are shown for the benefit of beginners.
>>> system.run(500) # run an analysis for 500 time-frames
>>> canvas.captureScreen('mekanograf_test.jpg') # Now save the canvas graphics as a pic.
>>> n = 6
>>> incRPM = 7 # motor1's RPM increment for each iteration
>>> nofframes = 1500 # Number of time-frames
>>> picbasename = 'mekanograf'
>>> for i in range(n):
... system.reinit()
... motor1.rpm += incRPM
... system.run(nofframes)
... picname = '%s_%s_%s_%s.jpg' %(picbasename, m1.rpm, m2.rpm, m3.rpm)
... canvas.captureScreen(picname)
...
Code snippet above increments the motor1’s rpm value by a constant value (incRPM) on each iteration. In this case we have three motors and we may want to conduct a parametric study. The most comprehensive approach would be covering all combinations, but in many cases this is not practical. If we had only three parameters and three levels we could cover every possible case in 27 experiments (3**3 = 27). The code snippet below shows how to generate all combinations for three motors and three rpm levels.
>>> levels = (20, 60, 100) #low, mid, high rpm values
>>> for i in levels:
... for j in levels:
... for k in levels:
... print i, j, k
...
20 20 20
20 20 60
20 20 100
20 60 20
20 60 60
20 60 100
20 100 20
20 100 60
20 100 100
60 20 20
60 20 60
60 20 100
60 60 20
60 60 60
60 60 100
60 100 20
60 100 60
60 100 100
100 20 20
100 20 60
100 20 100
100 60 20
100 60 60
100 60 100
100 100 20
100 100 60
100 100 100
Since the range of rpm values we would like to cover are rather large, a full factorial design is out of question (it would be over 22 million combinations) for today’s hardware. Generating random combinations should be good enough for our purposes. Python’s random module provides many different functions to generate random numbers according to various distributions (gauss, circular uniform, gamma, log normal, etc.).
>>> from random import choice
>>> maxRPM = 140
>>> rpms = range(-maxRPM, maxRPM) # Negative sign indicates clockwise rotation
>>> choice(rpms)
90
>>> choice(rpms)
-57
>>> (choice(rpms), choice(rpms), choice(rpms))
(60, -130, 80)
>>> combinations = [ ]
>>> n = 16
>>> for i in range(n):
... combinations.append((choice(rpms), choice(rpms), choice(rpms)))
...
>>> combinations
[(126, 71, 135), (71, 4, 33), (-45, -20, -122), (-56, 25, 109),
(85, -61, -10), (-17, -114, -29), (79, 34, 44), (-79, -67, 65),
(137, -40, -136), (-129, -136, 12), (76, -59, 67), (22, -50, 38),
(-57, 25, 51), (-75, -40, 68), (-113, -15, 126), (-107, -32, -49)]
Can you see the problem with the code snippet above?
The problem is we can end up with the same combination more than once and it would be annoying to see the same pattern more than once in our collage. This is a subtle bug. These types of bugs are very difficult to track down since they may never show up when you test your program. What do you think we can do to prevent these types of bugs? Please give me feedback if you have a solution, I would like to hear about other solutions before I tell you my solution.
To address this, we can use a set to make sure we have no duplicates in the randomly generated combinations as shown below. Sets cannot contain any duplicates by definition.
>>> combinations = set() # Start with an empty set
>>> n = 16
>>> while len(combinations) < n:
... combinations.add((choice(levels), choice(levels), choice(levels)))
...
>>> combinations
set([(10, -65, 8), (83, -91, 38), (-106, 78, 127), (117, -58, -118),
(139, 94, 73), (63, -88, -24), (71, -95, 28), (119, 30, -20),
(-4, 13, 108), (-86, 5, -111), (136, 95, 84), (25, 71, 29),
(84, 134, 58), (-90, -95, -20), (-3, -67, -4), (31, 93, 60)])
Since we learned how to generate the individual pictures, the last thing we need to figure out is how to put these pictures together in a collage. For this task we will use an external library called PIL. PIL is an excellent image manipulation library written by Fredrik Lundh. For more information about PIL please visit http://www.pythonware.com/products/pil/ . Mekanimo includes PIL so you don’t need to do anything other than importing it. The code snippet below demonstrates how to put two pictures in a single file.
bbox may be the most difficult to understand part of the code below without reading the PIL manual’s paste function definition. bbox stands for bounding box and in this case it can be specified by the upper left and lower right corners’ coordinate values (in pixels).
>>> from PIL import Image
>>> image1 = Image.open('first.jpg')
>>> image2 = Image.open('second.jpg')
>>> width1, height1 = image1.size
>>> width2, height2 = image2.size
>>> width = width1 + width2
>>> height = max([height1, height2])
>>> base = Image.new('RGB', (width, height))
>>> bbox1 = (0, 0, width1, height1)
>>> base.paste(image1, bbox1)
>>> bbox2 = (width1, 0, width, height2)
>>> base.paste(image2, bbox2)
>>> base.save('first_second.jpg')
See http://www.pythonware.com/library/pil/handbook/image.htm for more info about Image.paste method.
We have seen examples of each required step to complete our task. The code below shows the final script. Please note the way this code is organized. All the parameters are listed along with comments explaining what they are in the first section, the rest of the code computes the other required values by using these parameters only. This is a simple and effective technique to create easy to maintain code. Each task is clearly defined and separated from each other. The whole script can be summarized as:
- Set design parameters and other variables
- Generate random combinations
- Run an analysis for each combination and capture the graphics into a picture
- Stitch the pictures together and save the result
from random import choice
from PIL import Image
# Set design parameters
width, height = 300, 300 # Dimensions (in pixels) of each individual pic. in the collage
nofframes = 1500 # Number of time-frames to run for each analysis
maxRPM = 140 # Maximum revolutions per minute value for the motors
rows, cols = 4, 4 # Number of pictures per row and column
# Set file names
picname = 'mekanograf' # Base name for each picture
collagename = 'mekanograf_collage.jpg' # Name of the final collage
# Generate random combinations
combinations = set() # Initiate an empty set
n = rows * cols # Total number of unique combinations
rpms = range(-maxRPM, maxRPM + 1) # Range of rpm values to choose from
while len(combinations) < n:
combinations.add((choice(rpms), choice(rpms), choice(rpms)))
# Create a picture for each combination
pictures = [ ]
for combination in combinations:
system.reinit()
motor1.rpm, motor2.rpm, motor3.rpm = combination
system.run(nofframes)
pic = '%s_%s_%s_%s.jpg' %(picname, motor1.rpm, motor2.rpm, motor3.rpm)
canvas.captureScreen(pic)
pictures.append(pic)
# Stitch the pictures together to create a collage
base = Image.new('RGB', (width * cols, height * rows))
ind = 0
for row in range(rows):
for col in range(cols):
im = Image.open(pictures[ind])
im = im.resize((width, height))
bbox = (row * width, col * height, (row + 1) * width, (col + 1) * height)
base.paste(im, bbox)
ind += 1
# Save the result to a file
base.save(collagename)
If this is a script we want to use just once we are done, just paste it in the code editor. What if we would like to use this script multiple times or share it with others? Then, we need to tidy it up a little bit to make it easier to use. For this we may want to create a function. The advantage of a function should be obvious, we can call this function with different parameters without having to modify our script every time we need a new collage.
I would also like to demonstrate another technique that I find very useful for designing easy to maintain code. In ‘Putting it all together’ we summarized the steps we need to take. Now, I will create a function for each step and the main function (collage) will be almost identical to that summary.
from random import choice
from PIL import Image
rectangle(x=25.0, y=26.875, width=15.0, height=1.25,
name='rectangle1', alias='r1', fillColor=(234, 240, 109, 150))
rectangle(x=31.875, y=21.25, width=1.25, height=12.5,
name='rectangle2', alias='r2', fillColor=(113, 188, 167, 150))
rectangle(x=35.625, y=15.625, width=8.75, height=1.25,
name='rectangle3', alias='r3', fillColor=(147, 146, 122, 150))
motor(body1=rectangle2, body2=rectangle3, b1x=0.0, b1y=-5.625,
rpm=120, torqueLimit=5000, name='motor1', alias='m1')
motor(body1=rectangle1, body2=rectangle2, b1x=6.875, b1y=0.0,
rpm=120, torqueLimit=5000, name='motor2', alias='m2')
motor(body1=rectangle1, body2=ground, b1x=-6.875, b1y=0.0,
rpm=120, torqueLimit=5000, name='motor3', alias='m3')
trace(body=rectangle3, bx=3.75, by=0.0,
name='trace1', alias='t1')
system.box.active = False
noCollision([rectangle1, rectangle2, rectangle3])
def collage(collagename, rows=4, cols=4, width=300, height=300, nofframes=1500,
maxRPM=140, picname='mekanograf'):
def gencombinations(n, maxRPM):
combinations = set()
rpms = range(-maxRPM, maxRPM + 1)
while len(combinations) < n:
combinations.add((choice(rpms), choice(rpms), choice(rpms)))
return combinations
def genpictures(combinations, picname):
pictures = [ ]
for combination in combinations:
system.reinit()
motor1.rpm, motor2.rpm, motor3.rpm = combination
system.run(nofframes)
pic = '%s_%s_%s_%s.jpg' %(picname, motor1.rpm, motor2.rpm, motor3.rpm)
canvas.captureScreen(pic)
pictures.append(pic)
return pictures
def stitchpictures(collagename, pictures, rows, cols, width, height):
base = Image.new('RGB', (width*cols, height*rows))
ind = 0
for row in range(rows):
for col in range(cols):
im = Image.open(pictures[ind])
im = im.resize((width, height))
bbox = (col*width, row*height, (col+1)*width, (row+1)*height)
base.paste(im, bbox)
ind += 1
base.save(collagename)
combinations = gencombinations(rows*cols, maxRPM)
pictures = genpictures(combinations, picname)
stitchpictures(collagename, pictures, rows, cols, widht, height)
# collage function can be invoked from the interpreter in Mekanimo as shown below
# >>> collage('spirotest.jpg', 3, 3)
Note
Function docs and comments are omitted for brevity.