# Lorenz attractor drawing for the AxiDraw # Lindsay Wilson 31/05/16 # This calculates the Lorenz attractor in real time and outputs it to the AxiDraw. # Also triggers RB3 output so can be used for light painting. # Code for the Lorenz attractor from http://matplotlib.org/examples/mplot3d/lorenz_attractor.html # Requires Evilmadscientist's ebb_serial from https://github.com/evil-mad/plotink # The spatial limits of the attractor are the following: # X -19 to +22 # Y -25 to +30 # Z 0 to +55 # The calculated coordinates are first normalized to the range 0..1, then scaled to # the desired output drawing size # Imports import numpy as np import serial import ebb_serial from math import sqrt import time import sys # Specify drawing output scale on AxiDraw (mm) xScale=200 yScale=200 # Specify which plane to draw # 1=XY, 2=YZ, 3=XZ drawPlane=3 # Specify timestep and maximum steps # 6000 gives a good maximum for a big drawing dt=0.01 stepCnt=6000 # Definitions # currentX and currentY keep track of the current POSITION of the machine, in terms # of motor steps. This is done so we can move to a particular position, for example. # Maximum step frequency is 25kHz. Remember to account for 1.4x on diagonal moves. # At 16x microstepping, max speed is 220mm/s. At 8x, it's 440mm/s. currentX=0 # Leave at 0 currentY=0 # Leave at 0 stepsPerUnit=80 # Number of motor steps per real-world unit. 80 for mm, 2032 for inches, assuming 16x microstepping. velocity=70 # Global move velocity # Pen heights and delays penUpHeight=20000 penDownHeight=13000 penUpDelay=0.75 penDownDelay=0.75 # Servo raise/lower rate servoRate=1000 # Set up EBB - open port and send setup commands (motor mode, pen up/down positions etc) def ebb_init(): global ebbPort ebbPort=ebb_serial.openPort() if ebbPort is None: print("Error finding port") quit() ebb_serial.command(ebbPort,"SC,1,1\r") # Servo output only ebb_serial.command(ebbPort,"EM,1,1\r") # Motors on, 16x microstepping ebb_serial.command(ebbPort,"SC,10,"+str(servoRate)+"\r") ebb_serial.command(ebbPort,"SC,4,"+str(penUpHeight)+"\r") # Set pen up height ebb_serial.command(ebbPort,"SC,5,"+str(penDownHeight)+"\r") # Set pen down height ebb_serial.command(ebbPort,"SP,1\r") # Move pen max up ebb_serial.command(ebbPort,"PD,B,3,0\r") # RB3 as output ebb_serial.command(ebbPort,"PO,B,3,0\r") # RB3 low # Go to XY position (in real units) at fixed velocity def goto_xy(x,y,v): # Global thingy is needed to ensure Python knows these two are global variables global currentX global currentY # Calculate final position in terms of motor steps, rounded to the nearest step finalxsteps=long(x*stepsPerUnit) finalysteps=long(y*stepsPerUnit) # Calculate relative distances to move (in motor steps) relx=finalxsteps-currentX rely=finalysteps-currentY # Calculate move time in ms, and handle cases where it's zero movetime=long(sqrt((relx**2)+(rely**2))*1000/(v*stepsPerUnit)) if movetime==0: movetime=1 # Send move command to EBB ebb_serial.command(ebbPort,"XM,"+str(movetime)+","+str(relx)+","+str(-rely)+"\r") # Update current x and y positions - remember, these are in terms of motor steps currentX=finalxsteps currentY=finalysteps def laserOn(): ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 1 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 2 ebb_serial.command(ebbPort,"PO,B,3,1\r") def laserOff(): ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 1 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 2 ebb_serial.command(ebbPort,"PO,B,3,0\r") def penUp(): ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 1 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 2 ebb_serial.command(ebbPort,"SP,1\r") time.sleep(penUpDelay) def penDown(): ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 1 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 2 ebb_serial.command(ebbPort,"SP,0\r") time.sleep(penDownDelay) def lorenz(x, y, z, s=10, r=28, b=2.667): x_dot = s*(y - x) y_dot = r*x - y - x*z z_dot = x*y - b*z return x_dot, y_dot, z_dot def penChange(): penUp() raw_input("Change pen - press enter to continue") penDown() ###################### # Main program start # ###################### # Init EBB ebb_init() # Initialise the arrays xs = np.empty((stepCnt + 1,)) ys = np.empty((stepCnt + 1,)) zs = np.empty((stepCnt + 1,)) # Setting initial values xs[0], ys[0], zs[0] = (0., 1., 1.05) # Open output file f=open("out.txt","w") # Go to starting point, depending on plane chosen and scaling factors # Calculate normalised values based on maximum limits xn=(xs[0]+19)/41 yn=(ys[0]+25)/55 zn=zs[0]/55 if drawPlane==1: goto_xy(xn*xScale,(yn-1)*yScale,velocity) elif drawPlane==2: goto_xy(yn*xScale,(zn-1)*yScale,velocity) elif drawPlane==3: goto_xy(xn*xScale,(zn-1)*yScale,velocity) # Pen down laserOn() penDown() # Stepping through "time" for i in range(stepCnt): # Derivatives of the X, Y, Z state x_dot, y_dot, z_dot = lorenz(xs[i], ys[i], zs[i]) xs[i + 1] = xs[i] + (x_dot * dt) ys[i + 1] = ys[i] + (y_dot * dt) zs[i + 1] = zs[i] + (z_dot * dt) # Calculate normalised values based on maximum limits xn=(xs[i]+19)/41 yn=(ys[i]+25)/55 zn=zs[i]/55 # Depending on selected plane, draw output if drawPlane==1: goto_xy(xn*xScale,(yn-1)*yScale,velocity) f.write(str(xn)+","+str(yn)+"\n") elif drawPlane==2: goto_xy(yn*xScale,(zn-1)*yScale,velocity) f.write(str(yn)+","+str(zn)+"\n") elif drawPlane==3: goto_xy(xn*xScale,(zn-1)*yScale,velocity) f.write(str(xn)+","+str(zn)+"\n") # If we're at a pen change point, pause and tell user to change the pen #if i==2000 or i==4000: # penChange() # Close file f.close() # Pen up penUp() laserOff() # Move back to 0,0 goto_xy(0,0,velocity) # Turn motors off - remember to use a couple of dummy blocking commands # to prevent parser from jumping straight to the EM,0,0 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 1 ebb_serial.command(ebbPort,"SM,1,0,0"+"\r") # Blocking 2 ebb_serial.command(ebbPort,"EM,0,0"+"\r") # Close port ebb_serial.closePort(ebbPort)